App Shortcuts事始め

[Androidその２ Advent Calendar 2016](http://qiita.com/advent-calendar/2016/android_second)の4日目の記事です。
Android 7.1のAppShortcutsを紹介します。

## App Shortcutsとは
App ShortcutsとはAndroid 7.1で追加されたアプリ内機能のショートカットを作成できる機能です。
アプリアイコンを長押しすると表示されるショートカットからアプリ内の各機能にアクセスできます。
ショートカットはコピーしてランチャーにピン止めしておくこともできます。
利用頻度が高い機能をショートカットに追加しておくと機能へ即座にアクセスできるようになって便利です。

|ショートカット一覧|ショートカットのピン留め|
|:---:|:---:|
|![app-shortcuts-list.png](https://qiita-image-store.s3.amazonaws.com/0/59241/bc07eb20-92b2-d1fd-c0b3-df56c49ab151.png)|![app-shortcuts-pin.png](https://qiita-image-store.s3.amazonaws.com/0/59241/0b547fd9-3933-5da2-bd08-cce76ef070d8.png)|


ちなみにGoogleのアプリ以外だとTwitter、Talon(Tiwtterクライアント)等はすでに対応しているのでGooglePlayから落として動作をみてみると良いかもしれません。

## App Shortcutsの種類

ショートカットには2つの種類があります。

**Static Shortcuts**
静的なショートカットでxmlにショートカットの設定を記述しショートカットを表示します。
動的にショートカットの更新や削除等は出来ず、変更するにはアプリのアップデートが必要です。
（動的に削除しようとすると落ちます。。)

**Dynamic Shortcuts**
動的なショートカットでStaticShortcutsと違いコード上からショートカットの追加・削除・更新等が可能です。
動的に追加できるのでユーザーがよくアクセスするページをショートカットに追加したり、ユーザーにショートカットを選ばせたりいろいろな使い方ができそうです。

## Static Shortcutsを実装する

**ショートカットの設定ファイルを用意**

表示するショートカットの設定を記述したxmlファイルを `res/xml` に配置します。
`shortcuts`内に表示するショートカットを記述していきます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- ショートカットのID・アイコン・文言等を設定 -->
    <shortcut
        android:icon="@mipmap/ic_launcher"
        android:shortcutId="first_shortcut"
        android:shortcutShortLabel="@string/short_label">
        <!-- 遷移先 -->
        <intent
            android:action="android.intent.action.VIEW"
            android:targetPackage="com.kuwapp.myshortcutsample"
            android:targetClass="com.kuwapp.myshortcutsample.MainActivity" />
    </shortcut>

</shortcuts>
```

**AndroidManifest.xmlにショートカットの設定ファイルを指定**

intent-filterに`android.intent.action.MAIN`と`android.intent.category.LAUNCHER`を持つActivityのmeta-data要素にショートカットの設定ファイルを指定します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kuwapp.myshortcutsample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <!-- 設定ファイルを指定する -->
            <meta-data android:name="android.app.shortcuts"
                android:resource="@xml/shortcuts" />
        </activity>
    </application>

</manifest>
```

これだけでショートカットが表示されるはずです。

### shortcut要素に記述できる属性

|属性|説明|
|---|---|---|
|android:shortcutId| shortcutを識別するためのID 必須|
|android:enabled| 有効、無効のフラグ 初期値はtrue |
|android:shortcutShortLabel| ピンされたアイコンのタイトル文言 |
|android:shortcutLongLabel| ランチャーアイコン長押し時に表示されるショートカットの文言 (定義されていない場合ShortLabelが使われる) |
|android:shortcutDisabledMessage| 無効なピン留めされたショートカットがタップされたタイミングで表示される文言 |

## Dynamic Shortcutsを実装する

### DynamicShortcutsの追加

`ShortcutManager`クラスの`addDynamicShortcuts`または`setDynamicShortcuts`メソッドを使用します。
どちらのメソッドでもDynamicShortcutsは追加できますが、違いとして`addDynamicShortcuts`は既存のDynamicShortcutsのリストに新たなDynamicShortcutsを追加するのに対して`setDynamicShortcuts`は既存のDynamicShortcutsのリストを新しいDynamicShortcutsに置き換えます。

```java
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
// Actionを設定しないと落ちる
intent.setAction(Intent.ACTION_DEFAULT);
ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(this, id)
        .setShortLabel(getString(R.string.short_label))
        .setIntent(intent)
        .build();
shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcutInfo));
```

### DynamicShortcutsの削除

`ShortcutManager`クラスの`removeAllDynamicShortcuts`または`removeDynamicShortcuts`メソッドを使用します。
メソッド名から推測できる通り、`removeAllDynamicShortcuts`は全てのDynamicShortcutsを削除し、`removeDynamicShortcuts`は単一のDynamicShortcutsを削除します。
`removeDynamicShortcuts`の引数には削除するDynamicShortcutsのIDを指定します。

```java
// 削除するDynamicShortcutのIDを指定
shortcutManager.removeDynamicShortcuts(removeShortcutIdList);
```

### DynamicShortcutsの更新

`ShortcutManager`クラスの`updateShortcuts`メソッドを使用します。

```java
// 更新するShortcutと同一のIDを指定
ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(this, id)
        .setShortLabel(getString(R.string.short_label))
        .setIntent(intent)
        .build();
shortcutManager.updateShortcuts(Collections.singletonList(shortcutInfo));
```

## ショートカットを無効にする

ショートカットを無効にした場合、一覧から表示されなくなり、ピン留していたアイコンはグレーアウトしてタップしても動作しません。
タップ時は`disabledMessage`で指定した文言がトーストで表示されます。
ピン留めされたショートカットは無効にできますが、コードから動的に削除することはできません。

**Static Shortcuts**
`android:enabled`属性をfalseにすることで無効になります。
無効時に表示されるメッセージは`android:shortcutDisabledMessage`で指定します。

**Dynamic Shortcuts**
`ShortcutManager`クラスの`disableShortcuts`メソッドを呼び出します。
引数にはdisableにするショートカットのIDを指定します。

```java
shortcutManager.disableShortcuts(disableShortcutIds);
```

無効時に表示されるメッセージは`ShortcutInfo`作成時に`setDisabledMessage`メソッドで指定します。

```java
ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(this, id)
                .setShortLabel(getString(R.string.short_label))
                .setIntent(intent)
                .setDisabledMessage(getString(R.string.disable_message))
                .build();
```


## TIPSやはまったことなど

### ショートカットが表示されない
以下の場合にショートカットが表示されませんでした。

* `android:shortcutId`が別のショートカットと重複している
* `android:shortcutId`にstringリソースを指定している
* `android:action`を指定していない

### バックスタックに別のActivity積んだ上で遷移させたい

ActivityBに遷移させてバックキー押下時にAcitivtyAを表示させたいような場合は複数のIntentを指定します。
(ActivityA->ActivityBのように積まれている)

**StaticShortcuts**

```xml
<shortcut
    android:icon="@mipmap/ic_launcher"
    android:shortcutId="shortcut"
    android:shortcutShortLabel="@string/short_label">
    <!-- バックスタックに積みたいActivityの数だけintentを追加する 一番下に記述したintentが一番上に積まれる -->
    <intent
        android:action="android.intent.action.VIEW"
        android:targetPackage="com.kuwapp.myshortcutsample"
        android:targetClass="com.kuwapp.myshortcutsample.MainActivity" />
    <intent
        android:action="android.intent.action.VIEW"
        android:targetPackage="com.kuwapp.myshortcutsample"
        android:targetClass="com.kuwapp.myshortcutsample.SecondActivity" />
</shortcut>
```

**DynamicShortcuts**

`ShortcutInfo`のインスタンス生成時に`setIntents`メソッドの引数に複数のIntentを指定します。

```java
Intent[] intents = // 略
ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(this, id)
        .setShortLabel(getString(R.string.short_label))
        .setIntents(intents)
        .build();
```

### StaticShortcutsを使用していてビルド時にパッケージ名を動的に変更しているときの対応
複数のAPKを共存させるためにビルド時にパッケージ名を変更している場合(debugとrelease版など)はStaticShortcutsのintent要素に追加する`android:targetPackage`も同時に変更しなければなりません。
アプローチとしては以下が考えられると思います。

* パッケージ名の数だけショートカットの設定ファイルを作成しておく
  * BuildTypeやFlavorが増える毎にほぼ同内容の設定ファイルを追加しなければならない
* ビルド時に動的に設定ファイルを書き換えるGradleのタスク等を作る
  * 一度作ってしまえば楽だが、少し手間がかかる
* Static ShortcutsをやめDynamic Shortcutsを使用する
  * 単に置き換えるだけなので容易

Dynamic Shortcutsへの移行は容易なので自分はDynamicShortcutsを使用することにしました。

### 異なるショートカットから同一のActivityへ遷移させるが、ショートカット毎に振る舞いを変えたい

異なるショートカットから同一のActivityへ遷移させ表示内容を変えたいときなどはIntentのActionかExtraに値を入れておいてそれを見て処理を変えることで実現できます。
Static Shortcutsなら以下のように`android:action`に独自アクションを入れるかintent要素内にextraを記述して起動側で受け取るようにすればどのショートカットからきたか分かります。

```xml
<intent
    android:action="com.kuwapp.Intent.MAIN"
    android:targetClass="com.kuwapp.appshortcutssample.MainActivity"
    android:targetPackage="com.kuwapp.appshortcutssample">
    <extra android:name="Shortcut" android:value="Main" />
</intent>    
```

以下のようにAction or extraを取り出しどのショートカットからきたか判断して処理を変えれば良いです。

```java
String action = getIntent().getAction();
String extra = getIntent().getStringExtra("Shortcut");
```

## おわりに
AppShortcutsを実装してみました。現在Android7.1でしか使えませんが、実装は非常に容易であると同時に便利な機能なので入れてみても良いと思います。（いつかminSdkVersionが25になれば良いなあ・・・）
個人的にはDynamic Shortcutsの方が柔軟性があるのであえてStatic Shortcutsを使う意味があるのだろうかと思ったり。

## 参考
[App Shortcuts | Android Developers](https://developer.android.com/preview/shortcuts.html)