构建自动填充服务

自动填充服务是一种通过向其他应用的视图中注入数据来方便用户填写表单的应用。自动填充服务还可从应用的视图中检索用户数据,并将其存储起来以备后用。自动填充服务通常由管理用户数据的应用提供,比如密码管理器。

Android 通过在 Android 8.0(API 级别 26)和更高版本中提供自动填充框架,让填写表单更轻松。用户只有在自己的设备上有提供自动填充服务的应用时,才能利用自动填充功能。

此页面介绍了如何在您的应用中实现自动填充服务。如果您正在寻找能够展示如何实现服务的代码示例,请参阅 JavaKotlin 中的AutofillFramework 示例。 如需详细了解自动填充服务的工作原理,请参阅 AutofillServiceAutofillManager 类的参考页面。

清单声明和权限

应用如果提供自动填充服务,则必须包含一个描述服务实现的声明。如要指定声明,请在应用清单中包含 <service> 元素。此 <service> 元素必须包含以下属性和元素:

下方给出了一个自动填充服务声明示例:

<service
    android:name=".MyAutofillService"
    android:label="My Autofill Service"
    android:permission="android.permission.BIND_AUTOFILL_SERVICE">
    <intent-filter>
        <action android:name="android.service.autofill.AutofillService" />
    </intent-filter>
    <meta-data
        android:name="android.autofill"
        android:resource="@xml/service_configuration" />
</service>

<meta-data> 元素包含一个指向某 XML 资源的 android:resource 属性,该 XML 资源提供了关于服务的更多详细信息。 上一示例中的 service_configuration 资源指定了一个允许用户配置服务的 activity。下面的示例则展示了该 service_configuration XML 资源:

<autofill-service
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:settingsActivity="com.example.android.SettingsActivity" />

如需详细了解 XML 资源,请参阅应用资源概览

提示启用服务

当应用声明 BIND_AUTOFILL_SERVICE 权限并且用户在设备设置中启用它后,应用便会用作自动填充服务。应用可以通过调用 AutofillManager 类的 hasEnabledAutofillServices() 方法来验证它是否是当前已启用的服务。

如果应用不是当前的自动填充服务,则可以使用 ACTION_REQUEST_SET_AUTOFILL_SERVICE intent 来请求用户更改自动填充设置。如果用户选择的自动填充服务与调用方软件包相匹配,那么 intent 会返回一个 RESULT_OK 值。

填充客户端视图

当用户与其他应用交互时,自动填充服务会收到填充客户端视图的请求。如果自动填充服务拥有可满足相应请求的用户数据,便会在响应中发送这些数据。Android 系统会显示一个包含可用数据的自动填充界面,如图 1 所示:

自动填充界面

图 1. 显示数据集的自动填充界面。

自动填充框架定义了一个用于填充视图的工作流,旨在最大限度地缩短 Android 系统绑定到自动填充服务所需的时间。在每个请求中,Android 系统都通过调用 onFillRequest() 方法来向服务发送一个 AssistStructure 对象。

自动填充服务会检查能否使用之前存储的用户数据满足请求。如果能满足请求,服务便会将数据打包到 Dataset 对象中。服务将调用 onSuccess() 方法,以传递一个包含 Dataset 对象的 FillResponse 对象。如果服务没有可满足请求的数据,则会将 null 传递给 onSuccess() 方法。

如果处理请求时出现错误,服务会调用 onFailure() 方法。如需详细了解该工作流,请参阅 AutofillService 参考页面中的说明

以下代码展示了 onFillRequest() 方法的示例:

Kotlin

override fun onFillRequest(
    request: FillRequest,
    cancellationSignal: CancellationSignal,
    callback: FillCallback
) {
    // Get the structure from the request
    val context: List<FillContext> = request.fillContexts
    val structure: AssistStructure = context[context.size - 1].structure

    // Traverse the structure looking for nodes to fill out
    val parsedStructure: ParsedStructure = parseStructure(structure)

    // Fetch user data that matches the fields
    val (username: String, password: String) = fetchUserData(parsedStructure)

    // Build the presentation of the datasets
    val usernamePresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
    usernamePresentation.setTextViewText(android.R.id.text1, "my_username")
    val passwordPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
    passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username")

    // Add a dataset to the response
    val fillResponse: FillResponse = FillResponse.Builder()
            .addDataset(Dataset.Builder()
                    .setValue(
                            parsedStructure.usernameId,
                            AutofillValue.forText(username),
                            usernamePresentation
                    )
                    .setValue(
                            parsedStructure.passwordId,
                            AutofillValue.forText(password),
                            passwordPresentation
                    )
                    .build())
            .build()

    // If there are no errors, call onSuccess() and pass the response
    callback.onSuccess(fillResponse)
}

data class ParsedStructure(var usernameId: AutofillId, var passwordId: AutofillId)

data class UserData(var username: String, var password: String)

Java

@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
    // Get the structure from the request
    List<FillContext> context = request.getFillContexts();
    AssistStructure structure = context.get(context.size() - 1).getStructure();

    // Traverse the structure looking for nodes to fill out
    ParsedStructure parsedStructure = parseStructure(structure);

    // Fetch user data that matches the fields
    UserData userData = fetchUserData(parsedStructure);

    // Build the presentation of the datasets
    RemoteViews usernamePresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
    usernamePresentation.setTextViewText(android.R.id.text1, "my_username");
    RemoteViews passwordPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
    passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username");

    // Add a dataset to the response
    FillResponse fillResponse = new FillResponse.Builder()
            .addDataset(new Dataset.Builder()
                    .setValue(parsedStructure.usernameId,
                            AutofillValue.forText(userData.username), usernamePresentation)
                    .setValue(parsedStructure.passwordId,
                            AutofillValue.forText(userData.password), passwordPresentation)
                    .build())
            .build();

    // If there are no errors, call onSuccess() and pass the response
    callback.onSuccess(fillResponse);
}

class ParsedStructure {
    AutofillId usernameId;
    AutofillId passwordId;
}

class UserData {
    String username;
    String password;
}

一项服务可有多个能满足请求的数据集。此时,Android 系统会在自动填充界面显示多个选项,每个选项对应一个数据集。以下代码示例展示了如何在响应中提供多个数据集:

Kotlin

// Add multiple datasets to the response
val fillResponse: FillResponse = FillResponse.Builder()
        .addDataset(Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user1Data.username), username1Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user1Data.password), password1Presentation)
                .build())
        .addDataset(Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user2Data.username), username2Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user2Data.password), password2Presentation)
                .build())
        .build()

Java

// Add multiple datasets to the response
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user1Data.username), username1Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user1Data.password), password1Presentation)
                .build())
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user2Data.username), username2Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user2Data.password), password2Presentation)
                .build())
        .build();

自动填充服务可在 AssistStructure 中浏览 ViewNode 对象,以检索满足请求所需的自动填充数据。服务可以使用 ViewNode 类中的方法检索自动填充数据,例如 getAutofillId() 方法。

服务必须能描述视图的内容,以检查是否能够满足请求。使用 autofillHints 属性是服务必须用来描述视图内容的首选方法。不过,客户端应用必须在其视图中明确提供该属性,然后它才能供服务使用。

如果客户端应用未提供 autofillHints 属性,服务必须使用自己的启发法来描述内容。 服务可以使用其他类(例如 getText()getHint())中的方法来获取视图内容的相关信息。 如需了解详情,请参阅提供自动填充提示

以下示例展示了如何遍历 AssistStructure 以及从 ViewNode 对象中检索自动填充数据:

Kotlin

fun traverseStructure(structure: AssistStructure) {
    val windowNodes: List<AssistStructure.WindowNode> =
            structure.run {
                (0 until windowNodeCount).map { getWindowNodeAt(it) }
            }

    windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
        val viewNode: ViewNode? = windowNode.rootViewNode
        traverseNode(viewNode)
    }
}

fun traverseNode(viewNode: ViewNode?) {
    if (viewNode?.autofillHints?.isNotEmpty() == true) {
        // If the client app provides autofill hints, you can obtain them using
        // viewNode.getAutofillHints();
    } else {
        // Or use your own heuristics to describe the contents of a view
        // using methods such as getText() or getHint()
    }

    val children: List<ViewNode>? =
            viewNode?.run {
                (0 until childCount).map { getChildAt(it) }
            }

    children?.forEach { childNode: ViewNode ->
        traverseNode(childNode)
    }
}

Java

public void traverseStructure(AssistStructure structure) {
    int nodes = structure.getWindowNodeCount();

    for (int i = 0; i < nodes; i++) {
        WindowNode windowNode = structure.getWindowNodeAt(i);
        ViewNode viewNode = windowNode.getRootViewNode();
        traverseNode(viewNode);
    }
}

public void traverseNode(ViewNode viewNode) {
    if(viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) {
        // If the client app provides autofill hints, you can obtain them using
        // viewNode.getAutofillHints();
    } else {
        // Or use your own heuristics to describe the contents of a view
        // using methods such as getText() or getHint()
    }

    for(int i = 0; i < viewNode.getChildCount(); i++) {
        ViewNode childNode = viewNode.getChildAt(i);
        traverseNode(childNode);
    }
}

保存用户数据

自动填充服务需要用户数据来填充应用中的视图。当用户手动填充视图时,系统会提示他们将数据保存至当前的自动填充服务,如图 2 所示。

自动填充保存界面

图 2. 自动填充保存界面。

如要保存数据,服务必须表明它想保存数据以备将来使用。Android 系统在发送保存数据的请求之前,会先发送一个填充请求,服务可通过此请求填充视图。为表明想保存数据,该服务会在针对填充请求而发送的响应中添加一个 SaveInfo 对象。SaveInfo 对象中至少包含以下数据:

  • 已保存的用户数据的类型。如需查看可用 SAVE_DATA 值的列表,请参阅 SaveInfo
  • 需要更改以触发保存请求的最小视图集。例如,登录表单通常要求用户更新 usernamepassword 视图以触发保存请求。

SaveInfo 对象与 FillResponse 对象关联,如以下代码示例所示:

Kotlin

override fun onFillRequest(
    request: FillRequest,
    cancellationSignal: CancellationSignal,
    callback: FillCallback
) {
    ...
    // Builder object requires a non-null presentation
    val notUsed = RemoteViews(packageName, android.R.layout.simple_list_item_1)

    val fillResponse: FillResponse = FillResponse.Builder()
            .addDataset(
                    Dataset.Builder()
                            .setValue(parsedStructure.usernameId, null, notUsed)
                            .setValue(parsedStructure.passwordId, null, notUsed)
                            .build()
            )
            .setSaveInfo(
                    SaveInfo.Builder(
                            SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                            arrayOf(parsedStructure.usernameId, parsedStructure.passwordId)
                    ).build()
            )
            .build()
    ...
}

Java

@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
    ...
    // Builder object requires a non-null presentation
    RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);

    FillResponse fillResponse = new FillResponse.Builder()
            .addDataset(new Dataset.Builder()
                    .setValue(parsedStructure.usernameId, null, notUsed)
                    .setValue(parsedStructure.passwordId, null, notUsed)
                    .build())
            .setSaveInfo(new SaveInfo.Builder(
                    SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                    new AutofillId[] {parsedStructure.usernameId, parsedStructure.passwordId})
                    .build())
            .build();
    ...
}

自动填充服务可实现将用户数据保存到 onSaveRequest() 方法中所需的逻辑,该方法的调用通常发生在客户端 activity 结束后或客户端应用调用 commit() 时。 以下代码展示了 onSaveRequest() 方法的示例:

Kotlin

override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
    // Get the structure from the request
    val context: List<FillContext> = request.fillContexts
    val structure: AssistStructure = context[context.size - 1].structure

    // Traverse the structure looking for data to save
    traverseStructure(structure)

    // Persist the data - if there are no errors, call onSuccess()
    callback.onSuccess()
}

Java

@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
    // Get the structure from the request
    List<FillContext> context = request.getFillContexts();
    AssistStructure structure = context.get(context.size() - 1).getStructure();

    // Traverse the structure looking for data to save
    traverseStructure(structure);

    // Persist the data - if there are no errors, call onSuccess()
    callback.onSuccess();
}

自动填充服务必须在保存敏感数据之前对其进行加密。但是,用户数据可能包含不敏感的标签或数据。例如,用户账号中可能包含一种标签,它们将数据标记为工作或个人账号数据。服务不得对标签进行加密。这样,服务便能在用户未验证身份时在演示视图中使用标签。待用户验证身份之后,服务可将标签替换为实际数据。

推迟自动填充保存界面

从 Android 10 开始,如果您使用多个屏幕来实现自动填充工作流(例如,一个屏幕用于用户名字段填充,另一个屏幕用于密码填充),那么可以使用 SaveInfo.FLAG_DELAY_SAVE 标志推迟自动填充保存界面。

如果设置了此标志,当提交与 SaveInfo 响应关联的自动填充上下文时,不会触发自动填充保存界面。您可以改为在同一任务中使用单独的 activity 来提供未来的填充请求,然后通过保存请求显示自动填充保存界面。如需了解详情,请参阅 SaveInfo.FLAG_DELAY_SAVE

要求用户进行身份验证

自动填充服务要求用户先进行身份验证,然后才能填充视图,从而提高了安全系数。下面的场景很适合实现用户身份验证:

  • 应用中的用户数据需要使用主密码或指纹扫描进行解锁。
  • 特定数据集(例如信用卡详情)需要使用银行卡验证码 (CVC) 进行解锁。

当服务需要用户身份验证才能解锁数据时,服务可以提供样板数据或标签,并指定负责身份验证的 Intent。如果您在身份验证流程结束后需要更多数据来处理请求,可以将此类数据添加到 intent 中。然后,您的身份验证 activity 可将数据返回给应用中的 AutofillService 类。

以下是一个展示如何指明请求需要身份验证的代码示例:

Kotlin

val authPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
    setTextViewText(android.R.id.text1, "requires authentication")
}
val authIntent = Intent(this, AuthActivity::class.java).apply {
    // Send any additional data required to complete the request
    putExtra(MY_EXTRA_DATASET_NAME, "my_dataset")
}

val intentSender: IntentSender = PendingIntent.getActivity(
        this,
        1001,
        authIntent,
        PendingIntent.FLAG_CANCEL_CURRENT
).intentSender

// Build a FillResponse object that requires authentication
val fillResponse: FillResponse = FillResponse.Builder()
        .setAuthentication(autofillIds, intentSender, authPresentation)
        .build()

Java

RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
authPresentation.setTextViewText(android.R.id.text1, "requires authentication");
Intent authIntent = new Intent(this, AuthActivity.class);

// Send any additional data required to complete the request
authIntent.putExtra(MY_EXTRA_DATASET_NAME, "my_dataset");
IntentSender intentSender = PendingIntent.getActivity(
                this,
                1001,
                authIntent,
                PendingIntent.FLAG_CANCEL_CURRENT
        ).getIntentSender();

// Build a FillResponse object that requires authentication
FillResponse fillResponse = new FillResponse.Builder()
        .setAuthentication(autofillIds, intentSender, authPresentation)
        .build();

activity 一旦完成身份验证流程,就必须调用传递 RESULT_OK 值的 setResult() 方法,并将 EXTRA_AUTHENTICATION_RESULT extra 设为包含所填充数据集的 FillResponse 对象。以下代码示例展示了如何在身份验证流程完成后返回结果:

Kotlin

// The data sent by the service and the structure are included in the intent
val datasetName: String? = intent.getStringExtra(MY_EXTRA_DATASET_NAME)
val structure: AssistStructure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE)
val parsedStructure: ParsedStructure = parseStructure(structure)
val (username, password) = fetchUserData(parsedStructure)

// Build the presentation of the datasets
val usernamePresentation =
        RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
            setTextViewText(android.R.id.text1, "my_username")
        }
val passwordPresentation =
        RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
            setTextViewText(android.R.id.text1, "Password for my_username")
        }

// Add the dataset to the response
val fillResponse: FillResponse = FillResponse.Builder()
        .addDataset(Dataset.Builder()
                .setValue(
                        parsedStructure.usernameId,
                        AutofillValue.forText(username),
                        usernamePresentation
                )
                .setValue(
                        parsedStructure.passwordId,
                        AutofillValue.forText(password),
                        passwordPresentation
                )
                .build()
        ).build()

val replyIntent = Intent().apply {
    // Send the data back to the service
    putExtra(MY_EXTRA_DATASET_NAME, datasetName)
    putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse)
}

setResult(Activity.RESULT_OK, replyIntent)

Java

Intent intent = getIntent();

// The data sent by the service and the structure are included in the intent
String datasetName = intent.getStringExtra(MY_EXTRA_DATASET_NAME);
AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE);
ParsedStructure parsedStructure = parseStructure(structure);
UserData userData = fetchUserData(parsedStructure);

// Build the presentation of the datasets
RemoteViews usernamePresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
usernamePresentation.setTextViewText(android.R.id.text1, "my_username");
RemoteViews passwordPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username");

// Add the dataset to the response
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(userData.username), usernamePresentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(userData.password), passwordPresentation)
                .build())
        .build();

Intent replyIntent = new Intent();

// Send the data back to the service
replyIntent.putExtra(MY_EXTRA_DATASET_NAME, datasetName);
replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse);

setResult(RESULT_OK, replyIntent);

当需要解锁信用卡数据集时,服务可显示请求 CVC 的界面。您可以通过显示样板数据(例如银行名称和信用卡号码的最后四位数)隐藏数据,直到数据集解锁。以下示例展示了如何要求针对数据集进行身份验证,并隐藏数据,直到用户提供 CVC:

Kotlin

// Parse the structure and fetch payment data
val parsedStructure: ParsedStructure = parseStructure(structure)
val paymentData: Payment = fetchPaymentData(parsedStructure)

// Build the presentation that shows the bank and the last four digits of the
// credit card number, such as 'Bank-1234'
val maskedPresentation: String = "${paymentData.bank}-" +
        paymentData.creditCardNumber.substring(paymentData.creditCardNumber.length - 4)
val authPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
    setTextViewText(android.R.id.text1, maskedPresentation)
}

// Prepare an intent that displays the UI that asks for the CVC
val cvcIntent = Intent(this, CvcActivity::class.java)
val cvcIntentSender: IntentSender = PendingIntent.getActivity(
        this,
        1001,
        cvcIntent,
        PendingIntent.FLAG_CANCEL_CURRENT
).intentSender

// Build a FillResponse object that includes a Dataset that requires authentication
val fillResponse: FillResponse = FillResponse.Builder()
        .addDataset(
                Dataset.Builder()
                        // The values in the dataset are replaced by the actual
                        // data once the user provides the CVC
                        .setValue(parsedStructure.creditCardId, null, authPresentation)
                        .setValue(parsedStructure.expDateId, null, authPresentation)
                        .setAuthentication(cvcIntentSender)
                        .build()
        ).build()

Java

// Parse the structure and fetch payment data
ParsedStructure parsedStructure = parseStructure(structure);
Payment paymentData = fetchPaymentData(parsedStructure);

// Build the presentation that shows the bank and the last four digits of the
// credit card number, such as 'Bank-1234'
String maskedPresentation = paymentData.bank + "-" +
    paymentData.creditCardNumber.subString(paymentData.creditCardNumber.length - 4);
RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
authPresentation.setTextViewText(android.R.id.text1, maskedPresentation);

// Prepare an intent that displays the UI that asks for the CVC
Intent cvcIntent = new Intent(this, CvcActivity.class);
IntentSender cvcIntentSender = PendingIntent.getActivity(
        this,
        1001,
        cvcIntent,
        PendingIntent.FLAG_CANCEL_CURRENT
).getIntentSender();

// Build a FillResponse object that includes a Dataset that requires authentication
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                // The values in the dataset are replaced by the actual
                // data once the user provides the CVC
                .setValue(parsedStructure.creditCardId, null, authPresentation)
                .setValue(parsedStructure.expDateId, null, authPresentation)
                .setAuthentication(cvcIntentSender)
                .build())
        .build();

activity 一旦完成 CVC 验证,就应调用传递 RESULT_OK 值的 setResult() 方法,并将 EXTRA_AUTHENTICATION_RESULT extra 设置为包含信用卡号码和失效日期的 Dataset 对象。新数据集会替换需要身份验证的数据集,视图会立即得到填充。以下代码示例展示了如何在用户提供 CVC 后返回数据集:

Kotlin

// Parse the structure and fetch payment data.
val parsedStructure: ParsedStructure = parseStructure(structure)
val paymentData: Payment = fetchPaymentData(parsedStructure)

// Build a non-null RemoteViews object to use as the presentation when
// creating the Dataset object. This presentation isn't actually used, but the
// Builder object requires a non-null presentation.
val notUsed = RemoteViews(packageName, android.R.layout.simple_list_item_1)

// Create a dataset with the credit card number and expiration date.
val responseDataset: Dataset = Dataset.Builder()
        .setValue(
                parsedStructure.creditCardId,
                AutofillValue.forText(paymentData.creditCardNumber),
                notUsed
        )
        .setValue(
                parsedStructure.expDateId,
                AutofillValue.forText(paymentData.expirationDate),
                notUsed
        )
        .build()

val replyIntent = Intent().apply {
    putExtra(EXTRA_AUTHENTICATION_RESULT, responseDataset)
}

Java

// Parse the structure and fetch payment data.
ParsedStructure parsedStructure = parseStructure(structure);
Payment paymentData = fetchPaymentData(parsedStructure);

// Build a non-null RemoteViews object to use as the presentation when
// creating the Dataset object. This presentation isn't actually used, but the
// Builder object requires a non-null presentation.
RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);

// Create a dataset with the credit card number and expiration date.
Dataset responseDataset = new Dataset.Builder()
        .setValue(parsedStructure.creditCardId,
                AutofillValue.forText(paymentData.creditCardNumber), notUsed)
        .setValue(parsedStructure.expDateId,
                AutofillValue.forText(paymentData.expirationDate), notUsed)
        .build();

Intent replyIntent = new Intent();
replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, responseDataset);

将数据整理为不同的逻辑组

自动填充服务必须将数据整理为不同的逻辑组,这些逻辑组将不同范畴的概念隔离开来。在本页中,这些逻辑组称为分区。以下列表显示了分区和字段的典型示例:

  • 凭据,其中包含用户名和密码字段。
  • 地址,其中包含街道、城市、州/省级行政区和邮政编码字段。
  • 付款信息,其中包含信用卡号码、失效日期和验证码字段。

进行正确数据分区的自动填充服务仅公开数据集中一个分区内的数据,因而能更好地保护其用户的数据。例如,包含凭据的数据集不需要包含付款信息。通过将数据分区,您的服务可公开所需的最少量相关信息来满足请求。

通过将数据分区,服务不仅可填充视图分布在多个分区中的 activity,还能向客户端应用发送最少量的相关数据。例如,假设有一个包含用户名、密码、街道和城市视图的 activity,以及一个包含以下数据的自动填充服务:

分区 字段 1 字段 2
凭据 work_username work_password
personal_username personal_password
地址 work_street work_city
personal_street personal_city

服务可以准备一个数据集,其中的凭据分区同时包含工作账号和个人账号的数据。如果用户选择了某个数据集,后续的自动填充响应便能提供工作地址或个人地址,具体取决于用户的首选项。

服务可通过在遍历 AssistStructure 对象的同时调用 isFocused() 方法来识别发出请求的字段。这样,服务便能准备一个包含适当分区数据的 FillResponse

一次性短信验证码自动填充

您的自动填充服务可以使用 SMS Retriever API 帮助用户填充通过短信发送的一次性验证码。

如需使用此功能,必须满足以下要求:

  • 自动填充服务在 Android 9(API 级别 28)或更高版本上运行。
  • 用户已同意让您的自动填充服务读取短信中的一次性验证码。
  • 您从中提供自动填充服务的应用尚未使用 SMS Retriever API 读取一次性验证码。

您的自动填充服务可以使用 SmsCodeAutofillClient(通过从 Google Play 服务 19.0.56 或更高版本调用 SmsCodeRetriever.getAutofillClient())。

在自动填充服务中使用此 API 的主要步骤如下:

  1. 在自动填充服务中,使用 SmsCodeAutofillClient 中的 hasOngoingSmsRequest 来确定是否已经存在针对您要自动填充的应用软件包名称的有效请求。仅当返回 false 时,您的自动填充服务才应显示建议提示。
  2. 在自动填充服务中,使用 SmsCodeAutofillClient 中的 checkPermissionState 检查自动填充服务是否具备自动填充一次性验证码的权限。此权限状态可以是 NONEGRANTEDDENIED。自动填充服务必须针对 NONEGRANTED 状态显示建议提示。
  3. 在自动填充身份验证 activity 中,使用 SmsRetriever.SEND_PERMISSION 权限注册一个用于监听 SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTIONBroadcastReceiver,以便在有可用短信验证码结果时进行接收。
  4. SmsCodeAutofillClient 调用 startSmsCodeRetriever,以开始监听通过短信发送的一次性验证码。 如果用户授权您的自动填充服务检索短信中的一次性验证码,服务便会查找过去 1-5 分钟内收到的短信。

    如果您的自动填充服务需要请求用户权限以读取一次性验证码,那么 startSmsCodeRetriever 返回的 Task 可能会失败并返回 ResolvableApiException。如果发生这种情况,您需要调用 ResolvableApiException.startResolutionForResult() 方法来显示与权限请求对应的意见征求对话框。

  5. 从 intent 接收短信验证码结果,然后以自动填充响应的形式返回短信验证码。

高级自动填充场景

与键盘集成
从 Android 11 开始,平台允许键盘和其他输入法 (IME) 以内嵌方式(而不是使用下拉菜单)显示自动填充建议。 如需详细了解自动填充服务如何支持此功能,请参阅将自动填充功能与键盘集成
将数据集分页
较大的自动填充响应可能会超过 Binder 对象所允许的事务大小,该对象表示处理请求所需的远程对象。为了防止 Android 系统在此类情况下抛出异常,您可以将一次添加的 Dataset 对象数控制在 20 个以内,以便使 FillResponse 保持较小大小。如果您的响应需要更多数据集,您可以添加一个数据集,通过它让用户知道还有更多信息,并让用户能够通过主动选择来检索下一组数据集。如需了解详情,请参阅 addDataset(Dataset)
保存分布在多个屏幕中的数据

在同一个 activity 中,应用经常将用户数据拆分到多个屏幕中,尤其是在用于创建新用户账号的 activity 中。例如,第一个屏幕要求提供用户名,若有可用的用户名,第二个屏幕会要求输入密码。 针对此类情况,自动填充服务必须等到用户填写两个字段后才能显示自动填充保存界面。请按照以下步骤处理此类情况:

  1. 在第一个填充请求中,在包含与屏幕所显示部分字段的自动填充 ID 对应的响应中添加一个客户端状态软件包
  2. 在第二个填充请求中,检索客户端状态软件包,从客户端状态获取上一个请求中设置的自动填充 ID,然后将这些 ID 和 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE 标志添加到第二个响应中使用的 SaveInfo 对象。
  3. 保存请求中,使用适当的 FillContext 对象获取每个字段的值。每个填充请求都有 1 个填充上下文。

如需了解详情,请参阅保存分布在多个屏幕中的数据

为每个请求提供初始化和拆解逻辑

每当有自动填充请求时,Android 系统都会绑定到服务,并调用其 onConnected() 方法。 当服务处理完请求后,Android 系统会调用 onDisconnected() 方法并从服务解绑。您可以实现 onConnected() 来提供在处理请求之前运行的代码,实现 onDisconnected() 来提供在处理请求之后运行的代码。

自定义自动填充保存界面

自动填充服务可以自定义自动填充保存界面,从而帮助用户决定是否允许该服务保存他们的数据。服务可通过简单文本或自定义视图提供与所存内容有关的更多信息。此外,服务还可以更改用于取消保存请求的按钮的外观,并在用户点按该按钮时收到通知。如需了解详情,请参阅 SaveInfo 参考页面。

兼容模式

兼容模式允许自动填充服务将无障碍虚拟结构用于自动填充目的。 它特别适用于在未明确实现自动填充 API 的浏览器中提供自动填充功能。

若要使用兼容模式测试自动填充服务,请将需要使用兼容模式的浏览器或应用明确列入许可名单。您可通过运行以下命令检查哪些软件包已列入许可名单:

$ adb shell settings get global autofill_compat_mode_allowed_packages

如果您要测试的软件包未列入许可名单,请通过运行以下命令进行添加,其中 pkgX 是应用的软件包:

$ adb shell settings put global autofill_compat_mode_allowed_packages pkg1[resId1]:pkg2[resId1,resId2]

如果应用是一个浏览器,则需使用 resIdx 指定包含所呈现页面网址的输入字段的资源 ID。

兼容模式具有以下局限性:

  • 当服务使用 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE 标志或调用 setTrigger() 方法时,会触发保存请求。使用兼容模式时,系统会默认设置 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE
  • onSaveRequest(SaveRequest, SaveCallback) 方法可能不包含节点的文本值。

如需详细了解兼容模式及其局限性,请参阅 AutofillService 类参考文档。