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

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

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は読書だけでなく、植物の水やりなど毎日繰り返すタスクの管理も大変便利なので 我が家のパクチーも元気です。

Android N からMulti Language and Multi Locale になるらしい

※本エントリは2016/4/8 に書いたもので、Android N Preview 1 時点で公開されている情報に基づいています。

昨日「まったりAndroid Framework Code Reading #3」に参加しました。
mandroidfcr.doorkeeper.jp

結局Frameworkのソースコードを読まずにAndroid N Previewを読んだというオチ。
NからMulti Language and Multi Locale になるらしい。
間違ったこと書いてたら指摘して欲しいです。


公式資料はこちら
http://developer.android.com/intl/ja/preview/features/multilingual-support.html

概要の翻訳版はこちら
http://qiita.com/tnagao3000/items/562e81e8ac9bb581afcc#multi-locale-support-more-languages

Nから設定で複数言語選べるようになる

M以前ではラジオボタンで単一でしか選択できなかった言語設定ですが、Nからは複数言語選択できるようになるらしい。
複数言語を選択する場合、選択する言語には優先度がつきます。
(例:1.日本語、2.Englishなど)

Nから端末がサポートするLocaleが大幅に増える

残念ながら各言語が具体的にどれくらい増えるかどうかは言及されていません。
ただ、例として出てくるar_EG (Arabic - EGYPT) は27もArabicなLocaleに対応するそうです。

NからLocaleに階層の概念が持ち込まれる

例えばM以前ではLocaleクラスのen_CA(CANADA), fr_CA(CANADA_FRENCH), en(ENGLISH), fr(FRENCH)の間に階層概念はありませんでしたが、NではLocaleに階層概念が持ち込まれました。これにより、より柔軟にユーザに適切な言語を提供できるようになります。

Nからシステムがどの言語を選択するかルールが変わる

M以前

ユーザが設定できる言語は一つで、アプリ側でその言語リソースが用意されている場合はそのリソースが使われます。
(例: 日本語設定している人がFacebookを使うと日本語をサポートしているFacebookで日本語表示になる)
アプリ側がユーザが設定している言語をサポートしていない場合、アプリがサポートするデフォルトの言語表示になります。
(例: 日本語設定している人がAWS Consoleを使うと日本語をサポートしていないAWS Consoleで英語表示になる)

N以降

ユーザが設定できる言語は複数。また、Localeに階層概念が持ち込まれているため、より柔軟に言語を選択することができる。

例えば、提供するアプリのサポートする言語が以下で:
- en_US (US English) <- default
- es_ES (Spanish)

端末の設定言語が以下の場合:
- es_MX(Spanish mexico)

M以前ではen_USが選択されていましたが、Nだと es が一致するes_ESの方が選択されます。


また、複数言語設定した場合の例。
提供するアプリのサポートする言語が以下で:
- en_US (US English) <- default
- de_DE (GERMANY)
- es_ES (Spanish)
- it_IT (ITALY)

端末の設定言語が以下の場合:
- fr_CH (French - SWITZERLAND)
- it_IT (Italian - SWITZERLAND)

まず、fr_CHを探しに行って見つからず、次にfrを探しに行って見つからず、
次にfrの子供を探しに行って見つからず、次にit_ITを探しに行って見つからず、
次にitを探しに行って見つからず、次にitの子供を探しに行ってついに晴れてマッチしたと判定されます。
もしここで見つからない場合はアプリデフォルトのen_USになります。

パフォーマンス注意点

今までのLocaleが階層関係のになる場合が多いので、そこを注意しないとパフォーマンスで問題が出ます。
上の例のように、子供でぴったり一致するものがいなければ親を見て、さらにその子供を探しに行くからです。
例えば、今までen-GB(UK) だったものはen-001(international English)にリネームするべきと記載されています。
en-GBの最も一般的な親はen-001になるからです。

NからAdditional Locale を設定できるようになるらしい

Nから追加されたLocaleList.GetDefault()を使うと端末に設定されているLocaleのリストを取得できるようになります。
これによりより洗練された言語表示ができるようになるらしい。
たとえば、翻訳かける時もより適切な言語にできるとか。
// 正直どう使うのか想像できなかった…

Formattersについて

M以前だと主要な言語(en, es, ar, fr, ru)でも1,2種類しかLocaleは用意されていませんでした。
そのせいで数字や日時がハードコードされてしまうことがあり、ユーザに混乱を与えることがあったけれど
Nからは数字や日時をハードコードする代わりにformatterを使うことが推奨されています。
主要な例はアラビア言語。Nでは`ar_EG`が**27**もアラビックロケールをサポートするそうです。

formatterの使い方の例は以下の通り。

format(locale, "Choose a %d-digit PIN", 4)

// RTL言語の読めない数字周りの表示確認つらそう…

AndroidでChromeの共有メニューにアプリを追加する

やりたいこと

AndroidChromeで共有メニューを選択した際に出てくるアプリ群に自作アプリを追加したい。
最終的にChromeで開いていたページのURLを自作アプリで受け取るのがゴール。

試したこと

まずは公式ドキュメントを参照。
Receiving Simple Data from Other Apps | Android Developers

Intent FilterをAndroidManifest.xml に登録します。
URLを受け取りたかっただけなのでmimeTypeがtextの物のみ指定。

サンプルを同様の記載をしたところ、Chromeの共有メニューにテストアプリのアイコンが無事出てきました。
ただし、これだとActivity名が表示されてしまうので、アプリ名に変えるためIntentFilterにlabelを指定します。
frameworksのこのへんのコードを参考にしました。
Cross Reference: /packages/apps/UnifiedEmail/AndroidManifest.xml

        <activity
            android:name=".YourActivity"
            android:label="@string/your_app_name_lable" >
            <intent-filter android:label="@string/your_app_name">
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>

次に上記で指定したActivity側のコード。URLを受け取る側です。
`Intent.EXTRA_TEXT にリンク情報が入っています。

    if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND)) {
      Bundle extras = getIntent().getExtras();
      String extraText = extras.getCharSequence(Intent.EXTRA_TEXT).toString();
      if (!TextUtils.isEmpty(extraText)) {
        doSomething(extraText);
      }
    }