Android: 画像ファイルの共有は FileProvider と ShareCompat を使う

Android で画像ファイルを他のアプリへ共有するには、ACTION_SEND Intent または ShareCompat を使います。

共有するファイルの Uri は FileProvider で生成します。

この記事では、FileProvider + ShareCompat での実装についてまとめています。

画像のシェアについては以下が公式のドキュメントとなります。

https://developer.android.com/training/sharing/send

# 前提

AndroidX Core 1.5.0 以上を利用していること。

AndroidX Core 1.5.0-beta01 から、後述の Android 10 ShareSheet でのプレビュー問題が修正されています

# 実装: FileProvider の設定

画像ファイルを cacheDir へ保存し、Intent 発行時に一時的に外部のアプリに読み取り権限を与えられるようにします。

AndroidManixest.xml を設定します。`${applicationId}`もそのまま記述します。

app/src/main/AndroidManifest.xml

```xml
...
<manifest...>
    <application...>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_path" />
        </provider>
...
```

provider_path.xml を設置します。

app/src/main/res/xml/provider_path.xml

```xml
<paths>
    <cache-path name="cache" path="." />
</paths>
```

今回は cacheDir 以下へ共有画像ファイルを一時保存するため、`cache-path` タグを設定しています。FileProvider タグ一覧は以下のドキュメントを確認してください。

https://developer.android.com/reference/kotlin/androidx/core/content/FileProvider?hl=ja#specifying-available-files

# ShareCompat の実装

ShareCompat により画像ファイルを共有します。ShareCompat は内部的に ACTION_SEND Intent や ACTION_SEND_MULTIPLE Intent を用いてアプリ間データ共有を実現します。

https://developer.android.com/reference/kotlin/androidx/core/app/ShareCompat

サンプルコードでは、`res/raw/xml/image.png` が設置されている前提です。

```kotlin
class MyActivity: AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content)[0])
        binding.button.setOnClickListener {
            val imageUri = resources.openRawResource(R.raw.image).use { input ->
                val file = File("$cacheDir/image.png")
                file.createNewFile()
                file.outputStream().use { output ->
                    input.copyTo(output)
                }
                FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
            }
            ShareCompat.IntentBuilder(this).apply {
                setChooserTitle("share title")
                setText("share message")
                setStream(imageUri)
                setType("image/png")
            }.startChooser()
        }
    }
//...
```

これを実行すると、以下のような表示となります。上部に大きく表示されている画像が共有しようとした画像のプレビュー、2段目が OS によってレコメンドされた共有相手のユーザー一覧、3段目が共有を受け取れるアプリ一覧です。

ShareSheet の表示は OS バージョンによって異なります。

| OS バージョン | ShareSheet | シェア後の表示 (SMSアプリ） |
| --- | --- | --- |
| Android 11 | * 画像プレビューだけが表示されている<br/>* ChooserTitle の表示がない<br/>* Text の表示がない<br/><img width="170" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/4deeb0a6-4188-d02e-c35c-fbb17d047d93.png"/> <img width="170" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/19c0081d-04c4-57e0-25fc-b6894b8abd16.png"/> | <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/50876289-c2d3-1637-3931-08afb25e8f9a.png"/> |
| (参考) Android 7 | * ChooserTitle だけが表示されている<br/><img width="170" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/ff845a1e-953f-8ed1-4937-b93dc0fff46a.png"/> | <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/83fb3b35-7ee8-1a26-df18-6ac7c5c6aa96.png"/> |

# ShareCompat.IntentBuilder の設定項目

ShareCompat.IntentBuilder は以下の設定項目があります。

| fun | 説明 |
| --- | --- |
| addEmailBcc() | [Intent.EXTRA_BCC](https://developer.android.com/reference/kotlin/android/content/Intent#extra_bcc) |
| addEmailCc() | [Intent.EXTRA_CC](https://developer.android.com/reference/kotlin/android/content/Intent#extra_cc) |
| addEmailTo() | [Intent.EXTRA_EMAIL](https://developer.android.com/reference/kotlin/android/content/Intent#extra_email) |
| setChooserTitle() | Intent Chooser タイトル |
| setHtmlText() | [Intent.EXTRA_HTML_TEXT](https://developer.android.com/reference/kotlin/android/content/Intent#extra_html_text) を設定し、<br/>[Intent.EXTRA_TEXT](https://developer.android.com/reference/kotlin/android/content/Intent#extra_text) に Html.fromHtml() を適用した Spanned を設定する |
| setStream() | [Intent.EXTRA_STREAM](https://developer.android.com/reference/kotlin/android/content/Intent#extra_stream) |
| setSubject() | [Intent.EXTRA_SUBJECT](https://developer.android.com/reference/kotlin/android/content/Intent#extra_subject) |
| setText() | [Intent.EXTRA_TEXT](https://developer.android.com/reference/kotlin/android/content/Intent#extra_text) |
| setType() | [Intent.type](https://developer.android.com/reference/kotlin/android/content/Intent#setType(kotlin.String)) |


https://developer.android.com/reference/kotlin/androidx/core/app/ShareCompat.IntentBuilder

# ShareCompat を使わず、ACTION_SEND Intent を組み立てる実装

ShareCompat を使う実装がお勧めですが、ShareCompat を使わずに ACTION_SEND Intent を組み立てる実装は以下の通りとなります。

```kotlin
class MyActivity: AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content)[0])
        binding.button.setOnClickListener {
            val imageUri = resources.openRawResource(R.raw.image).use { input ->
                val file = File("$cacheDir/image.png")
                file.createNewFile()
                file.outputStream().use { output ->
                    input.copyTo(output)
                }
                FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
            }
            startActivity(
                Intent.createChooser(
                    Intent(Intent.ACTION_SEND).apply {
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                        putExtra(Intent.EXTRA_TEXT, "share message")
                        putExtra(Intent.EXTRA_STREAM, imageUri)
                        clipData = ClipData.newRawUri(null, imageUri)
                        type = "image/png"
                    },
                    "share title"
                )
            )
        }
    }
//...
```

Android 10 以上では、ShareSheet 上で画像のプレビュー表示がありますが、EXTRA_STREAM に設定するだけでは以下のエラーが発生し、ShareSheet 上の画像プレビューが表示されません。

```
E/DatabaseUtils: Writing exception to parcel
    java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.exmaple.myapplication.fileprovider/cache/image.png from pid=8822, uid=1000 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:820)
        at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:684)
        at android.content.ContentProvider$Transport.enforceFilePermission(ContentProvider.java:674)
        at android.content.ContentProvider$Transport.openTypedAssetFile(ContentProvider.java:548)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:327)
        at android.os.Binder.execTransactInternal(Binder.java:1154)
        at android.os.Binder.execTransact(Binder.java:1123)
E/DatabaseUtils: Writing exception to parcel
    java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.example.myapplication.fileprovider/cache/image.png from pid=8822, uid=1000 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:820)
        at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:684)
        at android.content.ContentProvider$Transport.enforceFilePermission(ContentProvider.java:674)
        at android.content.ContentProvider$Transport.openTypedAssetFile(ContentProvider.java:548)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:327)
        at android.os.Binder.execTransactInternal(Binder.java:1154)
        at android.os.Binder.execTransact(Binder.java:1123)
```

このエラーを回避するために、FLAG_GRANT_READ_URI_PERMISSION と clipData を設定しています。

ShareCompat を使う場合は ShareCompat が上記処理をしてくれるため、Android 10 以上でも問題なく動作します。