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

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

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でどうにかする方法で頑張ってください。

何番目のRadioButtonがチェックされているかうっかり取得されてしまう話

RadioGroup#getCheckedRadioButtonId() が、チェックが入ったviewのidではなく、何番目にチェックが入っているかを取得できると勘違いして、実際その勘違い通りの振る舞いをするように見えるケースが存在した調査ログです。

結論から言ってしまうと、RadioButtonにidを振らないとView#generateViewId() が1から順に勝手に新規idを振ってしまい、それを返してしまうという話です。

現象

RadioButtonを3つ配置したRadioGroupが存在しており、そのうち1番目にチェックをつけた状態でRadioGroup#getCheckedRadioButtonId()が1を返すケースがある。 この振る舞いになる条件は、

  1. RadioButtonにidをふらない
  2. 初回起動時のみ(n回目に対象Activityを起動した際には3n-2が返ってくる)

具体的にはこんなレイアウト

    <RadioGroup
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/radiogroup_sample">
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample1"/>
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample2"/>
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample3"/>
    </RadioGroup>

何がおこったか

RadioButtonがviewのhierarchyに追加される時点で、idが存在するかチェックし 存在しない場合は新規に追加します。

RadioGroup.java

    private class PassThroughHierarchyChangeListener implements
            ViewGroup.OnHierarchyChangeListener {
        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;

        /**
         * {@inheritDoc}
         */
        public void onChildViewAdded(View parent, View child) {
            if (parent == RadioGroup.this && child instanceof RadioButton) {
                int id = child.getId();
                // generates an id if it's missing
                if (id == View.NO_ID) {
                    id = View.generateViewId();
                    child.setId(id);
                }

View.java

    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    public static int generateViewId() {
        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }

generateViewId() は1で初期化したAtomicIntegerから順にインクリメントした値を返すので、それぞれn番目がチェックされているかのように振舞っていたのです。(returnするのがnewValueではなくresultなので初回は1が返ってきます。) ただし、例えばActivityを再起動するなどして再度viewのhierarchyに追加されるときは1からではなく続きからインクリメントされるので、初回起動時のみn番目がチェックされているかのように見えていたというオチでした。

余談

何番目がチェックされているか返すmethodはRadioGroupを拡張したクラスでaddView() をoverrideしてやれば作れそう。 ただし、「何番目がチェックされているか」ではなく「何がチェックされているか」の方がどう考えても重要なのであまり使い道はないかもしれない。

public class CustomRadioGroup extends RadioGroup {

  private List<RadioButton> mChildren = new ArrayList<>();
  public CustomRadioGroup(Context context) {
    super(context);
  }

  public CustomRadioGroup(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (child instanceof RadioButton) {
      mChildren.add((RadioButton) child);
    }
    super.addView(child, index, params);
  }

  public int getSelectedPosition() {
    int selectedViewId = getCheckedRadioButtonId();
    if (selectedViewId == -1 || mChildren.isEmpty()) {
      return -1;
    }
    for (int i = 0; i < mChildren.size() -1; i++) {
      if (mChildren.get(i).getId() == selectedViewId) {
        return i + 1;
      }
    }
    return 0;
  }
}

読書の習慣化に成功した話

今年に入って割と効率よくインプットが進んでいるので知見を共有します。

きっかけ

バレンタインにバズったこの記事に影響され、毎日コツコツと小さなインプットをしようと決意。

www.lifehacker.jp

何をやったか

昨年からダラダラと読んでいたSoft Skillsを読み切ることに決めました。

Soft Skills: The Software Developer's Life Manual

Soft Skills: The Software Developer's Life Manual

こちらはまだ翻訳版が出ておらず、英語版を読むしかないのでなかなか読み進められずにいました。 (英語自体はとても簡単な表現を使って書かれているので、英語苦手な人にもオススメです。) こちらの本を1日1章ずつ読むことにしました。

どうやったか

まずはこれから先一週間で毎日どの章を読むか決めます。 決めたらTo Do Listに登録していきます。私はこのアプリを使っています。

play.google.com

このアプリはマテリアルデザインが割と良くでおり、シンプルで使いやすいので気に入っています。 またリマインダー機能があるのと、その日のタスクが全て終わったらちょっとほっこりメッセージをくれるのが好きです。

一週間が経過するか、もしくはその週のタスクを消化しきったらまたその日から一週間先のタスクを登録していきます。 なぜ一週間ごとにやるかというと、自分の性格上週によって進捗が異なることがわかったからです。

結果

1ヶ月ほどで無事読み終わりました。(たしか40章ほどあったので結構かかりました。) 読み終えたのももちろん良かったのですが、毎日何かしらのインプットもするという習慣がついたのがおおきな成果でした。 現在は次の本を読んでいます。

おまけ

上記To Do Listは読書だけでなく、植物の水やりなど毎日繰り返すタスクの管理も大変便利なので 我が家のパクチーも元気です。