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

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でよく見かけます。
個人的にこのパターン結構好きです。

Android N Easter Egg (neko) に見るアンチパターン

🐱が70匹を超えたあたりから挙動がめちゃめちゃ重くなってきたAndroid N Easter Egg (neko) アプリ。 コードを読んで何が悪そうか調べてみました。

あくまで静的解析の結果なので検証してません。ごめんなさい。「ふーん」くらいに思ってください。ツッコミ大歓迎。

調査対象

下記2パターンのタイミングで重くなります。特に2は困った。

  1. 🐱一覧のダイアログでスクロールするとき
  2. Foodを選択した後、Dialogが消えず端末全体がほぼ操作不能になる

何が悪かったか

1. 🐱一覧のスクロールが重い原因

まず、アプリ全体がどうやって集まっている🐱を保存しているか。 SharedPreferenceに seedname を保存しています。

▼PrefState.java

    // Can also be used for renaming.
    public void addCat(Cat cat) {
        mPrefs.edit()
              .putString(CAT_KEY_PREFIX + String.valueOf(cat.getSeed()), cat.getName())
              .commit();
    }

seed は🐱の各パーツの色をretrieveするために使います。 (デフォルトで🐱の名前はseedになっていた気がしますが、🐱の名前はユーザが変えられるので、名前をvalueとして保持しています。)

🐱一覧のActivityの onCreate() でSharedPreferenceから全ての値をとってきて seed から各パーツの色を計算し、Drawableを描画するという処理を🐱の匹数分メインスレッドでやります。ちなみにパーツは全部で28か所くらいあります。そりゃ重くもなりますね。

2. Foodを置いた直後が重い原因

Foodが選択された後の処理ですが、🐱と同じくSharedPreferenceに書き込みます。

▼PrefState.java

    public void setFoodState(int foodState) {
        mPrefs.edit().putInt(FOOD_STATE, foodState).commit();
    }

これは別にいいんですけど、問題(の原因の一つ)は🐱と同じPreferenceに書いてしまっていること。 このSharedPreferenceを監視しているのは下記の2クラス。

A) NekoTile.java (QuickSettingsにあるFood用のタイル)
B) NekoLand.java (🐱一覧画面のActivity)

Aについて、Foodが選択された後にそのFoodの画像とテキストをタイルに表示するために監視しています。ここは別に問題なし。
問題はB。Bの方は🐱が増えたタイミングを取得するために監視しているようです。同じSharedPreferenceを監視しているせいでFoodの値が変わったタイミングでもここが動いてしまいます。

▼NekoLand.java

    @Override
    public void onPrefsChanged() {
        updateCats();
    }

    private void updateCats() {
        Cat[] cats;
        if (CAT_GEN) {
            cats = new Cat[50];
            for (int i = 0; i < cats.length; i++) {
                cats[i] = Cat.create(this);
            }
        } else {
            cats = mPrefs.getCats().toArray(new Cat[0]);
        }
        mAdapter.setCats(cats);
    }

▼PrefState.java

    public List<Cat> getCats() {
        ArrayList<Cat> cats = new ArrayList<>();
        Map<String, ?> map = mPrefs.getAll();
        for (String key : map.keySet()) {
            if (key.startsWith(CAT_KEY_PREFIX)) {
                long seed = Long.parseLong(key.substring(CAT_KEY_PREFIX.length()));
                Cat cat = new Cat(mContext, seed);
                cat.setName(String.valueOf(map.get(key)));
                cats.add(cat);
            }
        }
        return cats;
    }

ちなみにCatのコンストラクタで各パーツの色の計算とDrawableへのtintが行われています。差分チェック?知らない子ですね…
さらにAdapterのsetCats()notifyDataSetChanged() を呼び出しちゃっています。おそらく全ての🐱に対して再描画が走るんじゃないかな…

さらに悪いのが、SharedPreferenceの監視リスナーをonCreate()でつけています。(当然removeするタイミングは対となるonDestroy()です。)つまり、アプリがバックグラウンドにいる間も上記処理が走ってしまいます。画面に🐱一覧が表示されていないのに、Foodを置いただけで重くなってたのはそのためっぽい。

そもそもFoodの値が変更されたタイミングで🐱の再描画を走らせる必要がないのだから同じSharedPreferenceを利用しお互いそこを監視するのよくないですね。

どうしたらよかったか

ざっと思いついたのでこんな感じ。

  • まずはcatとfoodの保存場所を分離し、それぞれ必要な範囲のみ監視すること。Foodに関してはおそらくこれだけで問題ない。
  • cat側の監視タイミングを見直す。バックグラウンドにいる間に更新が必要なければ監視をonResume() / onPause()の間だけにする
  • 🐱の描画はできるだけ非同期で別スレッドで行う。実際にviewを触らないといけない部分だけUI Threadで。AOSPだと気軽にKotlinで書いたり外部ライブラリ導入したりできないからちょっと非同期処理面倒くさそう。

まぁEaster Egg ですもんね。作った人も100匹くらい集めるような使われ方するとも思ってなかったのかもしれない…

Android N Easter Egg (neko) のコード読んでわかったことまとめ

Android N のEaster Eggはねこあつめ的なアプリ。

コード読んでわかったことまとめます。実装観点とかFrameworkのコードの読み方とかはまたいつか別に書きたい。 以下、Easter eggのアプリ名は便宜上nekoとします。

nekoアプリの基本的な遊び方

  • いつものように設定のAndroidバージョン連打で"N" のロゴが表示される画面に行く。Nアイコン連打でToastで🐱の絵文字が表示されればnekoが使えるモードON, 🚫の絵文字が表示されればnekoが使える
  • nekoが使えるモードの時にNotificationのQuick Settings(Wi-FiとかBluetoothとかの簡易設定があるところ)の編集にneko用の"Empty dish" というアイコン(以下、🐱タイル)が追加されているのでドラッグしてQuick Settingsエリアに移動する
  • "Empty dish" をタップするとお皿におけるFoodが選べます。Foodを置いて一定時間経つとNotificationで🐱が来たことが通知される
  • 本家ねこあつめのように一度集めた🐱はいなくなったりしない
  • Notificationをタップすると集めた🐱の一覧が見られる

Foodについて

  • 選べるFoodの種類はBits(カリカリ) / Fish / Chicken / Treat(お菓子)の4種類
  • ↑の四つのうち左のものほどすぐ🐱が来てくれる
  • 来るまでのインターバルはそれぞれ 15 / 30 / 60 / 120 分
  • ただし左のものほど以前来た🐱がまた来る確率が高い
  • 新規🐱が来るのとインターバルの長さはバーター

🐱について

  • 🐱の形はみんな一緒。コードから規定の確率に従って各部分を着色している
  • 🐱の体の色は黒と白が一番確率が高い
  • 次いで茶色とか灰色とか
  • レアなところだと青、ピンク、紫、緑も居る
  • 上記の色もdarkとlightがある
  • 最強にレアなのは透明(α値が0)
  • あとは首輪とか足の色とか色々決まっている

その他

  • 🐱一覧画面で🐱を長押しするとその🐱を削除 or シェアできる
  • シェアする場合は対象の🐱のbitmapを生成しファイルに吐き出してシェアしてくれる
  • Notificationが表示されていない時に🐱一覧を見たい場合はQuick Settingsの🐱タイルを長押しするといつでも見られる*1

    *1:しばらくこれに気がつかず id:operandoさんのアプリを愛用していました。

hack-it-iron.hatenablog.com

FragmentManager#getFragments()で取得するListの要素がnullになっていたのでFrameworkのソースコードを読んでみた

FragmentManager#getFragments() の挙動がなんか思っていたのと違ったので調べてみたメモ。

具体例を挙げると、Fragment A がattachされている状態で、Fragment B をFragmentTransaction#add() -> remove() した時に、
FragmentManager#getFragments() で得られるリストの要素数は1だと思ったけど、残念2でした〜〜という話。
0番目には予想どおり Fragment A が、1番目にはnullが入っていた。removeしたって言ったじゃないか。解せぬ。

というわけでいつも通りFrameworkのコードを読んでいきます。まずはgetFragments() で何が返ってきているのか確認。

    ArrayList<Fragment> mActive;

    @Override
    public List<Fragment> getFragments() {
        return mActive;
    }

この mActive って名前のArrayListにactiveな状態のfragmentが管理されていそうなもんなのに、なんでactiveでなくなったfragmentを保持していたであろう要素がremoveされずにnullになっているのか?

activeじゃなくなる時のコードを見てみましょう。

    void makeInactive(Fragment f) {
        if (f.mIndex < 0) {
            return;
        }
        
        if (DEBUG) Log.v(TAG, "Freeing fragment index " + f);
        mActive.set(f.mIndex, null);
        if (mAvailIndices == null) {
            mAvailIndices = new ArrayList<Integer>();
        }
        mAvailIndices.add(f.mIndex);
        mHost.inactivateFragment(f.mWho);
        f.initState();
    }

ここでnullがセットされています。
気になるのはその下、 mAvailIndices にinactiveになったfragmentのindexがaddされているところですね。
mAvailIndices はIntergerのArrayListで、

    ArrayList<Integer> mAvailIndices;

使われるのはFragmentがactiveになった時。

    void makeActive(Fragment f) {
        if (f.mIndex >= 0) {
            return;
        }
        
        if (mAvailIndices == null || mAvailIndices.size() <= 0) {
            if (mActive == null) {
                mActive = new ArrayList<Fragment>();
            }
            f.setIndex(mActive.size(), mParent);
            mActive.add(f);
            
        } else {
            f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
            mActive.set(f.mIndex, f);
        }
        if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
    }

mAvailIndices Listの最後の要素をactiveにしたいfragmentのindexにセットしています。

つまり、 removeしたfragmentのindexを mAvailIndices に退避しておいて、 次にaddなりしてactiveになったfragmentにそのindexを割り当てるということをしています。

例えば、 3つのFragmentがActiveな場合、

index Fragment
0 A
1 B
2 C

ここで Fragment B をremoveすると、index 1 がnullになって、mAvailIndices にindex 1 が退避されます。

index Fragment
0 A
1 null
2 C

この状態で Fragment D をaddすると、退避されていたindex 1 が使用される。

index Fragment
0 A
1 D
2 C

indexの効率化が目的だったのかなー。

AsyncTaskのStatusを理解したくてFrameworkのコードを読んだメモ

RX全盛期のいま、こんなことに需要があるのか…ということは気にしない。

AsyncTaskのライフサイクルのstatusがぼんやりとしか理解できずググっても出てこなかったので調べたメモ。 具体的な疑問としては、taskの実行が終わったあと、初期状態(PENDING)に戻るのか?それともFINISH のまま?という点。 APIにもこのように書いてあるんだけどなんとなく釈然としない。taskのlifetimeってどういうことなんだろう。

Each status will be set only once during the lifetime of a task.

それではFrameworkのコードを読んでいきます。 読んだコードは6.0のもの。

初期値

当然ながら初期値は PENDING.
synchronizeよりコストの低いvolatileを使ってるんですね。へぇぇ。

    private volatile Status mStatus = Status.PENDING;

タスクの実行

タスクの実行命令がくるとまずは状態チェック。
PENDING 以外は許容しません。
チェックが終わるとRUNNING 状態に遷移して、onPreExecute() が呼ばれます。

    @MainThread
     public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
             Params... params) {
         if (mStatus != Status.PENDING) {
             switch (mStatus) {
                 case RUNNING:
                     throw new IllegalStateException("Cannot execute task:"
                             + " the task is already running.");
                 case FINISHED:
                     throw new IllegalStateException("Cannot execute task:"
                             + " the task has already been executed "
                             + "(a task can be executed only once)");
             }
         }
 
         mStatus = Status.RUNNING;
 
         onPreExecute();
 
         mWorker.mParams = params;
         exec.execute(mFuture);
 
         return this;
     }

タスクの完了・キャンセル

AsyncTaskは内部でhandlerを持っておき、タスクが完了もくしはキャンセルされた時に終了処理を実行するためのMessage(MESSAGE_POST_RESULT)を投げます。

    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

ここで呼ばれるfinish()の中で状態が FINISHED になります。 このあとはもう状態操作を行わないので、FINISH のまま。

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

ということでわかったのは

  • onPreExecute()の直前にRUNNINGになって、onCancelled()onPostExecute()が完了したらFINISHEDになる
  • statusは循環するわけではなく、一方通行。一度FINISHになったらそこから遷移はしない
  • メンバ変数として持っておいて何度もそのままタスクを使い回すのも無理そう

まぁActivityのonPause()とかでcancelするためにメンバで参照保持しないとなんですけどね。
executeする前に状態チェックして FINISH だったらnewしなおしてやるということで。

最近読んだAsyncTaskのメモリリークの話も一緒にどうぞ。

medium.com

meta-dataで数値だけのStringを渡したい

前回manifestPlaceholders最高!٩( 'ω' )و みたいな記事かいといてアレですが、ハマりどころがあったので記事に残しておきます。 某SDKを使うためにAndroidManifestのmeta-dataにidを記載する必要があり、ここにmanifestPlaceholdersを使っていました。

 <meta-data android:name="hoge" android:value="${fuga}"/>

しかしこれだとなぜかnull扱い… ただし、 .foo を足すとなぜかfugaの中身も .foo も取れます。

<meta-data android:name="hoge" android:value="${fuga}.foo"/>

何が起こっていたかというと、あくまで予想なのですが、 おそらくSDKの中身が ActivityInfoオブジェクトの metaData に対して getString()してるんじゃないかなぁと思います。 だから .foo をつけると文字列になるので問題なくfugaの中身も取れたっぽい。

※予想コード

      ActivityInfo info = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
      String fuga = info.metaData.getString("hoge");

型が違うものをgetしようとするとこんなエラーが出ます。

Bundle  : Key hoge expected String but value was a java.lang.Integer.  The default value <null> was returned.

自分でかいたコードなら getInt() に変えれば解決するけれどSDKの中身なので値を渡す側でどうにかするしかない。 ちょっとググってみたらバックスラッシュを一つ入れれば何故か解決するらしい。 回答つけた人もこんなこと言っちゃってます。

I have no idea quite how this works, if I'm honest.

stackoverflow.com

そんなわけでこれでうまく動きました٩( 'ω' )و

<meta-data android:name="hoge" android:value="\ ${fuga}"/>

Build typeによってアプリアイコンを分ける

はじめに

この記事は諸事情によりRibbonizerライブラリが使えない人たち向けに書いたものです。 Ribbonizerが使える人はそちらを使ったほうが良いです。

やりたいこと

Debug buildの時とRelease buildの時にapp idやapp nameを変えて一つの端末の中に両者を共存させていたのですが、 アイコンが同じだと分かりづらいのでアイコンも分けることに。

どうするか

Ribbonizerという素敵なライブラリがあります。これをつかえば2行ほどbuild.gradleに書き足すだけでやりたいこと達成です。

github.com

どうするか その2

以下、諸事情により↑が使えない人向けです。 自分の場合だと、ローカルでbuildする場合は何の問題もないのですが、 Circle CIでbuildするときにCircle CIの4G制限に引っかかってしまいdied unexpectedly…(´・_・`)

とりあえずライブラリをつかわず自前でどうにかする方向で考えます。 こちらの記事がとても参考になりました。

ninjinkun.hatenablog.com

typeの縛りが激しいresValueと違って、placeholderはdrawableも扱えます。

まずはこんな感じでdefaultConfigに追記。

    defaultConfig {

        manifestPlaceholders = [appIcon:"@drawable/your_app_icon"]
    }

debug版のみアイコンを変えたいのでbuildtypeがdebugの時はdebug用のアイコンを指定します。 (自分の場合はRibbonizerが生成したdebug用のアイコンをbuild/ディレクトリ下から拾ってリネームして指定しました笑)

    buildTypes {
        debug {

            manifestPlaceholders = [appIcon:"@drawable/your_debug_app_icon"]
        }
    }

最後にManifestでこれを指定すればOK.

    <application

            android:icon="${appIcon}"

この方法だとお手軽に動くのですが、debug用のアイコンリソースがapkパッケージの中に含まれてしまいます。 それが耐えられない人はやっぱりRibbonizerでどうにかする方法で頑張ってください。