言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Quick SettingsにCustom Tileを追加する

Android N からQuick Settingsに3rd Partyが好きな物を置けるようになりました。 これを使うと簡単にデバッグツール作れるのでは?と思ってやってみました٩( 'ω' )و

f:id:muumuumuumuu:20161006203754p:plain 右下のやつ

やりたかったこと

  1. Quick SettingsにTileを追加する
  2. Tileをタップした時に何か便利な挙動をする

長くなりそうなので、今回は1のみ扱います。

Quick SettingsにTileを追加する

TileServiceクラスを拡張したサービスを作ったら瞬殺です。 まずはAndroidManifestにサービスを追加してpermissionとintent filterを設定しましょう

        <service android:name=".DebugTileService"
                 android:label="@string/app_name"
                 android:icon="@drawable/ic_star_black_24dp"
                 android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" >
            <intent-filter>
                <action android:name="android.service.quicksettings.action.QS_TILE" />
            </intent-filter>
        </service>

あとはこのサービス内でclick eventのハンドリングをしましょう。

class DebugTileService : TileService() {

    override fun onClick() {
        super.onClick()
        when (qsTile.state) {
            Tile.STATE_ACTIVE -> {
                // do something
                qsTile.state = Tile.STATE_INACTIVE
            }
            Tile.STATE_INACTIVE -> {
                // do something
                qsTile.state = Tile.STATE_ACTIVE
            }
        }
        qsTile.updateTile()
    }

注意しなければならないのは、 Tile#setState() しただけでは表示は切り替わらず、 Tile#updateTile() をコールしてやる必要があります。

Kotlinで書くとproperty accessなのでちょっと意味がわかりにくいかもですが お察しください。

Quick SettingsにTileが追加できるようになる仕組み

せっかくなので上で登録したサービスがどのような仕組みでQuick Settingsに追加できるようになるのかFrameworkのコードを読んでみました。

まず、Quick Settingsのviewが QSCustomizer というLinearLayoutを拡張したクラスを持っています。
(何をQuick Settingsに置くか選べる全画面のview部分です)
このviewの表示時にQSCustomizer#show() がコールされますが、この時 TileQueryHelper クラスがnewされます。
TileQueryHelper のコンストラクタで呼ばれる addSystemTiles() の中で AsyncTask の拡張である QueryTilesTask をHandlerにpostします。そのタスクのbackground処理の中でマニフェストにintent filter(TileService.ACTION_QS_TILE) を登録してあるサービス一覧を撮ってきてtileにaddしているという仕組みでした。

Cross Reference: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java

   140     private class QueryTilesTask extends
    141             AsyncTask<Collection<QSTile<?>>, Void, Collection<TileInfo>> {
    142         @Override
    143         protected Collection<TileInfo> doInBackground(Collection<QSTile<?>>... params) {
    144             List<TileInfo> tiles = new ArrayList<>();
    145             PackageManager pm = mContext.getPackageManager();
    146             List<ResolveInfo> services = pm.queryIntentServicesAsUser(
    147                     new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
    148             for (ResolveInfo info : services) {
    149                 String packageName = info.serviceInfo.packageName;
    150                 ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
    151                 final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
    152                 String spec = CustomTile.toSpec(componentName);
    153                 State state = getState(params[0], spec);
    154                 if (state != null) {
    155                     addTile(spec, appLabel, state, false);
    156                     continue;
    157                 }

queryIntentServicesAsUser()、便利そうですが @hideなAPIですね。残念。
それにしてもAsyncTaskとかMessageとHandlerの組み合わせで非同期処理を行うパターン、AndroidのFrameworkでよく見かけます。
個人的にこのパターン結構好きです。