0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

自動入力サービス実装メモ

Posted at

上司から急に「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を指定できる。
AndroidManifest.xml
<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
    • 自動入力サービスの設定に使用したいアクティビティ
xml/user_service.xml
<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
上記を読んだ感じ、大体以下のような流れで自動入力が走る

  1. ユーザが編集可能なビューにフォーカスする。
  2. ビューがAutofillManager#notifyViewEntered(android.view.View)を呼び出す。
  3. 全てのビューを表すViewStructureが作成される。サービスはこのクラスから表示されているビューにアクセスする。
  4. Androidシステムが自動入力サービスにバインドし、onConnect()を呼び出す。
  5. サービスがonFillRequest(android.service.autofill.FillRequest, android.os.CancellationSignal, android.service.autofill.FillCallback)コールバックでViewStructureを受け取る。
    • ViewStructureFillRequestから取得できる
  6. サービスがFillCallback#onSuccess(FillResponse)を使用して応答する
  7. AndroidシステムがonDisconnected()を呼び出してバインドを解除する
  8. Androidシステムがサービスで作成したオプションを含む自動入力UIを表示する
  9. ユーザがオプションを選択する
  10. ビューに自動入力される

開発者が主に実装する箇所

開発者としては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を作成する

実際にユーザに選んでもらうデータを作成する。
以下の実装例は、AutofillHintusernamepasswordのどちらかが
設定されている場合に自動入力させたい場合の処理を記載している。

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で良いので呼び出す必要がある。

その他

サービスに適宜バインドして、入力を作り終わったらバインド解除をしている。
なので、常駐サービスの様に常に起動しているわけではない。(状態を持たせたりするのは難しいんじゃないかな?)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?