読者です 読者をやめる 読者になる 読者になる

Celeste Engineer

Androidとか自転車とか

android:windowBackground に指定する BitmapDrawable の位置を指定するときに気をつけること

Android アプリで SplashScreen を作る場合、android:windowBackground をつかうことでレイアウトを読み込まなくてもスプラッシュ用の画像を表示できます。レイアウトを待たなくてもよいので、アプリの起動直後からスプラッシュ画像が見えてよい、というのが android:windowBackground を使う場合の利点ですが、このプロパティに指定できるのはレイアウトではなく Drawable Resource です。

多くの場合スプラッシュ用画像には png 等の画像ファイルを用意すると思いますが、android:windowBackground に直接その画像を設定すると、画面サイズにあわせて画像が引き伸ばされてしまいます。これを避けるには、別途 XML で Bitmap Drawable を用意し、それを android:windowBackground に設定します。XML を用意すると画像を複数重ねたりすることも柔軟にできるようになり便利ですが、いくつか注意が必要です。

背景が必要な場合は Layer Drawable を使う

背景に模様を入れたり、色をちょっと変えたいなどの場合には、Layer Drawable で背景と Bitmap を重ねます。

背景のレイヤでは、<item>android:gravity="fill"を設定しましょう。

<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="fill">
        <shape android:shape="rectangle">
            <solid android:color="@color/splash_background"/>
        </shape>
    </item>
    <item>
        <bitmap
            android:src="@drawable/splash_image"/>
    </item>
</layer-list>

引き伸ばしを避けるために gravity を指定する

LayerDrawable 内で Bitmap を引き伸ばさず自分で位置を決めて表示する場合<item><bitmap>双方にandroid:gravityを設定します。 <bitmap>android:gravityがないと、端末によっては引き伸ばされてしまいます。

<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="fill">
        <shape android:shape="rectangle">
            <solid android:color="@color/splash_background"/>
        </shape>
    </item>
    <item android:gravity="center">
        <bitmap
            android:src="@drawable/splash_image"
            android:gravity="center"/>
    </item>
</layer-list>

Context#startActivity からすぐに Activity が起動しないパターン

前提条件

Service から次のように Activity を起動しようとした時、直近 5 秒以内に他の Activity を Home キーで閉じていると、startActivity の呼び出しからすぐには Activity が起動しません。

// this は Service
Intent intent = new Intent(this, SomeActivity.class);
// Service から起動するときには FLAG_ACTIVITY_NEW_TASK が必要
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

Affinity やその他 LaunchMode などの設定に関わらず、Activity を Home キーで閉じたあと 5 秒は Context#startActivity を呼んでもすぐに Activity は起動しないようになっています。

StackOverflow

同じようなことにハマっている人がいて、質問が上がっていました。

stackoverflow.com

Home キーをおした時の処理

PhoneWindowManager に、Home キーを押したときの処理があります。具体的にはPhoneWindowManager#launchHomeFromHotKey(boolean, boolean)がその処理を受け持つメソッドです。 このメソッドはまず、 KeyGuard が有効かどうかで処理を分岐し、有効でロックスクリーンが表示されている場合はなにもしません。 KeyGuard のアンロックに成功した時、または KeyGuard が関係ない場合にはホームに戻る処理を実行しますが、どちらも必ず次の処理が入ります。

try {
  ActivityManagerNative.getDefault().stopAppSwitches();
} catch (RemoteException e) {
}

名前からして何かを差し止めるための処理に見えますが、ActivityManagerNative.getDefault()で取得しているオブジェクトの実体はActivityManagerServiceです。

ActivityManagerService#stopAppSwitches()

そしてActivityManagerService#stopAppSwitches()を見ると、次にあげる処理を実行しています。

  1. メンバ変数のmAppSwitchesAllowedTimeに5000ミリ秒後(==5秒後)を代入
  2. メンバ変数のmDidAppSwitchにfalseを代入
  3. Handlerに送信した未実行のDO_PENDING_ACTIVITY_LAUNCHES_MSGを削除
  4. APP_SWITCH_DELAY_TIMEの遅延(5000ミリ秒==5秒)でDO_PENDING_ACTIVITY_LAUNCHES_MSGを再送信

これで"Home キー押下後 5 秒は startActivity がすぐに効かない"の 5 秒という時間の理由がはっきりしました。またDO_PENDING_ACTIVITY_LAUNCHES_MSGという定数名から分かるとおり、5 秒後にActivityManagerService#L1878で Activity の起動を再開します。

この 5 秒以内にContext#startActivity(Intent)を実行すると、ActivityManagerService#checkAppSwitchAllowedLocked()でfalseが返ります。このメソッドを読んでいるとandroid.Manifest.permission.STOP_APP_SWITCHESというパーミッションの存在に気が付きますが、このパーミッションの ProtectionLevel はsystemOrSignatureですから、通常のアプリは許可を得られません。

なぜ Home キー押下後の 5 秒間 Context#startActivity() を止められるのか

APP_SWITCH_DELAY_TIMEのコメントによると、Home キーを押したあとに信頼されない Activity の起動を防ぐためとあります。Home キーのイベントで Activity を起動するような、端末をハイジャックしたような挙動を防ぐためのもののようです。

Home キーのイベント処理からコードを追いかけて分かる通り、Activity#onUserLeaveHint()などでActivity#finish()をしていてもこの問題は回避できません。なぜなら問答無用で Home キーのイベントで強制的に 5 秒間の保留が決まるからです。

都民の森に登ってきた

今日も自転車でヒルクライムでした。今日は以前話をしていた人たちと都民の森へ!

昼に武蔵五日市駅にあつまって、そこから都民の森へ行って帰ってくる行程です。以前は武蔵五日市から檜原街道の途中の交差点を上野原にむかって登っていったことがあるのと、都民の森自体は風張峠から下ってきたのを通り過ぎただけだったので、実は目的地としては初めてでした。

残り8kmくらいのところでチェーンが脱落してしまい、直そうとしたところ脱落したチェーンを受けるやつが邪魔をしてうまく引っ掛けられなかったので、一旦それをゆるめてどかして引っかかったら戻す…ということをしました。お陰さまで手がドロドロになってしまいました😞

都民の森ではアイスクリームと甘辛醤油のだんごを食べてリフレッシュしました😁。

くだりは快調でしたが、ふもとのあたりでは大渋滞…ゴールデンウィーク感あるなぁと思って渋滞をノロノロ進んでいたところ、もう数キロで駅につくというところで、通りすがりのおばちゃんが「こっちの裏道抜けたら渋滞避けられるよ!!」ということで、住民じゃないと分から無さそうな住宅街の裏道を教えてもらいました。昨日に引き続き、親切なおばちゃんに助けられて大変ありがたい😉。 「そこを右に曲がってあとは道なりにいけばいいよ」と犬のお散歩をしつつもいい感じのきつい坂を登っていて、おばちゃんの足腰の強さを感じつつ、無事に駅にたどり着きました。

子の権現に登ってきた

本当は子の権現に行くつもりはなく、国道299号を秩父まで行って、そこから奥武蔵グリーンラインを走ってみようかなと思っていたのですが、途中で財布を忘れたことに気が付き、じゃあ子の権現に行ってそのまま帰ろうということで子の権現に行くことになったのでした。

ちなみに、ゴールデンウィークの国道299号は秩父へ向かう行楽客のマイカーが入間まで続くほどの渋滞っぷりで、あのまま秩父に行っていたら返ってくる頃には日が暮れている気もしました。そして、最初に目にした子の権現こちらの看板通りに行ったら、いきなり斜度18%の鬼のような坂を登った挙句登山道に行き当たって先にいけなくなるというボケもかましてしまいました。近所のおばあちゃん、親切に道を教えてくださって本当にありがとうございました。

さて世間で有名なあの激坂とは逆側から登るということで、くだりを気をつければ大丈夫なんじゃないかな?と気軽に構えていました。実際、残り5km地点から3km地点まではほぼ平坦な道でスイスイ進んでいけたので、これはこのままシュッと登って激坂を眺めながら帰れるな?と思っていました。

ところがどっこい、参道残り3km地点からは常時斜度10%以上の坂が続きます。途中ベンチのあるところで休んでいたおばあさんに「気をつけてね」と声をかけていただきましたが、息が上がりまくって出がらしのような声で「ありがとう…ございます…」と返事をしました。行けども行けども延々キツイ坂が続き、あまりにしんどくて残り1kmをきってしばらくしたところでギブアップしてしまいました。結局600mほど自転車を押して登りましたが、ゴールまで斜度が緩むことはなく、裏の道でも十分にキツイということがわかりました。

途中足はつけてしまったものの無事子ノ権現に登りきるも、財布がないのでなにもできず、かの激坂も自転車をおしながら慎重に降りることに。激坂区間の下でクリートカバーを外していると、これからあの激坂を攻めようとする人たちがちらほらと登ってきました。自分も子ノ権現は初めてでしたが、その方たちも初めてだったようで、軽く挨拶をし、あと300mの看板からがキツイですよ!とお伝えしたところ、壁のような坂を前に「なんじゃこれは!」と叫んでいました。

そりゃあ、こんな坂じゃあね…

f:id:KeithYokoma:20170503123128j:plain

上から見たらただの崖だし、下から見たらただの壁です。本当にありがとうございました。

間違えた道の18%の坂を下るときもそうでしたが、普通に降りようとすると後輪が浮いたら死んでしまう!というのと、ブレーキを書け続けられる自信が全くなかったので、自転車を降りて恐る恐る歩きました。 総行程100kmといつもよりは若干短いサイクリングでしたが、無事に帰ってこれてよかったです。

potatotips #39 に行ってきた

技術書典2 では TechBooster から Colorful Android のコンテンツの一つとして DroidKaigi で発表した WindowManager の内容 + Android O の話を書き書きしましたが、その Android O の部分の抜粋 LT をしてきました。

speakerdeck.com

Android O Preview 触った人手をあげてー!」ってやってたくさん手があがるのを見て「おー!」っていう雑なことをしようとしたら一人しか手があがらなくて「おっおっ」てなったのはここだけの話です。

内容は以前ブログAndroid O で WindowManager の振る舞いが変わる - Celeste Engineerに書いたことの通りで、整理されたけどシステムの介入があるから気をつけようね!という感じです。

shibuya.apk #13 行ってきた

shibuya.apk #13 で"Automation with Wercker and Container Builder"というタイトルで発表をしてきました。

speakerdeck.com

Wercker をつかった Android アプリの CI は以前から取り組んでいたものですが、実運用をしている上で困ったところを Google Container Builder でいい感じに解決し、アプリだけでなくビルド環境も CI/CD できるようにしたよ!というお話しです。

Wercker は主要な Docker イメージのレジストリをサポートしていて、ほぼ選択に困ることはないのではと思っていますが、Dockerfile で Docker イメージをビルドすることには使えません。あくまで、イメージを pull してコンテナを立て、その中でアプリを動かしテストする用途のサービスになります。

Docker イメージをビルドしてくれるサービスは Google Container Builder 以外に Dockerhub もあります。ただし設定の柔軟性は圧倒的に Container Builder のほうが高く、なにかと Dockerhub のビルドサービスには足りたいものがあったので、今回は Container Builder を使用しました。

環境そのもののリビジョン管理がこれでかなり楽になったのは超うれしいです。

Android O で WindowManager の振る舞いが変わる

先日の DroidKaigi 2017 で発表した「Building my own debugging tool on overlay」のなかで、WindowManager で取り扱うレイヤについて触れた部分がありますが、Android の次バージョンである O から使用できなくなるレイヤ、代替レイヤについてのアップデートがありますので、こちらにも書き残しておこうと思います。

developer.android.com

developer.android.com

使用できなくなるレイヤ

以下のレイヤは使用できなくなります。

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR

このうち DroidKaigi の発表で取り扱った部分は TYPE_SYSTEM_OVERLAY と TYPE_SYSTEM_ALERT です。ドキュメントを読む限り、引き続き TYPE_SYSTEM_DIALOG は使用できるようです。

代替レイヤ

これらの使用できなくなるレイヤに View を描画しているアプリは、代わりに O から導入される TYPE_APPLICATION_OVERLAY を使わなければなりません。ドキュメント上でも must use とあり、エミュレータで動作を確認したところ、使えなくなったレイヤに描画はできてもタッチイベントは取れなくなっているようです(すべての使えなくなったレイヤで同じかは未検証)。

TYPE_APPLICATION_OVERLAY をつかっているプロセスは優先度が上がるようです。おそらく Service で WindowManager に View を描いている時、startForeground() しなくても visible process として扱ってくれるものと思います。

この TYPE_APPLICATION_OVERLAY については、ステータスバー等のシステムが受け持っている UI の下に位置するようです。システムは TYPE_APPLICATION_OVERLAY にある View を適宜動かしたりリサイズしたりできるようです。また、通知シェード(ドロワーのこと?)から直接 TYPE_APPLICATION_OVERLAY に View を書いているアプリをブロックすることができるとも記載されています。

まとめ

DroidKaigi のセッションでは、Keyguard の上のレイヤに View を置くときはタッチイベント等の入力を受け付けてはいけない話にも触れましたが、この変更では実質 Keyguard 上に View を置くことができなくなるように見えます(TYPE_PRIORITY_PHONE, TYPE_SYSTEM_OVERLAY, TYPE_SYSTEM_ERROR がダメになるので)。オーバーレイ表示そのものがかなり気を遣って実装する必要のあることを考えると、今回のレイヤの整理と挙動の変更はなるほどという感じですが、システムの介入が増える分考慮すべき部分も増えているはずです。