Codelabs から学ぶ Direct Share

Codelabsの [Direct Share to an Android app](https://codelabs.developers.google.com/codelabs/android-direct-share/index.html?index=..%2F..index#0) (eventタグ：Android Dev Summit 2019) を元にDirect Share（ダイレクトシェア）について説明します。

# Direct Share とは

Android 6.0 から導入された機能で、アプリからコンテンツを共有する時に直接共有する相手を選択することができるというものです。この機能を使わない場合は、まず共有するアプリを選択した上で連絡先を指定する必要があります。

<img width="400" alt="スクリーンショット 2019-12-22 14.42.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/348753c3-c959-9520-17a6-b05134c2f21b.png">

Android 6.0 (APIレベル23) では `ChooserTargetService` を利用する手法でしたが、 Android 10 (APIレベル20) では ChooserTargetService DirectShare API が  Sharing Shortcuts API に置き換えられました。こちらではターゲットの読み込み時間を短縮することができます。

# ChooserTargetService DirectShare API と Sharing Shortcuts API

旧式の DirectShare API ではプルモデルが使用されていたのに対し、新式の Sharing Shortcuts API ではプッシュモデルが使用されています。これによりシェアターゲットを取得するプロセスが大幅に高速化されています。新式の API を使う場合は、シェアターゲットのリストを事前に用意し、例えば新しい連絡先が追加された時などにそのリストを更新する必要があります。

新式 API に書き換えないといけないわけではありません。が、Android 10 以降の Android Sharesheet では新式 API で提供された共有ターゲットの方が優先されるようになるので、旧式 API だと他の共有ターゲットに埋もれてしまう可能性はあります。

後方互換性を保ちたいからと言って旧式 API と新式 API を併用すると望ましくない挙動が発生する恐れがあります。なので旧式 API の代わりに `ShortcutManagerCompat` を使用するのが望ましいです。

# 今回作るもの

コードは Codelabs の Direct Share to an Android app を使います。
・プレーンなテキストが送信できる
・他アプリからテキストを共有しようとした時に、今回作ったアプリが選択肢として出てくる
というものです。
https://github.com/googlecodelabs/android-direct-share

<img width="320" alt="スクリーンショット 2019-12-22 14.42.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/34eba780-973b-1572-ccf0-27ff8aa70901.png"> <img width="320" alt="スクリーンショット 2019-12-22 14.42.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/cbd92f0f-de77-58da-97d2-3defc99e779e.png"> <img width="320" alt="スクリーンショット 2019-12-22 14.42.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/6c8f2cae-4765-bf10-0bf5-0699a61799d1.png">

この Codelabs ではすでに Direct Share 以外の部分は実装されていて、自分でコードを書く部分はこれくらいでした。

* 共有ショートカットを公開する
* 古いAndroidバージョンとの後方互換性を保ちつつ Direct Share 機能を使えるようにする
* コンテンツプレビューにタイトルとサムネイルを追加する

# 実装

## shortcuts.xml

ここでは以下のことを宣言します。

* シェアするデータのタイプ
* 共有ショートカットのカテゴリ
* 共有インテントを扱うActivity

```xml:app/src/main/res/xml/shortcuts.xml
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
    <share-target android:targetClass="com.example.android.directshare.SendMessageActivity">
        <data android:mimeType="text/plain" />
        <category android:name="com.example.android.directshare.category.TEXT_SHARE_TARGET" />
    </share-target>
</shortcuts>
```

`<category>` は公開済みショートカットと共有ターゲット定義を一致させるために使用するものです。この値は自由に決めて良く、また、1つの<share-target>要素に複数のカテゴリを含めることができます。

# AndroidManifest.xml

`shortcuts.xml` を `android.intent.action.MAIN` アクションと `android.intent.category.LAUNCHER` カテゴリを intent-filter に持つアクティビティ内で宣言する必要があります。

```xml
<activity
    android:name=".MainActivity"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <!-- Reference resource file where the app's shortcuts are defined -->
    <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
</activity>
```

`SendMessageActivity` では `<intent-filter>` タグ内で、共有する時に扱うタイプを定義する必要があります。 `SendMessageActivity` は起動した時にインテントの内容を確認します。連絡先に関する情報がないときは `SelectContactActivity` を起動して連絡先を選択し、その結果を `onActivityResult` で取得します。

```xml
<activity
    android:name=".SendMessageActivity"
    android:label="@string/app_name"
    android:theme="@style/DirectShareDialogTheme">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
    <!-- 後方互換性を保つための記述 -->
    <meta-data
        android:name="android.service.chooser.chooser_target_service"
        android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
```

ファイルコンテンツURIを安全に生成、共有するために `FileProvider` の定義も記述します。

```xml
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.android.directshare.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
```

## build.gradle

ShortcutManager API を使う準備をします。この API にはショートカットを更新、削除、追加するメソッドがあります。
※ShortcutManager とのやり取りはバックグラウンドスレッドで行う必要があります。

Codelabs では `ShortcutManagerCompat` を使用しています。`ShortcutManagerCompat` は旧式の ChooserTargetService DirectShare API との後方互換性を備えた共有ショートカットを提供する AndroidX の API です。プロジェクトで使うときは `core` を gradle に追加します。`sharetarget` には古いAndroidバージョンでも動作する `ChooserTargetServiceCompat` が入っています。

```xml:build.gradle
implementation "androidx.core:core:${versions.androidxCore}"
implementation "androidx.sharetarget:sharetarget:${versions.shareTarget}"
```

## MainActivity

共有するテキストを入力、送信する画面です。

<img width="320" alt="スクリーンショット 2019-12-22 14.42.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/34eba780-973b-1572-ccf0-27ff8aa70901.png">

今回の例では、ユーザがアプリを開くたび（= MainActivity が立ち上がるたび）に共有ショートカットをプッシュします。この処理の詳細は `SharingShortcutsManager` の章で説明します。

```kotlin:onCreate
sharingShortcutsManager = SharingShortcutsManager().also {
    it.pushDirectShareTargets(this)
}
```

SHARE ボタンをタップした時の処理はこちらです。

```kotlin
private fun share() {
    val sharingIntent = Intent(Intent.ACTION_SEND)
    sharingIntent.type = "text/plain"
    sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, bodyEditText.text.toString())

    // sharesheetのプレビューに表示されるタイトルを設定(optional)
    sharingIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.send_intent_title))
    // Android Q 以降ではタイトル、画像などのプレビューを表示させることができる
    // sharesheetのプレビューに表示される画像のURIを設定(optional)
    val thumbnail = getClipDataThumbnail()
    thumbnail?.let {
        sharingIntent.clipData = it
        sharingIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    }

    startActivity(Intent.createChooser(sharingIntent, null))
}

private fun getClipDataThumbnail(): ClipData? {
    val contentUri = saveThumbnailImage()
    // フォルダアクセスの必要があるので、AndroidManifest.xmlでFileProviderの定義をする
    ClipData.newUri(contentResolver, null, contentUri)
    ...
}
```

## SelectContactActivity

共有する人物の選択肢を RecyclerView で表示しています。 Direct Share 特有の実装はないので、ここでは説明を省きます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/796d3ea7-7cb5-2c7d-6ed3-44cc10cca7fc.png" width="320px">

## SendMessageActivity

送る相手とメッセージを表示します。 `SEND` をタップするとトーストが表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/993d20af-20ce-de10-e170-7bba24aee021.png" width="320px"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/e0809d80-90ea-f48c-fb1b-6b9704c218c0.png" width="320px">

Direct Share を使用した場合は `Intent.EXTRA_SHORTCUT_ID` を取得することができます。今回は連絡先IDがセットされているので、これを元に連絡先のUI表示を行います。

```kotlin
val shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)
```

## SharingShortcutsManager

ShortcutManager とやり取りするクラスです。ショートカットに関連付けるカテゴリは自分たちでつけることができますが、 `shortcuts.xml` で定義したものと一致する必要があります。

```kotlin
private val categoryTextShareTarget = "com.example.android.directshare.category.TEXT_SHARE_TARGET"
```

公開するショートカットのリストを追加するコードは以下の通りです。ここでは `Contact.kt` で定義した4つの連絡先を追加しています。ちなみに `ShortcutManagerCompat.getMaxShortcutCountPerActivity` で定義されている以上の数のショートカットを追加するとアプリがクラッシュしてしまいます。

```kotlin
fun pushDirectShareTargets(context: Context) {
    val shortcuts = ArrayList<ShortcutInfoCompat>()

    // さっきのカテゴリをSet化
    val contactCategories = setOf(categoryTextShareTarget)

    for (id in 0 until maxShortcuts) {
        val contact = Contact.byId(id)

        // ショートカットが静的ショートカットとして開かれた時のみ送られる
        val staticLauncherShortcutIntent = Intent(Intent.ACTION_DEFAULT)

        shortcuts.add(
                // idはアクティビティが共有インテントを受け取った時の識別子として必要
                // idは受け取ったインテントでEXTRA_SHORTCUT_IDとして受け取れます
                ShortcutInfoCompat.Builder(context, Integer.toString(id))
                        // 共有先選択ダイアログに表示される短いラベルとアイコン
                        .setShortLabel(contact.name)
                        .setIcon(IconCompat.createWithResource(context, contact.icon))
                        .setIntent(staticLauncherShortcutIntent)
                        // ショートカットがキャッシュされるようにしている
                        // ショートカットの公開を停止してもsharesheetに表示される
                        .setLongLived(true)
                        // ショートカットのフィルタリングに使われる
                        .setCategories(contactCategories)
                        // 共有先の提案最適化のため
                        // 指定はオプションであるものの、強く推奨されている
                        .setPerson(
                                Person.Builder()
                                        .setName(contact.name)
                                        .build()
                        )
                        .build()
        )
    }

    // 本当はCRUD操作等々で管理を改善した方がいいらしい
    ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts)
}
```

これで、 Direct Share サンプルアプリ作成完了です。


# 参考文献

* [他のアプリへの単純なデータの送信](https://developer.android.com/training/sharing/send?hl=JA)
* [他のアプリからシンプルなデータを受信する](https://developer.android.com/training/sharing/receive?hl=JA)
* [Android Q: Sharing shortcuts](https://proandroiddev.com/android-q-sharing-shortcuts-3be59ea2f4ec)