共有
Share
荒木佑一 Yuichi Araki
2023/09/15
共有 / Share
ACTION_SEND のインテントによる共有 / Share by Intent with ACTION_SEND
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)
}
受け取る側 / 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.
}
}
}
今日話すこと / Agenda
5
送る側と受け取る側 / Sender and Receiver
混同しないように注意�Don’t be confused
送る側
Sender
受け取る側
Receiver
Intent
様々なデータを
送る
Send various types of data
7
テキストを送る / 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)
}
ShareCompat.IntentBuilder
fun shareWithCompat(context: Context) {
context.startActivity(
ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText("Hello, world!")
.createChooserIntent()
)
}
テキストを受け取る / 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>
テキストを受け取る / 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.
}
}
}
ShareCompat.IntentReader
private fun handleIntent(context: Context, intent: Intent) {
val intentReader = ShareCompat.IntentReader(context, intent)
if (intentReader.isShareIntent) {
val text = intentReader.text
// Use text...
}
}
ShareCompat
送る側
Sender
受け取る側
Receiver
IntentBuilder
IntentReceiver
Intent
画像を送る / Send an image
fun shareJpegImage(context: Context, uri: Uri) {
context.startActivity(
ShareCompat.IntentBuilder(context)
.setType("image/jpeg")
.setStream(uri)
.createChooserIntent()
)
}
コンテント URI / Content URI
例: “content://com.example.sender/image/123”
アプリ間でファイルを受け渡す仕組み
送る側は ContentProvider
受け取る側は ContentResolver
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
画像を送る / 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()
)
}
画像を受け取る / 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>
画像もテキストも受け取る / 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>
画像を受け取る / Receive an image
fun handleIntent(context: Context, intent: Intent) {
val intentReader = ShareCompat.IntentReader(context, intent)
if (intentReader.isShareIntent) {
val uri = intentReader.stream
// Use uri...
}
}
画像を表示する / 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
}
画像の権限 / 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
画像の編集 / Edit an image
画像を共有するとき、共有シートには編集ボタンが出る
The sharesheet provides “Edit” for images
編集後に共有すると形式が変わっているかもしれない
After editing the image, its format might be different
おそらく PNG
様々な画像を受け取る / 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>
様々な画像を受け取る / 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>
画像をテキスト付きで送る / Send image + text
private fun shareImageWithText(context: Context, uri: Uri) {
context.startActivity(
ShareCompat.IntentBuilder(context)
.setType("image/jpeg")
.setStream(uri)
.setText("Hello, text!")
.createChooserIntent()
)
}
複数の画像を送る / 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()
)
}
複数の画像を受け取る / 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>
複数の画像を受け取る / 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)
}
}
いろいろ受け取る / 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>
いろいろ受け取る / 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
}
}
}
}
ここまでのまとめ / 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
直接共有
Direct Share
33
直接共有 / Direct Share
アプリに共有するのではなく、アプリの中の連絡先に直接共有する
Directly share to a contact
共有シートでの利用率、約 ⅓
Approximately ⅓ of sharesheet usage
チャットなどソーシャル機能のあるアプリならサポートしない手はない
Should support in apps with chat and other social features
これ
ショートカット / Shortcuts
静的なショートカット / Static shortcuts
動的なショートカット / Dynamic shortcuts
ショートカットを作る / 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)
// ...
}
ショートカットを作る / 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)
}
ショートカットを作る / 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
ショートカットを作る / 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>
直接共有を受け取る / 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)
// …
}
}
どう作るべきか / 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
いつ作るべきか / When to create a shortcut
その相手にメッセージを出すとき
On an outgoing message to the contact
その相手からメッセージが来たとき
On an incoming message from the contact
メッセージを出すとき / 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)
メッセージが来たとき / 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)
ShortcutManagerCompat
使う / Use these
動的なショートカットについては用途なし / No need for dynamic shortcuts
使う必要なし / No need
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>
直接通知まとめ / 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.
共有シート
Sharesheet
48
標準の共有シート / Android Sharesheet
カスタムの共有シートを使わないでほしい
Please don’t use a custom sharesheet
Chrome など移行中
Chrome and other apps are migrating
標準の共有シートでできないことはないはず
Android Sharesheet should cover all your needs
共有シートでできること / Sharesheet can do these
任意のアクションを追加できる
Can add custom actions
共有先候補を並べ替えられる
Can reorder targets
共有先候補を除外できる
Can exclude some targets
どの共有先が選ばれたか取得できる
Can obtain which target has been chosen
カスタム アクション / Custom Actions
API 34 (Android 14) から
共有シートに任意のアクションを追加できる
Can place a custom action on sharesheet
カスタム アクション / 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+
初期インテント / Initial intents
val intent = Intent(...)
context.startActivity(
ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(text)
.createChooserIntent()
.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(intent))
)
}
共有先の除外 / Exclude some targets
context.startActivity(
ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(text)
.createChooserIntent()
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(
ComponentName(...)
))
)
共有先を知る / 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)
共有先を知る / 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,
)
// ...
}
}
共有シートまとめ / Summary of sharesheet
カスタムの共有シートはやめようか
Migrate away from custom sharesheets
標準の共有シートで何でもできるはず
Android sharesheet should cover all your needs
要望・フィードバック募集中
We want feedback
issuetracker.google.com
ありがとうございました
共有
58