1 of 58

共有

Share

荒木佑一 Yuichi Araki

2023/09/15

2 of 58

共有 / Share

​

ACTION_SEND のインテントによる共有 / Share by Intent with ACTION_SEND

3 of 58

API レベル 1 から / Since API level 1

​

fun sharePlainText(context: Context) {

val sendIntent = Intent(Intent.ACTION_SEND)

.setType("text/plain")

.putExtra(Intent.EXTRA_TEXT, "Hello, world!")

val chooserIntent = Intent.createChooser(sendIntent, null)

context.startActivity(chooserIntent)

}

4 of 58

受け取る側 / Receiver

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

​

val i = intent

if (i.action == Intent.ACTION_SEND) {

if (i.type == "text/plain") {

val text = i.getStringExtra(Intent.EXTRA_TEXT)

// Use the text.

}

}

}

5 of 58

今日話すこと / Agenda

​

​

  • 様々なデータの共有 (テキスト、画像、複数)�Share various data (text, image, multiple)
  • 直接共有�Direct share
  • 共有シート�Sharesheet

5

6 of 58

送る側と受け取る側 / Sender and Receiver

混同しないように注意�Don’t be confused

送る側

Sender

受け取る側

Receiver

Intent

7 of 58

様々なデータを

送る

Send various types of data

  • テキスト / Texts
  • 画像 / Images
  • 複数種類 / Multiple

7

8 of 58

テキストを送る / Send a text

​

fun sharePlainText(context: Context) {

val sendIntent = Intent(Intent.ACTION_SEND)

.setType("text/plain")

.putExtra(Intent.EXTRA_TEXT, "Hello, world!")

val chooserIntent = Intent.createChooser(sendIntent, null)

context.startActivity(chooserIntent)

}

9 of 58

ShareCompat.IntentBuilder

​

fun shareWithCompat(context: Context) {

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("text/plain")

.setText("Hello, world!")

.createChooserIntent()

)

}

10 of 58

テキストを受け取る / Receive a text

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="text/plain" />

</intent-filter>

</activity>

</application>

</manifest>

11 of 58

テキストを受け取る / Receive a text

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

​

val i = intent

if (i.action == Intent.ACTION_SEND) {

if (i.type == "text/plain") {

val text = i.getStringExtra(Intent.EXTRA_TEXT)

// Use the text.

}

}

}

12 of 58

ShareCompat.IntentReader

private fun handleIntent(context: Context, intent: Intent) {

val intentReader = ShareCompat.IntentReader(context, intent)

if (intentReader.isShareIntent) {

val text = intentReader.text

// Use text...

}

}

13 of 58

ShareCompat

送る側

Sender

受け取る側

Receiver

IntentBuilder

IntentReceiver

Intent

14 of 58

画像を送る / Send an image

​

fun shareJpegImage(context: Context, uri: Uri) {

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("image/jpeg")

.setStream(uri)

.createChooserIntent()

)

}

15 of 58

コンテント URI / Content URI

​

例: “content://com.example.sender/image/123”

アプリ間でファイルを受け渡す仕組み

​

送る側は ContentProvider

受け取る側は ContentResolver

​

16 of 58

FileProvider

<paths>

<files-path

name="images"

path="images" />

</paths>

<provider

android:name="androidx.core.content.FileProvider"

android:authorities="com.example.sender.provider"

android:exported="false"

android:grantUriPermissions="true">

<meta-data

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths" />

</provider>

res/xml/file_paths.xml

AndroidManifest.xml

17 of 58

画像を送る / Send an image

​

fun shareJpegImage(context: Context) {

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("image/jpeg")

.setStream(

FileProvider.getUriForFile(

context,

“com.example.sender.provider”,

File(context.filesDir, “images/sheep.jpg”)

)

)

.createChooserIntent()

)

}

18 of 58

画像を受け取る / Receive images

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="image/jpeg" />

</intent-filter>

</activity>

</application>

</manifest>

19 of 58

画像もテキストも受け取る / Receive images and texts

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="text/plain" />

<data android:mimeType="image/jpeg" />

</intent-filter>

</activity>

</application>

</manifest>

20 of 58

画像を受け取る / Receive an image

fun handleIntent(context: Context, intent: Intent) {

val intentReader = ShareCompat.IntentReader(context, intent)

if (intentReader.isShareIntent) {

val uri = intentReader.stream

// Use uri...

}

}

21 of 58

画像を表示する / Display the image

@Composable

private fun contentPainter(context: Context, uri: Uri): Painter {

val state = produceState<Painter>(initialValue = ColorPainter(Color.Gray)) {

withContext(Dispatchers.IO) {

try {

context.contentResolver.openInputStream(uri).use { input ->

val bitmap = BitmapFactory.decodeStream(input)

value = BitmapPainter(image = bitmap.asImageBitmap())

}

} catch (e: FileNotFoundException) {

value = ColorPainter(Color.Red)

}

}

}

return state.value

}

22 of 58

画像の権限 / Permission for the image

相手のアプリに画像へのアクセス権限を与える

Give the receiver the permission to the image

​

Send インテントに FLAG_GRANT_READ_URI_PERMISSION が必要

The send intent needs FLAG_GRANT_READ_URI_PERMISSION

​

​

ShareCompat を使っていれば気にしなくて OK

You don’t need to care about this if you use ShareCompat

23 of 58

画像の編集 / Edit an image

​

画像を共有するとき、共有シートには編集ボタンが出る

The sharesheet provides “Edit” for images

​

​

編集後に共有すると形式が変わっているかもしれない

After editing the image, its format might be different

​

おそらく PNG

24 of 58

様々な画像を受け取る / Receive various images

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="image/jpeg" />

<data android:mimeType="image/png" />

</intent-filter>

</activity>

</application>

</manifest>

25 of 58

様々な画像を受け取る / Receive various images

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="image/*" />

</intent-filter>

</activity>

</application>

</manifest>

26 of 58

画像をテキスト付きで送る / Send image + text

​

private fun shareImageWithText(context: Context, uri: Uri) {

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("image/jpeg")

.setStream(uri)

.setText("Hello, text!")

.createChooserIntent()

)

}

27 of 58

複数の画像を送る / Send multiple images

​

fun shareMultiple(context: Context, uris: List<Uri>) {

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("image/*")

.apply {

for (uri in uris) {

addStream(uri)

}

}

.createChooserIntent()

)

}

28 of 58

複数の画像を受け取る / Receive multiple images

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND_MULTIPLE" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="image/*" />

</intent-filter>

</activity>

</application>

</manifest>

29 of 58

複数の画像を受け取る / Receive multiple images

private fun handleIntent(context: Context, intent: Intent) {

val intentReader = ShareCompat.IntentReader(context, intent)

if (intentReader.isShareIntent) {

val uris = intentReader.streams

// Use uris...

}

}

​

private val ShareCompat.IntentReader.streams: List<Uri>

get() = buildList {

for (i in 0 until streamCount) {

add(getStream(i) ?: break)

}

}

30 of 58

いろいろ受け取る / Receive various data

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application …>

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<action android:name="android.intent.action.SEND_MULTIPLE" />

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.BROWSABLE" />

<data android:mimeType="text/*" />

<data android:mimeType="image/*" />

</intent-filter>

</activity>

</application>

</manifest>

31 of 58

いろいろ受け取る / Receive various data

private fun handleIntent(context: Context, intent: Intent) {

val intentReader = ShareCompat.IntentReader(context, intent)

if (intentReader.isShareIntent) {

val text = intentReader.text

val uris = intentReader.streams

intentReader.type?.let { type ->

if (type.startsWith("text/")) {

// Use text

} else if (type.startsWith("image/")) {

// Use text and uris

}

}

}

}

32 of 58

ここまでのまとめ / Summary so far

​

ShareCompat を使えば何かと楽

ShareCompat makes things easier

​

action は SEND と SEND_MULTIPLE のサポートを検討

Consider supporting both SEND and SEND_MULTIPLE actions

​

category は DEFAULT 必須、BROWSABLE 推奨

Consider specifying both DEFAULT and BROWSABLE categories

​

画像 (ファイル) 共有には FileProvider

Use FileProvider for sharing images (files)

​

mime タイプにはワイルドカード (*) が使える

Use a wildcard (*) in mime types

33 of 58

直接共有

Direct Share

  • ショートカット Shortcuts

33

34 of 58

直接共有 / Direct Share

アプリに共有するのではなく、アプリの中の連絡先に直接共有する

Directly share to a contact

​

共有シートでの利用率、約 ⅓

Approximately ⅓ of sharesheet usage

​

チャットなどソーシャル機能のあるアプリならサポートしない手はない

Should support in apps with chat and other social features

これ

35 of 58

ショートカット / Shortcuts

静的なショートカット / Static shortcuts

動的なショートカット / Dynamic shortcuts

36 of 58

ショートカットを作る / Create a shortcut

suspend fun pushShortcut(

context: Context,

contact: Contact

) = withContext(Dispatchers.IO) {

val icon = IconCompat.createWithAdaptiveBitmapContentUri(contact.iconUri)

val person = Person.Builder()

.setName(contact.name)

.setIcon(icon)

.build()

val intent = Intent(context, MainActivity::class.java)

.setAction(Intent.ACTION_VIEW)

.setData(contact.contentUri)

// ...

}

37 of 58

ショートカットを作る / Create a shortcut

suspend fun pushShortcut(

context: Context, contact: Contact

) = withContext(Dispatchers.IO) {

// ...

val shortcutInfo = ShortcutInfoCompat.Builder(context, contact.shortcutId)

.setLongLived(true)

.setCategories(setOf("com.example.android.myapp.category.SHARE_TARGET"))

.setActivity(ComponentName(context, MainActivity::class.java))

.setLocusId(LocusIdCompat(contact.shortcutId))

.setShortLabel(contact.name)

.setIcon(icon)

.setIntent(intent)

.setPerson(person)

.build()

ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo)

}

38 of 58

ショートカットを作る / Create a shortcut

<?xml version="1.0" encoding="utf-8"?>

<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

<share-target � android:targetClass="com.example.android.myapp.MainActivity">

<data android:mimeType="text/*" />

<data android:mimeType="image/*" />

<category� android:name="com.example.android.myapp.SHARE_TARGET" />

</share-target>

</shortcuts>

res/xml/sharing_shortcuts.xml

39 of 58

ショートカットを作る / Create a shortcut

<activity android:name=".MainActivity" …>

<intent-filter>

<action android:name="android.intent.action.SEND" />

<!-- … -->

</intent-filter>

<intent-filter>

<action android:name="android.intent.action.VIEW" />

<!-- … -->

</intent-filter>

<meta-data

android:name="android.app.shortcuts"

android:resource="@xml/sharing_shortcuts" />

</activity>

40 of 58

直接共有を受け取る / Receive direct share

private fun handleIntent(context: Context, intent: Intent) {

val intentReader = ShareCompat.IntentReader(context, intent)

if (intentReader.isShareIntent) {

val text = intentReader.text

val shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)

// …

}

}

41 of 58

どう作るべきか / How shortcuts should be created

交流頻度に応じて並び替えたい

The shortcuts should be ranked based on their usage

​

他のアプリのショートカットとの間でも公正に並べ替えてほしい

Our shortcuts should be ranked fairly among the shortcuts from all the apps

​

できるだけシンプルなやり方で

It should be simple

42 of 58

いつ作るべきか / When to create a shortcut

その相手にメッセージを出すとき

On an outgoing message to the contact

​

​

その相手からメッセージが来たとき

On an incoming message from the contact

43 of 58

メッセージを出すとき / Outgoing message

val shortcutInfo = ShortcutInfoCompat.Builder(context, contact.shortcutId)

.setLongLived(true)

.setCategories(setOf("com.example.android.myapp.category.SHARE_TARGET"))

.setActivity(ComponentName(context, MainActivity::class.java))

.setLocusId(LocusIdCompat(contact.shortcutId))

.setShortLabel(contact.name)

.setIcon(icon)

.setIntent(intent)

.setPerson(person)� .addCapabilityBinding("actions.intent.SEND_MESSAGE")

.build()

ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo)

44 of 58

メッセージが来たとき / Incoming message

val shortcutInfo = ShortcutInfoCompat.Builder(context, contact.shortcutId)

.setLongLived(true)

.setCategories(setOf("com.example.android.myapp.category.SHARE_TARGET"))

.setActivity(ComponentName(context, MainActivity::class.java))

.setLocusId(LocusIdCompat(contact.shortcutId))

.setShortLabel(contact.name)

.setIcon(icon)

.setIntent(intent)

.setPerson(person)� .addCapabilityBinding("actions.intent.RECEIVE_MESSAGE")

.build()

ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo)

45 of 58

ShortcutManagerCompat

使う / Use these

  • pushDynamicShortcut: ショートカットをひとつ作る / Create a shortcut
  • removeDynamicShortcuts: ショートカットを消す / Remove shortcuts

​

動的なショートカットについては用途なし / No need for dynamic shortcuts

  • enableShortcuts/disableShortcuts

​

使う必要なし / No need

  • reportShortcutUsed: ショートカットの利用を通知する / Inform the system of shortcut usage
  • add/setDynamicShortcuts: ショートカットを一度に複数作る / Create multiple shortcuts
  • getMaxShortcutCountPerActivity: 作れる数の上限を取得する / Get the limit count of shortcuts
  • isRateLimitingActive: 作る制限にかかっているか取得する / See if we are rate limited

46 of 58

API 23, 24 との互換性 / Compatibility with API 23, 24

ライブラリ “androidx.sharetarget:sharetarget:1.2.0” をインポート

​

AndroidManifest.xml

​

<activity …>

<meta-data

android:name="android.service.chooser.chooser_target_service"

android:value="androidx.sharetarget.ChooserTargetServiceCompat" />

</activity>

47 of 58

直接通知まとめ / Summary of Direct share

ソーシャル機能があるアプリなら積極的に検討すべき

Should support in apps with social features

​

メッセージを出すとき、来たときにショートカットを作る

Push shortcuts on ongoing/incoming messages

​

その際、それぞれのフラグを付ける

Add a corresponding capability binding for an outgoing or incoming message

​

基本的に pushDynamicShortcuts だけ使えばいい

Most use cases can be achieved with pushDynamicShortcut alone.

48 of 58

共有シート

Sharesheet

  • カスタム アクション�Custom actions
  • 初期インテント�Initial intents
  • 共有先候補の除外�Exclude targets
  • 共有先の取得�Know the receiver

48

49 of 58

標準の共有シート / Android Sharesheet

カスタムの共有シートを使わないでほしい

Please don’t use a custom sharesheet

​

Chrome など移行中

Chrome and other apps are migrating

​

標準の共有シートでできないことはないはず

Android Sharesheet should cover all your needs

50 of 58

共有シートでできること / Sharesheet can do these

任意のアクションを追加できる

Can add custom actions

​

共有先候補を並べ替えられる

Can reorder targets

​

共有先候補を除外できる

Can exclude some targets

​

どの共有先が選ばれたか取得できる

Can obtain which target has been chosen

51 of 58

カスタム アクション / Custom Actions

API 34 (Android 14) から

​

共有シートに任意のアクションを追加できる

Can place a custom action on sharesheet

52 of 58

カスタム アクション / Custom Actions

val intent = Intent(Intent.ACTION_WEB_SEARCH)

.putExtra(SearchManager.QUERY, text)

val customAction = ChooserAction.Builder(

Icon.createWithResource(context, R.drawable.ic_custom_action),

"Search on web",

PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

).build()

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("text/plain")

.setText(text)

.createChooserIntent()

.putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, arrayOf(customAction))

)

API 34+

53 of 58

初期インテント / Initial intents

val intent = Intent(...)

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("text/plain")

.setText(text)

.createChooserIntent()

.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(intent))

)

}

  • 並べ替える / Reorder
  • SEND ではない action で渡す / Pass data through non-SEND actions
  • 標準にはない追加の引数を渡す / Pass non-standard extra parameters

54 of 58

共有先の除外 / Exclude some targets

context.startActivity(

ShareCompat.IntentBuilder(context)

.setType("text/plain")

.setText(text)

.createChooserIntent()

.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(

ComponentName(...)

))

)

  • 自分自身を出さない / Exclude the sender itself
  • 他の動線との重複を避ける / Avoid providing duplicated paths

55 of 58

共有先を知る / Know the receiving app

val sendIntent = ShareCompat.IntentBuilder(context)

.setType("text/plain")

.setText("Hello, world!")

.intent

val sender = PendingIntent.getBroadcast(

context,

1,

Intent(context, ShareResultReceiver::class.java),

PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,

)

val chooserIntent = Intent.createChooser(

sendIntent, null, sender.intentSender

)

context.startActivity(chooserIntent)

56 of 58

共有先を知る / Know the receiving app

class ShareResultReceiver: BroadcastReceiver() {

​

override fun onReceive(context: Context, intent: Intent) {

val componentName: ComponentName? = IntentCompat.getParcelableExtra(

intent,

Intent.EXTRA_CHOSEN_COMPONENT,

ComponentName::class.java,

)

// ...

}

}

  • アナリティクス / Analytics

57 of 58

共有シートまとめ / Summary of sharesheet

カスタムの共有シートはやめようか

Migrate away from custom sharesheets

​

標準の共有シートで何でもできるはず

Android sharesheet should cover all your needs

​

要望・フィードバック募集中

We want feedback

​

issuetracker.google.com

58 of 58

ありがとうございました

共有

58