上司から急に「Androidでこれできない?」って言われた人(a.k.a自分)用メモ
自動入力サービスってなに
https://developer.android.com/guide/topics/text/autofill-services?hl=ja
フォームに入力する手間を省くアプリ
多分このメモよりも上記のリファレンスを読んだほうが良い
自動入力サービスというだけあって
サービスを実装して、自前で自動入力の処理を実装する必要がありそう
サービスを実装するには
AndroidManifest.xml
の中に以下の属性を定義する
- android:name
- サービスを実装するアプリのクラス名。
AutofillService
を継承していること。
- サービスを実装するアプリのクラス名。
- android:permission
-
BIND_AUTOFILL_SERVICE
パーミッションを宣言。
ユーザが端末の設定で作成した自動入力サービスを有効にできる。
-
-
<intent-filter>
-
<action>
にandroid.service.autofill.AutofillService
を指定する。
-
-
<meta-data>
- (オプション)実装した自動入力サービスの設定をする
Activity
を指定できる。
- (オプション)実装した自動入力サービスの設定をする
<service
android:name=".UserAutofillService"
android:label="デモ用自動入力サービス"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data
android:name="android.autofill"
android:resource="@xml/user_service"/>
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
meta-data要素について
ここには自動入力サービスを設定するためのアクティビティを設定できる。
ここでアクティビティを設定しておくと
設定から自動入力サービスを選択した際に、右側に歯車のマークが出て
タップすると該当のアクティビティが開く様になる。
- android:settingsActivity
- 自動入力サービスの設定に使用したいアクティビティ
<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.android.SettingsActivity" />
作ったサービスを使う
ユーザが設定する場合は設定 > システム > 言語と入力 > 詳細設定 > 自動入力サービス
から設定可能
(Xperia XZ2@Android10 で確認)
アプリから設定させる場合は、ACTION_REQUEST_SET_AUTOFILL_SERVICE
インテントを使うと
自動入力設定を変更するリクエストを画面に表示することができる。
(ただし、設定画面を開くだけなのでユーザの操作は必須っぽい)
// 念の為端末に自動入力サービスがあるか確認
getSystemService(AutofillManager::class.java) ?: return
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
// 設定したい自動入力サービスのパッケージ名
intent.data = Uri.parse("package:com.example.myautofillservice")
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT)
パッケージ名が一致する自動入力サービスを、ユーザーが選択した場合はRESULT_OK
値が返る
自動入力サービスの実装
ユーザが自動入力するまでの流れ
https://developer.android.com/reference/android/service/autofill/AutofillService?hl=ja#BasicUsage
上記を読んだ感じ、大体以下のような流れで自動入力が走る
- ユーザが編集可能なビューにフォーカスする。
- ビューが
AutofillManager#notifyViewEntered(android.view.View)
を呼び出す。 - 全てのビューを表す
ViewStructure
が作成される。サービスはこのクラスから表示されているビューにアクセスする。 - Androidシステムが自動入力サービスにバインドし、
onConnect()
を呼び出す。 - サービスが
onFillRequest(android.service.autofill.FillRequest, android.os.CancellationSignal, android.service.autofill.FillCallback)
コールバックでViewStructure
を受け取る。-
ViewStructure
はFillRequest
から取得できる
-
- サービスが
FillCallback#onSuccess(FillResponse)
を使用して応答する - Androidシステムが
onDisconnected()
を呼び出してバインドを解除する - Androidシステムがサービスで作成したオプションを含む自動入力UIを表示する
- ユーザがオプションを選択する
- ビューに自動入力される
開発者が主に実装する箇所
開発者としてはAutofillService
クラスを継承したサービスを作成し
onFillRequest
メソッドを目的に合わせて実装することで自動入力ができる
ビュー解析
ビュー構造を解析して自動入力するビューを探す。
以下の実装例ではビューに設定されているautofillHints
を確認して
fields
にヒントとIdを登録している。
fun getAutofillableFields(structure: AssistStructure): Map<String, AutofillId> {
val fields: MutableMap<String, AutofillId> = mutableMapOf()
val nodes = structure.windowNodeCount
for (i in 0 until nodes) {
val node = structure.getWindowNodeAt(i).rootViewNode
addAutofillableFields(fields, node)
}
return fields
}
private fun addAutofillableFields(
fields: MutableMap<String, AutofillId>, node: ViewNode
) {
val hints = node.autofillHints
if (hints == null) {
// 子ノードに対しても同様に再帰で調べる
val childrenSize = node.childCount
for (i in 0 until childrenSize) {
addAutofillableFields(fields, node.getChildAt(i))
}
return
}
// とりあえず最初のヒントだけ確認する
val hint = hints[0].toLowerCase(Locale.getDefault())
val id = node.autofillId
if (id == null) {
Log.d(TAG, "addAutofillableFields: autofillId == null")
} else {
if (!fields.containsKey(hint)) {
Log.v(TAG, "$id にヒントを設定 '$hint' ")
fields[hint] = id
}
}
val childrenSize = node.childCount
for (i in 0 until childrenSize) {
addAutofillableFields(fields, node.getChildAt(i))
}
}
自動入力データの取得
自動入力するビューに対応するユーザのデータを探す。
実際はユーザIDやパスワード等を、ビジネスロジックに合わせて取得する処理になると思うので
その時その時でいい感じに実装する。
Dataset
を作成する
実際にユーザに選んでもらうデータを作成する。
以下の実装例は、AutofillHint
にusername
とpassword
のどちらかが
設定されている場合に自動入力させたい場合の処理を記載している。
val packageName = applicationContext.packageName
val response = FillResponse.Builder()
val dataset = Dataset.Builder()
// AutofillHintとIdでペアとしている
for ((hint, id) in fields) {
when {
hint.contains("username") -> {
val userName = pref.userName
val presentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
// ユーザへ表示するテキスト(例:ユーザ名、パスワード)
presentation.setTextViewText(android.R.id.text1, userName)
// 自動入力したいビューのid, 自動入力する値(例:username、passw0rd), ユーザに表示するビュー
dataset.setValue(id, AutofillValue.forText(userName), presentation)
}
hint.contains("password") -> {
val userName = pref.passWord
val presentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
presentation.setTextViewText(android.R.id.text1, "password for $userName")
dataset.setValue(id, AutofillValue.forText(userName), presentation)
}
else -> {
Log.d(TAG, "onFillRequest: hint:$hint id:$id")
}
}
}
response.addDataset(dataset.build())
結果の返却
作ったDatasetをresponseに詰めてcallback.onSuccess(response.build())
を呼ぶ。
仮に自動入力できない場合でもonSuccess
メソッドはnullで良いので呼び出す必要がある。
その他
サービスに適宜バインドして、入力を作り終わったらバインド解除をしている。
なので、常駐サービスの様に常に起動しているわけではない。(状態を持たせたりするのは難しいんじゃないかな?)