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

上司から急に「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`インテントを使うと
自動入力設定を変更するリクエストを画面に表示することができる。
(ただし、設定画面を開くだけなのでユーザの操作は必須っぽい)

``` kotlin
// 念の為端末に自動入力サービスがあるか確認
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`を受け取る。
   - `ViewStructure`は`FillRequest`から取得できる
6. サービスが`FillCallback#onSuccess(FillResponse)`を使用して応答する
7. Androidシステムが`onDisconnected()`を呼び出してバインドを解除する
8. Androidシステムがサービスで作成したオプションを含む自動入力UIを表示する
9.  ユーザがオプションを選択する
10. ビューに自動入力される

### 開発者が主に実装する箇所
開発者としては`AutofillService`クラスを継承したサービスを作成し
`onFillRequest`メソッドを目的に合わせて実装することで自動入力ができる

#### ビュー解析
ビュー構造を解析して自動入力するビューを探す。
以下の実装例ではビューに設定されている`autofillHints`を確認して
`fields`にヒントとIdを登録している。

``` kotlin
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`のどちらかが
設定されている場合に自動入力させたい場合の処理を記載している。

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

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