2018年5月31日木曜日

I/O Recap : Slices

https://developer.android.com/guide/slices/

Slices は自分のアプリの情報を他のアプリに表示するための仕組みです。
コンセプトとしては App Widget で使う RemoteViews と似ていますが、使えるレイアウトは用意されているテンプレートだけなど制限があります。 Google Search の検索結果で利用することを例示されていることからも、統一された Look and Feel を持たせたいのかなと思います。

Google Search アプリで2018年夏に対応することが Google I/O 2018 のセッションでアナウンスされていました。 将来的に Google Assistant での応答結果に利用できるようになることもアナウンスされていました。

Slices のサポートが Jetpack に組み込まれており、これを利用すると Android 4.4 以降に対応できます。

前提条件

  • AndroidX にリファクタリングされていること
  • 必須ではないが Android Studio 3.2 以降には Slice を開発するのに便利なツールや機能が追加されている

Slice Provider Widzard

Android Studio 3.2 には Slice Provider を追加する Widzard が用意されています。 この Widzard を使うと
  • build.gradle に slice の dependencies を追加
  • SliceProvider を継承したクラスの生成
  • AndroidManifest に Provider の宣言を追加
を自動で行ってくれます。

* Android Studio 3.2 Canary 15 まで Wizard で追加される dependencies が typo しているバグがありました。Canary 16 では修正されています。 https://issuetracker.google.com/issues/79996770


dependencies の設定

現時点(2018年5月31日)での最新版は 1.0.0-alpha2 です。 implementation "androidx.slice:slice-core:1.0.0-alpha2" implementation "androidx.slice:slice-builders:1.0.0-alpha2"

SliceProvider を継承したクラスを用意

自分のアプリで Slice を提供するには SliceProvider を継承したクラスを用意します。 androidx.slice.SliceProviderandroid.app.slice.SliceProvider があるので注意してください。 import androidx.slice.Slice import androidx.slice.SliceProvider ... class MySliceProvider : SliceProvider() { override fun onCreateSliceProvider(): Boolean { return true } override fun onBindSlice(sliceUri: Uri): Slice? { ... } }

AndroidManifest に Provider の宣言を追加

android:exported="true" が必要です。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.example.slicecodelab"> <application ... > ... <provider android:name=".MySliceProvider" android:authorities="com.android.example.slicecodelab" android:exported="true" /> </application> </manifest>

Uri と Slice の bind

各 Slice は URI と紐づいており、Slice の表示がリクエストされると SliceProvider の onBindSlice(uri: Uri) が呼ばれます。
onBindSlice(uri: Uri) では送られてきた Uri を処理し、対応する Slice インスタンスを返します。 override fun onBindSlice(sliceUri: Uri): Slice? { return when (sliceUri.path) { "/temperature" -> { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("Temperature : $temperature") } .build() } else -> null } } 引数の sliceUri には content://<authorities>/<path> が入ってきます。


Slice の更新

Slice で表示している内容を更新したいときは Slice の Uri に対して notifyChange() を呼びます。 そうすると Provider の onBindSlice(uri: Uri) が呼ばれるので新しい内容で Slice を作ります。 class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // update slice related data ... context.contentResolver.notifyChange(sliceUri, null) } }

Templates

https://developer.android.com/guide/slices/templates

Slice は TemplateSliceBuilder を 継承したクラスを使って作ります。 このうち、一番外側の Slice は ListBuilder で作ります。


ListBuilder

List 形式の Slice を作成できます。
子要素(一行の要素)として ListBuilder.RowBuilder、ListBuilder.HeaderBuilder、ListBuilder.RangeBuilder、ListBuilder.InputRangeBuilder、GridRowBuilder を追加できます。HeaderBuilder は一つしか指定できませんが、その他は複数追加できます。

MODE_SHORTCUT では、Header に PrimaryAction が指定されていればそれが使われ、なければ最初の Row の PrimaryAction が使われます。
MODE_SMALL では 1 row だけ表示されます。Header が指定されていればそれだけが表示され、なければ最初の Row だけが表示されます。
MODE_LARGE では表示可能な数だけ row が表示されます。Slice を表示する側が scrolling に対応していれば全ての row がスクロールできるビュー内に表示されます。

ListBuilder のコンストラクタでは TTL をミリ秒で指定します。time sensitive なコンテンツを含まない場合は ListBuilder.INFINITY を指定します。

setAccentColor(color: Int) で accentColor を指定できます。accentColor はアイコンの tint や toggle の tint、horizontal progress の tint などに適用されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setAccentColor(Color.BLUE) ... .build() }


setKeywords(keywords: List) で指定したキーワードは Slice の Hints(getHints() で取得できる)に追加されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setKeywords(listOf("Cupcake", "Donuts", "Eclair")) ... .build() }

WiFi 設定など表示したい row が多いときは setSeeMoreAction(intent: PendingIntent) を指定しておくと、全ての row が表示できないときに右下に See more ボタンが表示されるようになります。 private fun createSlice(sliceUri: Uri): Slice? { val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setSeeMoreAction(seeMoreIntent) ... .build() }


addAction() で Header の右側に SliceAction を表示させることができます(Header がない場合最初の Row の右側に表示されますが、Header が指定されている前提の機能だと思います)。
複数追加できますが 3つまでしか表示されませんでした。 private fun createSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addAction(homeAction) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") } .build() }


ListBuilder.RowBuilder

ListBuilder の子要素(一行の要素)として Row を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストに追加されず ListBuilder.addRow() で明示的にリストに追加する必要があります。

setPrimaryAction() で指定した SliceAction は Row がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher), "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setPrimaryAction(action) } .build() } 左 : MODE_SHORTCUT、右 : MODE_LARGE




addEndItem(timeStamp: Long) で右側に timestamp を表示することができます。
指定した timeStamp の long 値から自動で差分時間表示(10 min ago とか)になります。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(System.currentTimeMillis() - 10 * 60 * 1000) } .build() }


addEndItem(icon: IconCompat, imageMode: Int) で右端にアイコンを表示することができます。

imageMode には のどれかを指定します。 ICON_IMAGE を指定すると小さいサイズで表示され tint されます。SMALL_IMAGE、LARGE_IMAGE では tint されません。

アイコンは複数追加できますが 3つまでしか表示されませんでした。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE) } .build() } 左 : ICON_IMAGE、中 : SMALL_IMAGE、右 : LARGE_IMAGE



左 : ICON_IMAGE での MODE_SHORTCUT、右 : LARGE_IMAGE での MODE_SHORTCUT




addEndItem(action: SliceAction) で右側に SliceAction を表示できます。

EndItem にアイコン(上記)と SliceAction を併用することはできません。
private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(homeAction) } .build() }


toggle 表示用の SliceAction を指定するとスイッチを表示できます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val toggleAction = SliceAction( pendingIntent, "Toggle", true) return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(toggleAction) } .build() }


setTitleItem() で左側に Timestamp、アイコン、SliceAction が表示できるはずなのですが、指定しても表示されませんでした。


ListBuilder.HeaderBuilder

ListBuilder の子要素(一行の要素)として Header を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストにセットされず ListBuilder.setHeader() で明示的にリストにセットする必要があります。
ListBuilder にセットできる HeaderBuilder は一つだけです。

title, subTitle に加えて summary をセットすることができ、summary はモードが SliceView.MODE_SMALL のときに使われます。

setPrimaryAction() で指定した SliceAction は Header がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle, summary の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createHeaderSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground), "Temperature controls") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setSummary("summary") .setPrimaryAction(action) } .build() } 左: MODE_SHORTCUT、中 : MODE_SMALL、右 : MODE_LARGE




ListBuilder.RangeBuilder

ListBuilder の子要素(一行の要素)として horizontal progress を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addRange() で明示的にリストに追加する必要があります。

Min は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。 private fun createRangeSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMax(10) .setValue(5) } .build() } 左: setMax(), setValue() のみ、右 : title, subTitle あり




ListBuilder.InputRangeBuilder

ListBuilder の子要素(一行の要素)として horizontal slider を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addInputRange() で明示的にリストに追加する必要があります。

Min のデフォルト値は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。
InputAction は必須です。 private fun createInputRangeSlice(sliceUri: Uri): Slice? { val pendingIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addInputRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMin(5) .setMax(10) .setValue(8) .setThumb(IconCompat.createWithResource(context, R.drawable.square)) .setInputAction(pendingIntent) } .build() } 左: setMin(), setMax(), setValue() のみ、中 : title, subTitle あり、右 : setThumb()




GridRowBuilder

ListBuilder の子要素(一行の要素)として horizontal grid を表示できます。
grid の各アイテムは GridRowBuilder.CellBuilder で作成します。

コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addGridRow() で明示的にリストに追加する必要があります。

ListBuilder と同じように setSeeMoreAction(intent: PendingIntent) で see more ボタンを表示する機能があります。


GridRowBuilder.CellBuilder

GridRowBuilder の子要素(1アイテム)として cell を表示できます。

コンストラクタで親の GridRowBuilder インスタンスを指定する必要がありますが、 それだけではグリッドに追加されず GridRowBuilder.addCell() で明示的にグリッドに追加する必要があります。

addTitleText() でタイトルスタイルで表示したいテキストを追加し、addText() でノーマルスタイルで表示したいテキストを追加します。 両方含め最初に追加したテキスト2つが利用され、それ以降に追加されたテキストは無視されます。

addImage() で画像を追加します。最初に追加した画像1つが利用され、それ以降に追加した画像は無視されます。

setContentIntent() で cell をタップしたときに発行される PendingIntent を指定することができます。 private fun createGridSlice(sliceUri: Uri): Slice? { ... val action = ... val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addGridRow { it.setContentDescription("description") .setPrimaryAction(action) .setSeeMoreAction(seeMoreIntent) .addCell { it.addImage( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.LARGE_IMAGE) .addTitleText("Title") .addText("normal") .setContentDescription("description") .setContentIntent(cellIntent) } ... } .build() }

ドキュメントには cell の数について言及されていませんが、SliceViewer(後述)では5つまで表示されました。
また、setSeeMoreAction() すると、4つ表示 + More になりました。



なぜか SliceViewer(1.0.0-alpha1.1 で確認)では画像が長方形に表示されてしまうのですが、Google Search が対応したときにはちゃんと正方形で表示されると思います。

画像のみ



左: テキストのみ MODE_SMALL、右: テキストのみ MODE_LARGE



左: 画像 + テキスト MODE_SMALL、右: 画像 + テキスト MODE_LARGE



MODE_SMALL では表示されるテキストが1つだけになります。



Slice Viewer

https://developer.android.com/guide/slices/getting-started#run-the-slice-viewer

自分のアプリの Slice の表示を確認するために SliceViewer サンプル が用意されています。
このサンプルの apk ダウンロードしデバイスにインストールします。 $ adb install -r -t slice-viewer.apk 自分の Slice を表示確認をするには、slice-<表示させたい Slice の URI> を Intent の Data として指定します。 $ adb shell am start -a android.intent.action.VIEW -d slice-content://com.android.example.slicecodelab/temperature ランチャーから SliceViewer アプリを起動し、アプリ内の検索バーで <表示させたい Slice の URI> を検索すると、トップの一覧画面に検索した Slice が表示されます。


Codelabs

Creating Android Slices
  • Java のみで Kotlin 版はない
  • androidx.slice のバージョンが 1.0.0-alpha1 になっているが 1.0.0-alpha2 が出ている
  • P と androidx で同じ名前のクラスでも API が異なるので import 先が androidx になっているか注意すべし

Google I/O 2018 session





2018年5月26日土曜日

I/O Recap : ML Kit 情報まとめ(Android 向け)

* 以下は 2018年5月25日時点での情報です。


ML Kit for Firebase

現在 ML Kit はベータで、以下の機能を Android と iOS で利用することができます。
  • テキスト認識(Text recognition)
  • 顔検出(Face detection)
  • バーコードの読み取り(Barcode scanning)
  • 画像のラベルづけ(Image labeling)
  • ランドマーク認識(Landmark recognition)
  • 独自 TensorFlow Lite モデルの実行(TensorFlow Lite model serving)
  • (Google I/O 2018 のセッションで High density face contour feature と Smart Replay API が Coming soon であると紹介されています。いずれも on-device で real time に動作するようです)

これらの処理にはデバイス上で行う on-device API とクラウドで実行される cloud-based API が用意されています。
on-device API はリアルタイムに処理でき、オフラインでも動作し、無料で使うことができます。
cloud-based API(Cloud Vision API)は on-device API よりも詳しい情報を提供しますが、一定の利用回数以上は有料です。

例えばテキスト認識では、on-device API では Latin-based language しか認識できず、他の言語も認識したいなら cloud-based API を使う必要があります。
画像のラベルづけでは、on-device API では 400+ labels ですが cloud-based API なら 1000+ labels に対応しています。

Cloud Vision API を利用するには Firebase の課金プランを Blaze(従量制課金)にする必要があります。
Firebase Pricing Plans

機能ごとに毎月1000 API calls までは無料で使うことができます。
Cloud Vision API Pricing

独自の TensorFlow Lite モデルを Firebase に upload するだけで、アプリからそのモデルを実行できるようになります。 モデルのホスティングと実行は無料で使うことができます。


API には on-device でのみ使えるもの、Cloud でのみ使えるもの、両方用意されているものがあります。

機能on-deviceCloud
テキスト認識oo
顔検出o
バーコード読み取りo
画像のラベルづけoo
ランドマーク認識o
独自モデルの実行o


テキスト認識

https://firebase.google.com/docs/ml-kit/recognize-text

画像からテキストを認識します。

on-device API と cloud-based API 両方用意されています。 on-device API は Latin-based language のみ認識でき、cloud-based API は他の言語にも対応しています。

on-device API は無料で使うことができ、cloud-based API は毎月1000 API calls までは無料で使うことができます。
cloud-based API を使うには Firebase の課金プランを Blaze(従量制課金)にする必要があります。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }
cloud-based API には通常の文字認識用の FirebaseVisionCloudTextDetector の他に、書類のように文字密度の高いテキストの認識用に FirebaseVisionCloudDocumentTextDetector が用意されています。


顔検出

https://firebase.google.com/docs/ml-kit/detect-faces

画像から顔を検出します。
目・耳・頬・鼻・口の位置を取得できます。
笑顔かどうか(笑顔の確率)を取得できます。
目が閉じているかどうか(目が閉じている確率)を取得できます。
検出された個々の顔ごとの識別子を取得でき、動画のフレーム間で同一の顔をトラッキングできます。

on-device API のみです。無料で使うことができます。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

バーコード読み取り

https://firebase.google.com/docs/ml-kit/read-barcodes

画像からバーコードを読み取ります。

対応フォーマット
  • Linear formats: Codabar, Code 39, Code 93, Code 128, EAN-8, EAN-13, ITF, UPC-A, UPC-E
  • 2D formats: Aztec, Data Matrix, PDF417, QR Code
バーコードの向きに関係なく認識します。

on-device API のみです。無料で使うことができます。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

画像のラベルづけ

https://firebase.google.com/docs/ml-kit/label-images

追加のメタ情報なしで画像内のエンティティ(人、物、場所、活動など)を認識し、リストとして取得できます。

on-device API と cloud-based API 両方用意されており、on-device API は 400+ labels に、cloud-based API は 1000+ labels に対応しています。
on-device API は無料で使うことができ、cloud-based API は毎月1000 API calls までは無料で使うことができます。
cloud-based API を使うには Firebase の課金プランを Blaze(従量制課金)にする必要があります。

最新は 16.0.0、on-device API は 15.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' // on-device implementation 'com.google.firebase:firebase-ml-vision-image-label-model:15.0.0' }

ランドマーク認識

https://firebase.google.com/docs/ml-kit/recognize-landmarks

画像からランドマーク(例えば東京タワーなど)を認識します。

cloud-based API のみです。そのため Firebase の課金プランを Blaze(従量制課金)にする必要があります。毎月1000 API calls までは無料です。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

独自 TensorFlow Lite モデルの実行

https://firebase.google.com/docs/ml-kit/use-custom-models

独自の TensorFlow Lite モデルは Firebase console からアップロードします。

アプリへのモデルのダウンロードは Firebase が動的に行ってくれるため、APK にモデルをバンドルする必要がありません。これによりアプリのインストール時のサイズを減らすことができます。
また、アプリのリリースとモデルのリリース(Firebase への upload)プロセスが分離されることで、それぞれのチームでリリースをハンドリングできるようになります。

Firebase Remote Config と組み合わせれば A/B test を行うこともできます。
full TensorFlow モデルを lightweight TensorFlow Lite モデルへ変換・圧縮する機能が coming soon だと I/O で発表されています。

(Firebase console に upload せずに)APK にモデルをバンドルしたり、自分のサーバーでモデルをホストしてアプリにダウンロードして、それを ML Kit の API 経由で使うこともできます。


最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-model-interpreter:16.0.0' } upload した独自モデルを使うには、FirebaseCloudModelSource.Builder にモデル名を渡して指定します。
(I/O のセッション動画のコードが古いので注意)
モデル名は Firebase にモデルをアップロードするときに指定します。あとから変更はできません。 val cloudSource = FirebaseCloudModelSource.Builder("my_model_v1") ... .build() RemoteConfig でモデル名を切り替えるようにすれば、target ごとにそれぞれ異なるモデルを使うことができます。 val modelName = firebaseRemoteConfig.getString("my_model") val cloudSource = FirebaseCloudModelSource.Builder(modelName) ... .build()

High density face contour feature

I/O のセッション動画 より



100以上の点を検出し 60fps で処理できるとのこと。 coming pretty soon だそうです。


ML Kit console

左側のメニューの [DEVELOP] - [ML Kit] で ML Kit のコンソールを開くことができます。



ここのカスタムタブから独自モデルをアップロードします。



Codelabs

最初に Codelabs のアプリで動作を見てからドキュメントを読むのがよいと思います。
実際に両方ともやりましたが on-device での認識が速くすごいと思いました。

Android 向けの ML Kit のコードラボは2つ用意されています。
サンプルコードは Java で Kotlin 版は用意されていません。

Recognize text in images with ML Kit for Firebase
  • アプリにあらかじめ用意されている画像からテキストを認識する
  • on-device API と cloud-based API 両方使う
  • Cloud Vision API (https://console.cloud.google.com/apis/library/vision.googleapis.com/)を試すには Firebase の料金プランを Blaze(従量課金制)にしないといけない
左: on-device API での認識結果、右 : cloud-based API での認識結果




Identify objects in images using custom machine learning models with ML Kit for Firebase
  • アプリにあらかじめ用意されている画像に対し独自 TensowFlow Lite モデルを実行し、物体を認識する
  • TensorFlow Lite のモデルファイルが用意されているので Firebase console の設定等実際に試すことができてよい



Quick Start Sample

コードラボはテキスト認識と独自モデルしかないので、その他の機能を使い方をみるには Github で公開されている Quick Start Sample が参考になります。
  • Live Preview : カメラのプレビューに対して on-device API (顔検出、テキスト認識、バーコード読み取り、画像のラベルづけ、独自モデル)を実行
  • Still Image : 画像に対して cloud-based API(画像のラベルづけ、ランドマーク認識、テキスト認識)を実行





Google I/O 2018 session



Reference




2018年5月18日金曜日

IO recap : Android vitals: debug app performance and reap rewards (Google I/O '18)



星1つのレビューでは42%のユーザーが安定性やバグについて言及している
星5つのレビューでは73%のユーザーがスピード、デザイン、使いやすさについて言及している

ANR 率やクラッシュ率が上がると、ユーザーがアプリで費やす時間が有意に減る


Android vitals とは、Android デバイスの安定性とパフォーマンスを向上するための Google が主導する取り組み
もっとも重要なパフォーマンスメトリクスであるバッテリー、安定性、レンダリングの情報を開発者にわかりやすく提供する
データ提供を opt in している1億以上のユーザーからの情報

昨年 Android vitals をリリースしてから 10,000 以上の開発者がコンソールからパフォーマンスを理解した
昨年に比べ、スピード、デザイン、使いやすさについて言及している星5つのレビューは 4% 増え、安定性やバグに言及している星1つのレビューは 18% 減り、リソースの使用について言及している星1つのレビューは 21% 減った


Starbucks アプリは ANR rate が 70% 減り、Crash rate が 85% 減った
ANR は 3rd party のライブラリで起こっていたため、自分たちの観測に引っかかっていなかった
Android vitals はプラットフォームレベルのツールなので、Starbucks の 3rd party crash SDK では検出できていなかったクラッシュを見つけることができた
なぜならそのクラッシュは 3rd party crash SDK が開始される前に起こっていたため

Kiloo の Subway Surfers というゲームでは ANR を 95% 減らすことができた


昨年 Android vitals を公開したとき、バッテリー、安定性、レンダリングの3つの項目があった
新しくアプリのスタートアップ時間(App startup time)と権限(Permissions)が追加された

Vitals
  • バッテリー(Battery)
  • 安定性(Stability)
  • レンダリング(Rendering)
  • New: アプリのスタートアップ時間(App startup time)
    • コールド スタートアップ時間が長い(Slow cold start)
    • ウォーム スタートアップ時間が長い(Slow warm start)
    • ホット スタートアップ時間が長い(Slow hot start)
  • New: 権限(Permissions)
    • 権限リクエストの拒否率(Permission request denials)

バッテリー(Battery)

  • 停止した wake lock
  • 過度の wakeup
  • 過度のバックグラウンドでの Wi-Fi スキャン
  • 過度のバックグラウンドでのネットワーク使用

安定性(Stability)

  • ANR 発生率
  • クラッシュ発生率

レンダリング(Rendering)

  • フリーズした UI フレーム
  • 遅いレンダリング

New: アプリのスタートアップ時間(App startup time)

  • コールド スタートアップ時間が長い : 5秒以上
  • ウォーム スタートアップ時間が長い : 2秒以上
  • ホット スタートアップ時間が長い : 1.5秒以上

New Metric: コールド スタートアップ時間が長い
  • 5秒以上かかると遅いと判断
  • コールドスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリがしばらく使われておらず、アプリがメモリ上にいない状態からスタート

New Metric: ウォーム スタートアップ時間が長い
  • 2秒以上かかると遅いと判断
  • ウォームスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリが最近使われており、アプリがメモリ上にいる状態からスタート(アプリはkillされていない)

New Metric: ホット スタートアップ時間が長い
  • 1.5秒以上かかると遅いと判断
  • ホットスタート :
    • onRestart() から running になるまで
    • onRestart() → onStart() → onResume() → Activity running
    • アプリと Activity がメモリ上にいる状態からスタート

New: 権限(Permissions)

アプリのコアバリューに必要な権限だけをリクエストし、必要に応じて権限リクエストの正当な理由をランタイム時に提供する
  • 権限リクエストの拒否率
〜40%のユーザーが権限を拒否した理由として、その権限は不必要だと思ったと回答している

権限の詳細ビューでは権限をグループに分けて表示しているので、どの権限がユーザーにとって納得感があり、どの権限が不必要だと思われているかがわかる

Android Vitals の詳細の内訳

  • 一般的な内訳
    • APK versionごと
    • デバイスごと
    • Android versionごと
  • Wake locks, wakeups
    • tag ごと
  • ANR率
    • Activity 名ごと
    • ANR type ごと
    • Clusters
  • クラッシュ率
    • Clusters

カテゴリーベンチマーク

自分のアプリの vital が特定のカテゴリーの中でどのくらい良いかを見ることができる
vital の各 metric でパーセンタイル 25, 50, 75 の値を見ることができる

概要画面で全ての vital がリストされ、直近の30日とその前の30日の値、ベンチマークの値を見ることができる



概要画面の主な指標(Core Vitals)には Google Play でのアプリの表示やランキングに影響するパフォーマンス指標が表示される
主な指標が下位25%より悪くなると Bad behavior として表示される

異常検知(Anomaly Detection)

新リリースやリグレッションの結果値に急変があるとアラートを出す
  • ANRやクラッシュ率の大きな変化
  • 主な指標(Core Vitals)の大きな変化
概要画面右上の[通知設定]からAlertをメールで受け取るよう設定できる




主な指標(Core Vitals)を改善するには

ANRの原因
  • Network / Disk operations
  • Long calculations
  • InterProcess Communication (IPC)
  • Locks and Synchronization
  • Slow BroadcastReceiver handling

Network / Disk operations

例: SharedPreferences インスタンスを生成する時点で Disk 処理が行われる override fun onCreate(state: Bundle?) { // この時点で Disk 処理が行われる prefs = PreferenceManager.getDefaultSharedPreferences(this) } どのメソッドが Network 処理や Disk 処理をするのか理解するのは難しいので StrictMode を利用する class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .penaltyDeath() .build()) } } }

Long calculations

例: 数独ゲームの盤面生成に時間がかかる

Android Profiler で CPU の使用をチェックする
ちなみに Android Studio 3.2 Canary ではスタートアップ時間をプロファイルできるようになっている

StrictMode にはこのメソッドを呼ぶと遅くなるということを指定できる class GenerateBoardSource() { fun generateBoard(seed: Long) : SudokuGame { StrictMode.noteSlowCall("Generating Sudoku board") return SudokuSolver.generate() } } StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() ... .detectCustomSlowCalls() ... .build())

InterProcess Communication (IPC)
  • 他のアプリを呼び出す場合、基本こちらに制御権がない
  • 呼び出し先が Network 処理や Disk 処理をするかもしれないので別スレッドで呼び出す


Locks and Synchronization
  • これはとても難しい問題
  • deadlock になったり main thread をブロックするかもしれない
  • デバッグが難しい
  • Android Vitals が提供する trace file の情報がデバッグに役立つかもしれない


Slow BroadcastReceiver handling
  • Android Manifest に BroadcastReceiver を登録した場合、onReceive メソッドは main thread で呼び出される
  • 実行に時間がかかる処理を onReceive() でやるべきではない
  • 10秒以内に処理を終えないと ANR になる
  • Notification に表示する画像を Disk から読み出すなどちょっとした Disk 処理が必要な場合は onReceive() で goAsync() を呼び、別のスレッドを立ち上げ、終わったら PendingResult.finish() を呼ぶ
  • https://developer.android.com/guide/components/broadcasts#effects-process-state
  • あまり長いと結局システムに kill されるので、長い処理が必要なら JobScheduler や WorkManager を使う



クラッシュ対策
  • クラッシュの対応として Activity のライフサイクルでむやみに null チェックや例外の握りつぶしをするべきではない
  • 車輪の再発明をしない : 問題を解決する利用できるライブラリを使う
    • Lifecycle handling (LiveData, ViewModel)
    • Database object mapping (Room)
    • Data paging (Paging)
    • *NEW* Fragment transitions, up/back, deep link handling (Navigation)
    • *New* Job scheduling (WorkManager)
  • 3rd party の優れたライブラリもたくさんある
  • Kotlin を使う : でも全てのクラッシュを防げる銀の弾丸ではないよ!
  • private / hidden API を使わない


バッテリー対策

停止した wake lock

wake lock が取得されたが適切に release されなかった
  • wake lock を使わない
  • 画面をつけっぱなしにしたいなら Activity の Window に FLAG_KEEP_SCREEN_ON を指定する
  • 自分で Service を管理せず job を schedule する
  • AlarmManager で BroadcastReceiver を起こすようにすると onReceive() の間 AlarmManager は wake lock を hold してしまう
  • wake lock を使わなければ permission も必要なくなる
  • どうしても wake lock を使わないといけないなら、常に PARTIAL_WAKE_LOCK を使うこと
  • wakeLock.acquire() にタイムアウトをセットすること
  • static な descriptive tag を渡すこと(Android Vitals でのデバッグがしやすくなる)
  • try { ... } finally { wakeLock.release() } すること


過度の wakeup
  • もっとも大きい原因は AlarmManager の *_WAKEUP アラーム
  • 可能ならなくす(Remove)
  • 頻度を減らす(Reduce)
  • FCM, WorkManager, JobScheduler, SyncManager などに置き換える(Replace)
  • Android Studio 3.2 Canary に追加された Energy Profiler で wake lock に関する問題をデバッグできる


関連





2018年5月17日木曜日

IO recap : Migrate your existing app to target Android Oreo and above (Google I/O '18)



新規アプリは2018年8月以降
既存アプリのアップデートは2018年11月以降
targetSdkVersion を >= 26 にしないといけない話

Permissions

Runtime permissions : ユーザーは設定からon/offできる
Special permissions : 画面の上にdrawするやつとか

Alarms

WorkManager を使う

BroadcastReceivers

Android Manifest で register したほとんどの implicit receiver はもはや受け取れなくなる
例外もある、ACTION_BOOT_COMPLETED とか

BroadcastReceiver の使用を避ける例として、JobScheduler を使ってネットワーク状態の変更を検知し、 val jobScheduler = getSystemService(Context.JOB_SCHDULER_SERVICE) as JobScheduler val jobInfo = JobInfoBuilder(JOB_ID, serviceComponent) .setRequiredNetwork(JobInfo.NETWORK_TYPE_ANY) .build() BroadcastReceiver は Android Manifest で disabled にしておき(android:enabled="false" を追加) <receiver android:name=".NetworkConnectionReceiver" android:enabled="false"> <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver> JobScheduler で receiver を有効にする fun setNetworkReceiverState(enabled:Boolean) { val componentName = ComponentName(package, NetworkConnectivityReceiver::class.java.name) val state = if (enabled) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED } else { PackageManager.COMPONENT_ENABLED_STATE_DISABLED } packageManager.setComponentEnabledSetting( componentName, state, PackageManager.DONT_KILL_APP ) }

ACTION_MY_PACKAGE_REPLACED で全ての処理をやるのではなく ChangedPackages でも判断できる val packages : ChangedPackages = packageManager.getChangedPackages(prefs.getPackageSequenceNumber()) prefs.setPackageSequenceNumber(packages.getSequenceNumber())

Background Limits

Foreground Service にすべきものならそうする

Foreground のもの
  • Visible App
  • Foreground Service
  • Foreground Client に bound されている Service
  • ForeGround Client への Content Provider
  • AccessibilityService, NotificationListenerService, AbstractAccountAuthenticator, WallpaperServiceなどの例外もある
Background のもの
  • Not visible
  • Non-Foreground Service
  • JobService
  • BroadcastReceiver
O以降では、Background から Service を start しようとすると IllegalStateException が投げられる

Grace Period : Service が background に置かれてから1分程度は生きている

Whitelist
  • Notification action
  • High Priority FCM message
  • SMS/MMS delivery
Background Service を使わずに Background で仕事をさせるには
  • background task には WorkManager を使う
  • IntentService は JobIntentService に置き換える

JobScheduler の振る舞いについてよく理解するまで Android L で JobScheduler を使わないほうがいい
L と M の first release では JobScheduler は同じ constrains の2つの job を正しく実行しない問題がある
この問題の workaround として、同じ constrains の2つの job をスケジューリングすればよい(がどうするかは言ってない)
MR1 以降は JobScheduler はちゃんと動いているが、minimum latecy を 0 にセットするのはやめたほうがよい
失敗したときの処理は backoff でやること
backoff を超えて reschedule しないこと

PendingIntent の向き先を Service から explicit な BroadcastReceiver に変更し、30秒以内に goAsync() を呼び出す
または BroadcastReceiver 内で WorkManager を使う

外部からの time-sensitive な trigger が必要なら Firebase Cloud Messaging を使う
high priority messages はデバイスが DOZE でも起きるので使いすぎはよくない
10秒以内に実行し終えるならそのままそこで処理をし、それ以上かかるなら WorkManager を使う

ユーザーが明示的に始めた時間のかかる処理は Foreground Service で実行する
Maps Navigation, fitness tracking, playing music など

Photo Broadcasts

N 以降では Photo Broadcasts が起こらないので、代わりに ContentUris をトリガーとした work を使う val constrains = Constraints.Builder() .constraints.addContentUriTrigger(SOME_URI, true) ... .build() val work = OneTimeWorkRequest.Builder(MyWork::class.java) .setConstraints(constrains) .build()

Background Location

O 以降のデバイスでは Background Location の制限は targetSdkVersion によらず適用される

対応として
  • 1. Geofencing を使う : 100個までしか Geofencing を active にできないので、必要に応じて動的に変えるなどの対策をとる
  • 2. Beacon による Nearby Notification を使う
  • 3. FusedLocationProvider の Batch 処理 fun createLocationRequest() { ... val request = LocationRequest() request.interval = 10L * 60L * 1000L request.maxWaitTime = 30L * 60L * 1000L }
  • 4. Passive Location を使う fun createLocationRequest() { ... val request = LocationRequest() request.interval = 10L * 60L * 1000L request.maxWaitTime = 30L * 60L * 1000L request.fastestInterval = 2L * 60L * 1000L }
アプリの location を更新するのはネットワーク処理などの重たい処理と紐づいているべき

Battery

バッテリーに関する機能
  • Doze(M+)
  • Doze on the go (N+)
  • App Standby (M~O)
  • App Standby Buckets (P+)
App Standby Buckets (P+)
  • 使用履歴に基づく制限
  • アプリは Standby Bucket のどこかに割り当てられる
  • 割り当てられた Bucket によって適用される制限が変わる


Battery Saver (P+)
  • Screen Off のときは Location を取らない
  • 全てのアプリが App Standby
  • Background のアプリは Network 処理をできない
  • (OLED Devices では)可能なら Dark Theme が有効になる

Testing

Testing Doze $ adb shell dumpsys deviceidle force-idle Testing App Standby $ adb shell dumpsys battery unplug $ adb shell am get-inactive <package-name> $ adb shell am set-inactive <package-name> true Testing App Standby Buckets 1. $ adb shell dumpsys battery unplug 2. $ adb shell am get-standby-bucket <package name> 10 ACTIVE 20 WORKING_SET 30 FREQUENT 40 RARE 3. $ adb shell am set-standby-bucket <package name> <bucket> 4. API: UsageStatsManager.getAppStandbyBucket() Testing Battery Saver $ adb shell dumpsys battery unplug $ adb shell settings put global low_power 1 <do your tests> $ adb shell dumpsys battery reset API: PowerManager.isPowerSaveMode() PowerManager.ACTION_POWER_SAVE_MODE_CHANGED アプリに Dark Theme があるなら Save Mode のときは Dark Theme にするという選択肢
OLED Devices なら電池の節約になる

Modern features

  • Notification Channels
  • Display Cutout : Developer Options で Cutout モードにできる
  • Picture in Picture
  • Multi-display

non-SDK interface

DP1 で non-SDK interface の使用を制限し、使われていたら Toast や log で警告を出すようにした

DP2 ではメソッドが単に動かなくなるので、アプリがクラッシュすることになる

将来的には StrictMode に新しい VM policy を追加する
これを使って全ての non-SDK API を検出できる StrictMode.setVmPolicy( StrictMode.VmPolicy.Builder() .detectNonSdkApiUsage().build()) non-SDK の使用がライブラリ内で起こるかもしれないので、これでチェックすることが重要

https://developer.android.com/distribute/best-practices/develop/target-sdk



2018年5月16日水曜日

IO recap : What's new with ConstraintLayout and Android Studio design tools (Google I/O '18)




design 時の tools: 属性
  • tools:context
  • tools:itemCount
    • ListView, RecyclerView のプレビューで表示するitemの数
  • tools:layout
  • tools:listitem
    • ListView, RecyclerView のプレビューで表示する各itemのレイアウト
  • tools:listheader
  • tools:listfooter
  • tools:showIn
    • 指定した layout リソースに include された状態のプレビューになる
  • tools:menu
  • tools:minValue
  • tools:maxValue
  • tools:openDrawer
  • tools:text
    • プレビューでの文字
  • tools:textColor
    • プレビューでの文字色


サンプルデータ

[File] - [New] - [Sample Data Directory] から sample data を置く場所を作る
場所としては app/ 直下に sampledata/ が作られる



ここに material_colors という名前のファイルを作って、各行にカラーコードを書く #F44336 #9C27B0 #3F51B5 #673AB7 RecyclerView の各 item の ImageView に tools:tint="@sample/material_colors" と指定すると、行ごとに @sample/material_colors 内の色が順番に割り当てられる

dimensions なら 4dp 8dp 16dp ... のようなファイルを用意する

画像の custom sample data はディレクトリ(例えば albumcovers/)を作ってサンプルデータの画像をそこに配置する
ディレクトリ名に _ や大文字を使うと動かないので注意



JSON の custom sample Data では複数のサンプルをまとめて定義できる

albumname.json { "songs" : [ {"title": "Nougat", "author" : "Dylan Dalton"}, {"title": "Ice Cream", "author": "Isa Henderson"}, ... ]} tools:text="@sample/albumname.json/title" tools:text="@sample/albumname.json/author"

Predefined Sample data

Images, Text, olors, Dates...
  • @tools:sample/first_names
  • @tools:sample/last_names
  • @tools:sample/full_names
  • @tools:sample/cities
  • @tools:sample/us_phones
  • @tools:sample/us_zipcodes
  • @tools:sample/date/day_of_week
  • @tools:sample/date/ddmmyy
  • @tools:sample/date/hhmm
  • @tools:sample/date/hhmmss
  • @tools:sample/date/mmddyy
  • @tools:sample/lorem
  • @tools:sample/lorem/random
  • @tools:sample/avatars
  • @tools:sample/backgrounds/scenic
  • ...
Android Studio 3.2 では resource picker の Drawable のところに sample data category が追加された



Android Studio 3.2 の新しい design time helper では、ImageView を選択して表示されるレンチアイコンをクリックして、sample data を切り替えたり、data set のうち一つだけを使うようにできる
Browse をクリックすると、resource picker が開く



design time helper は TextView でも使える







RecyclerView でも使える

listitem に使うレイアウトや itemCount を指定できる
用意されているテンプレを指定すると、そのレイアウトリソースが layout/ にコピーされる
(たまにレンチが出なくなり、まだかなり不安定)












ConstraintLayout

去年(2017) ConstraintLayout 1.0 をリリースした
先月(2018/4) 1.1 をリリースした
implementation 'com.android.support.constraint:constraint-layout:1.1.0'

ConstraintLayout 2.0

Helpers
  • 画面に表示されないがUIを作成するのを助けるもの
  • Guideline とか Barrier とか
  • Viewの参照を保持してあれこれする
  • ConstraintHelper を継承して Custom の Helper を作れる
Helper の3つのカテゴリー
  • Layout Manipulation
    • LinearLayout のような配置を助ける Helper とか FlexBox 的な配置を助けるものとか
  • Post-Layout Manipulation
    • flying object みたいなエフェクトをかけるやつとか
  • Rendering or Decorating
    • Viewの代わりに描画するやつ
Helper の例

Layers
  • Helper の一種
  • View のセットに対する表示・非表示・変形などをサポートする




Circular Reveal
  • Rendering or Decorating 系の Helper
  • 既存の circular reveal code を利用
  • 参照しているViewにだけ適用


Lava Decorator
  • Rendering or Decorating 系の Helper
  • View の background を透明にして、この Decorator が Lava っぽい描画を実現する




Bottom Panel Decorator
  • ボトムパネルの background にインタラクティブなエフェクトを描画するやつ


Helper で view の実態と behavior を分離できる



ConstraintLayout 2.0

ConstraintLayout 2.0 では State を XML で指定できる <ConstraintLayoutStates> <State android:id="+id/small" app:constraints="@layout/layout_small" /> <State android:id="+id/large" app:constraints="@layout/layout_large" /> </ConstraintLayoutStates> fun onCreate(savedInstanceState : Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.layout); cl = findViewById(R.id.root) cl.setLayoutDescription(R.xml.layout_states) } fun change(v : View) { cl.setState(closed ? R.id.large : R.id.small) closed = !closed } 特定の constraint set が適用されるときの region を指定できる <ConstraintLayoutStates> <State app:constraints="@layout/layout_small" > <Constraints app:constraints="@layout/layout_small" app:region_widthLessThan="550dp" /> <Constraints app:constraints="@layout/layout_large" app:region_widthLessThan="450dp" /> </State> </ConstraintLayoutStates> fun onConfigurationChanged(newConfig : Configuration) { super.onConfigurationChanged(newConfig) cl.setState(newConfig.screenWidthDp, newConfig.screenHeightDp) } cl.setOnConstraintsChanged { state, layoutId -> TransitionManager.beginDelayedTransition(cl) }

MotionLayout
  • ConstraintLayout 2.2 予定
  • ConstraintLayout の subclass
  • ConstraintLayout の全てのプロパティを持っている
  • 2つの State 間のアニメーションを代わりにやってくれる
MotionLayout が複数の View と Helper を持ち、MotionScene が2つの ConstraintSet と OnTouch と KeyFrame を持つ
ConstraintSet に custom 属性が増え、アニメーションをカスタマイズできる
MotionLayout は Nest できる




Motion Editor
  • keyframe を追加してアニメーションを編集できる
  • going work now

Codelab