はじめに

突然ですが、App Shortcuts は使っていますか?
アプリアイコンを長押しすると出てくる、これです。

地味ですし、一見気づきにくいので、なかなか使われなさそうな機能ですが、
実は簡単に導入できます。今回はApp Shortcutsの実装について紹介します。

以下はショートカットの種類と概要です。

種類 対応開始バージョン 最大設定可能数 推奨用途
静的ショートカット Android 7.1 (API 25) 通常は4つ前後※ 固定アクション(新規作成・検索・ホーム遷移など)
動的ショートカット Android 7.1 (API 25) 静的と共通の上限制約 利用履歴や文脈に応じて差し替えるアクション(最近開いたコンテンツ、特定ユーザーへのアクセスなど)
ピン留めショートカット Android 8.0 (API 26) システム上の数値制限なし ユーザー自身が永続的に置きたい導線。ユーザー主導でホームに固定する用途

ShortcutManager.getMaxShortcutCountPerActivity() で取得される上限

静的ショートカット

静的ショートカットは、定義したいショートカット情報を記載した新規XML(例:shortcut.xml)と、
AndroidManifest.xmlへショートカットする情報を記載すれば反映されます。

新規XMLは以下のように記載します。この例では一覧ページを静的ショートカットとして定義してます。

<!-- shortcut.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
    <shortcut android:shortcutId="shortcut_list" android:enabled="true" android:icon="@drawable/ic_list"
        android:shortcutShortLabel="@string/label_list" android:shortcutLongLabel="@string/label_list">
        <intent android:action="android.intent.action.VIEW" android:targetPackage="jp.co.appshortcutsample"
            android:targetClass="jp.co.appshortcutsample.MainActivity">
            <extra android:name="shortcut_id" android:value="shortcut_list" />
        </intent>
    </shortcut>
</shortcuts>

AndroidManifest.xmlには既存のactivityスコープ内に追記します。

<!--AndroidManifest.xml(抜粋)-->
<application>
    <activity android:name="Main">
        <!-- 以下を既存のactivityスコープ内に追記する -->
        <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
    </activity>
</application>

ドキュメントページでは extra の指定がなかったことと、Intent.EXTRA_SHORTCUT_ID といった定数が存在していたことから、
Intent から shortcutId が取得できるのかと思ったのですが、実際には取得できませんでした。中身を見た限りでは extra 以外に shortcutId が存在していなさそうなので、明示的に渡してあげる必要がありそうです。
https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts#static

またオプションとして、categoriescapability-binding を設定できます。
これは、システムにショートカット遷移先の機能を紐付けるためのラベルのようなもの、という認識ではありますが、App Shortcuts としては現状、特に意味を持っていなさそうでした。

categories は、システムとして定義されている android.shortcut.conversation のほか、実装者が自由に名前をつけることができます。
https://developer.android.com/reference/android/content/pm/ShortcutInfo#SHORTCUT_CATEGORY_CONVERSATION

capability-binding には action.intent で指定できる値を設定する想定で、それ以外の場合は不明として扱われるようです。

動的ショートカット

動的ショートカットの場合は、以下のように ShortcutInfoCompat.Builderで XML で指定したような要素をコード上で設定します。
作成したショートカット情報を、ShortcutManagerCompat.pushDynamicShortcutで 1 つずつ、
またはShortcutManagerCompat.setDynamicShortcutsでまとめて登録します。
どちらの場合も、静的・動的を合わせた最大数を超過しても特にエラーは発生しませんが、最大数以上は登録されません。
pushDynamicShortcutの場合は古いものから順に削除され、setDynamicShortcutsの場合は超過分が無視されます。

細かい点として、setDynamicShortcutsは指定した要素を追加ではなく「置き換え」として扱うため、注意が必要です。
また、APIレートリミットが存在するので、短期間に何度も更新することは避けたほうが良いです。

```kotlin
/**
 * AppShortcutからShortcutInfoCompatを作成
 */
private fun createShortcutInfo(context: Context, shortcut: AppShortcut): ShortcutInfoCompat {
    val intent = Intent(context, MainActivity::class.java).apply {
        action = Intent.ACTION_VIEW
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
        // intentからshortcutIdが取得できないので明示的に渡す。特定できれば良いのでキーは何でも良い
       // 静的ショートカット同様にこちらでショートカットを判定できるようにする必要がある
        putExtra("shortcut_id", [対応するshortcutId])
        // 普通のintentなのでもちろん追加でパラメータを渡すことも可能(初期値の設定とか)
        putExtra("extra_data", "何かしらの値")
    }

    ShortcutInfoCompat.Builder(context, [対応するshortcutId])
        .setShortLabel("shortcutShortLabelにあたる値")
        .setLongLabel("shortcutLongLabelにあたる値")
        .setIcon("iconにあたるリソース")
        .setIntent(intent)
        .build()
}
```

ピン留めショートカット

ピン留めショートカットは、ユーザーが静的・動的ショートカットから選択してホーム画面に追加するほか、アプリ側からリクエストを送って追加してもらうこともできます。
ShortcutManagerCompat.requestPinShortcutでショートカットを指定するだけで、標準リクエストのモーダルダイアログを表示してくれます。

// (UI抜粋)
TextButton(
    onClick = {
        // Pinned Shortcutを作成
        val success = AppShortcutsManager.requestPinEditShortcut(context)
        if (!success) {
            android.widget.Toast.makeText(
                context,
                "ショートカットの追加に失敗しました",
                android.widget.Toast.LENGTH_SHORT
            ).show()
        }
        showPinShortcutDialog = false
        onDismiss()
    }
) {
    Text(stringResource(id = R.string.app_shortcuts_pin_dialog_confirm))
}
...

fun requestPinEditShortcut(context: Context): Boolean {
    // Pinned Shortcutがサポートされているかチェック
    if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
        return false
    }

    // 動的ショートカットと同じ作り方
    val shortcutInfo = createShortcutInfo(context, AppShortcut.Edit)
    return ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
}
```

おわりに

部分的に公式と異なる箇所はあったものの、簡単に実装することができました。
実際のところ、あまり目立たない機能なため、機能追加する機会も多くなさそうですが、地味ながら捗る機能だと思います。ぜひ対応してみてはいかがでしょうか?



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる