构建自动填充服务

自动填充服务是一个应用程序,通过将数据注入其他应用程序的视图中,使用户更容易填写表单。自动填充服务还可以从应用程序中的视图中检索用户数据并将其存储以供以后使用。自动填充服务通常由管理用户数据的应用程序(如密码管理器)提供。

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> 元素包含一个 android:resource 属性,指向包含有关服务更多详细信息的 XML 资源。前面的示例中的 service_configuration 资源指定了一个允许用户配置服务的活动。以下示例显示 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 意图请求用户更改自动填充设置。如果用户选择了与调用者包匹配的自动填充服务,则该意图将返回 RESULT_OK 的值。

填写客户端视图

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

Autofill UI

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

自动填充框架定义了一个填写视图的工作流程,该流程旨在最大限度地减少 Android 系统绑定到自动填充服务的时间。在每次请求中,Android 系统都会通过调用 AssistStructure 对象来调用 onFillRequest() 方法,将该对象发送到服务。

自动填充服务会检查它是否可以使用之前存储的用户数据来满足请求。如果可以满足请求,则服务会将数据打包在 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 系统会在自动填充 UI 中显示多个选项,每个选项对应一个数据集。以下代码示例显示了如何在响应中提供多个数据集。

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();

自动填充服务可以导航 ViewNode 对象以检索 AssistStructure 中的自动填充数据,从而满足请求。服务可以使用 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 所示。

Autofill save UI

图 2. 自动填充保存 UI。

要保存数据,服务必须表明它有兴趣将数据存储以供将来使用。在 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() 方法中实现逻辑来持久保存用户数据,该方法通常在客户端活动完成或客户端应用调用 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();
}

自动填充服务必须在持久保存敏感数据之前对其进行加密。但是,用户数据可以包含标签或非敏感数据。例如,用户帐户可以包含将数据标记为工作帐户或个人帐户的标签。服务不得加密标签。通过不加密标签,服务可以在用户未进行身份验证的情况下使用标签在演示视图中。然后,服务可以在用户进行身份验证后将标签替换为实际数据。

推迟自动填充保存 UI

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

如果设置了此标志,则在提交与 SaveInfo 响应相关的自动填充上下文时,不会触发自动填充保存 UI。相反,您可以使用同一任务中的单独活动来传送未来的填写请求,然后通过保存请求显示 UI。有关更多信息,请参阅 SaveInfo.FLAG_DELAY_SAVE

要求用户进行身份验证

自动填充服务可以通过要求用户进行身份验证才能填写视图来提供额外的安全级别。以下情况非常适合实施用户身份验证。

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

在服务需要用户身份验证才能解锁数据的情况下,服务可以呈现样板数据或标签,并指定用于处理身份验证的 Intent。如果您需要在身份验证流程完成后处理请求的其他数据,则可以将这些数据添加到意图中。然后,您的身份验证活动可以将数据返回到应用中的 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();

活动完成身份验证流程后,必须调用 setResult() 方法,并传递 RESULT_OK 值,并将 EXTRA_AUTHENTICATION_RESULT 额外信息设置为包含填充数据集的 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);

在需要解锁信用卡数据集的情况下,服务可以显示一个 UI,要求用户提供 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();

活动验证 CVC 后,应调用 setResult() 方法,并传递 RESULT_OK 值,并将 EXTRA_AUTHENTICATION_RESULT 额外信息设置为包含信用卡号和到期日期的 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);

将数据组织到逻辑组中

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

  • 凭据,包括用户名和密码字段。
  • 地址,包括街道、城市、州和邮政编码字段。
  • 付款信息,包括信用卡号、到期日期和验证码字段。

正确分区数据的自动填充服务能够通过不在一个数据集中公开多个分区的数据来更好地保护用户的數據。例如,包含凭据的数据集不需要包含付款信息。将数据组织到分区中允许您的服务公开满足请求所需的最小数量的相关信息。

将数据组织到分区中使服务能够填写包含来自多个分区的视图的活动,同时向客户端应用发送最少数量的相关数据。例如,假设一个活动包含用户名、密码、街道和城市字段的视图,并且自动填充服务拥有以下数据。

分区 字段 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. 在自动填充服务中,使用 hasOngoingSmsRequest(来自 SmsCodeAutofillClient)来确定您正在自动填充的应用程序的包名是否有任何活动请求。只有当此方法返回 false 时,您的自动填充服务才能显示建议提示。
  2. 在自动填充服务中,使用 checkPermissionState(来自 SmsCodeAutofillClient)来检查自动填充服务是否具有自动填充一次性验证码的权限。此权限状态可以是 NONEGRANTEDDENIED。自动填充服务必须在 NONEGRANTED 状态下显示建议提示。
  3. 在自动填充身份验证活动中,使用 SmsRetriever.SEND_PERMISSION 权限注册一个监听 SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTIONBroadcastReceiver,以便在 SMS 代码结果可用时接收它。
  4. SmsCodeAutofillClient 上调用 startSmsCodeRetriever 以开始监听通过短信发送的一次性验证码。如果用户授予您的自动填充服务从 SMS 中检索一次性验证码的权限,则此操作会查找从现在起的 1 到 5 分钟内收到的 SMS 消息。

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

  5. 从意图中接收 SMS 代码结果,然后将 SMS 代码作为自动填充响应返回。

高级自动填充方案

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

应用程序通常将用户数据分成同一个活动中的多个屏幕,尤其是在用于创建新用户帐户的活动中。例如,第一个屏幕要求输入用户名,如果用户名可用,则第二个屏幕要求输入密码。在这些情况下,自动填充服务必须等到用户输入了这两个字段后才能显示自动填充保存 UI。按照以下步骤处理此类情况

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

有关更多信息,请参阅 数据分成多个屏幕时的保存

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

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

自定义自动填充保存 UI

自动填充服务可以自定义自动填充保存 UI,以帮助用户决定是否要让服务保存其数据。服务可以通过简单的文本或自定义视图提供有关保存内容的附加信息。服务还可以更改取消保存请求按钮的外观,并在用户点击该按钮时收到通知。有关更多信息,请参阅 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 指定包含呈现页面 URL 的输入字段的资源 ID。

兼容模式具有以下限制

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

有关兼容模式的更多信息,包括与之相关的限制,请参阅 AutofillService 类参考。