Android NDK开发详解用户数据和身份之构建自动填充服务
自动填充服务是一种通过向其他应用视图中注入数据,从而方便用户填写表单的应用。自动填充服务还可以从应用的视图中检索用户数据,并将其存储起来,以便以后使用。自动填充服务通常由管理用户数据的应用提供,比如密码管理器。
Android 通过在 Android 8.0(API 级别 26)和更高版本中提供自动填充框架,让填写表单更轻松。用户只有在自己的设备拥有提供自动填充服务的应用时,才能利用自动填充功能。
此页面显示了如何在您的应用中实现自动填充服务。如果您正在寻找显示如何实现服务的代码示例,请参阅 Android 自动填充框架示例。如需详细了解自动填充服务的工作原理,请阅读 AutofillService 和 AutofillManager 类的参考文档。
Manifest 声明和权限
应用如果提供自动填充服务,则必须包含一个描述服务实现的声明。指定声明时,请在应用清单中包含 元素。而在此 元素中,应包含以下属性和元素:
android:name 属性,指向实现该服务的应用中的 AutofillService 的子类。
android:permission 属性,声明 BIND_AUTOFILL_SERVICE 权限。
元素,其强制性的 子级指定 android.service.autofill.AutofillService 操作。
(可选) 元素:可用于为服务提供其他配置参数。
下方给出了一个自动填充服务声明示例:
<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>
元素包含一个指向某 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 对象中。服务调用传递 FillResponse 对象的 onSuccess() 方法,其中包含 Dataset 对象。如果服务没有可满足请求的数据,则会将 null 传递给 onSuccess() 方法。如果处理请求时出现错误,服务则会调用 onFailure() 方法。如需了解工作流详情,请参阅基本用法。
注意:从 Android 10 开始,您可使用 FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST 标记确定自动填充请求是否通过兼容模式生成。
以下是一个 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);
}
}
注意:大多数视图提供的 autofillHints 属性都符合 View 类中包含的 AUTOFILL_HINT 值列表。但 HtmlInfo 等视图则更有可能符合 W3C autocomplete attribute 文档中所列的属性。
保存用户数据
自动填充服务需要用户数据来填充应用中的视图。当用户手动填充视图时,系统会提示他们将数据保存至当前的自动填充服务,如图 2 所示。
自动填充保存界面
图 2. 自动填充保存界面
要保存数据,服务必须表明它想保存数据以备将来使用。在 Android 系统发送保存数据的请求之前,有一个填充请求,服务可在此时填充视图。为了表明它想保存数据,服务应包含一个 SaveInfo 对象来响应先前的填充请求。SaveInfo 对象中至少包含以下数据:
要保存的用户数据类型。如需查看可用 SAVE_DATA 值的列表,请参阅 SaveInfo。
需要更改以触发保存请求的最小视图集。例如,登录表单通常要求用户更新 username 和 password 视图以触发保存请求。
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.
// For example '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.
// For example '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 that we can 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 that we can 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,以及一个包含以下数据的自动填充服务:
服务可准备一个数据集,其中包含工作帐号和个人帐号的凭据分区。当用户在二者中选择其一后,随后的自动填充响应可根据用户的首选项提供工作地址或个人地址。
服务可通过在遍历 AssistStructure 对象时调用 isFocused() 方法,来识别发出请求的字段。服务因而可准备一个提供适当分区数据的 FillResponse。
高级自动填充场景
为数据集标页码
一个较大的自动填充响应可能会超过表示处理请求所需的远程对象的 Binder 对象的允许事务大小。为了防止 Android 系统在此类情况下抛出异常,您可将一次性添加的 Dataset 对象控制在 20 个以内,以便使 FillResponse 保持较小大小。如果您的响应需要更多数据集,则您可添加一个数据集,通过它让用户知道还有更多信息,并让用户能够通过主动选择来检索下一组数据集。如需了解详细信息,请参阅 addDataset(Dataset)。
保存分布在多个屏幕中的数据
在同一个 Activity 中,应用经常将用户数据分割到多个屏幕中,尤其是在用于创建新用户帐号的 Activity 中。例如,第一个屏幕要求提供用户名,当用户提供用户名后即转至第二个屏幕,要求提供密码。针对此类情况,自动填充服务必须等待用户输入两个字段后才会显示自动填充保存界面。服务可能会按照以下步骤处理此类场景:
在第一个填充请求中,服务在包含屏幕中显示的部分字段的自动填充 ID 的响应中添加客户端状态软件包。
在第二个填充请求中,服务检索客户端状态软件包,从客户端状态中获取在上一个请求中设置的自动填充 ID,并将这些 ID 以及 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE 标记添加至在第二个响应中使用的 SaveInfo 对象。
在保存请求中,服务使用适当的 FillContext 对象获取每个字段的值。每一个填充请求都有一个填充上下文。
如需了解详细信息,请参阅保存分布在多个屏幕中的数据。
为每个请求提供初始化和拆卸逻辑
每当有自动填充请求时,Android 系统都会绑定到服务,并调用其 onConnected() 方法。当服务处理完请求后,Android 系统就会调用 onDisconnected() 方法并从服务中解绑。您可以通过实现 onConnected() 来提供在处理请求之前运行的代码,通过提供 onDisconnected() 来提供在处理请求之后运行的代码。
自定义自动填充保存界面
自动填充服务可自定义自动填充保存界面,从而帮助用户决定是否允许服务保存他们的数据。服务可通过简单文本或自定义视图提供关于保存内容的附加信息。此外,服务还可更改用于取消保存请求的按钮的外观,并在用户按该按钮时获得通知。如需了解详细信息,请参阅 SaveInfo 参考文档。
兼容模式
兼容模式允许自动填充服务将无障碍虚拟结构用于自动填充目的。当在尚未显式实现自动填充 API 的浏览器中提供自动填充功能时,它尤其有用。
要使用兼容模式测试自动填充服务,您必须将需要兼容模式的浏览器或应用显示列入白名单。您可以通过运行以下命令检查哪些软件包已列入白名单:
$ adb shell settings get global autofill_compat_mode_allowed_packages
如果您要测试的软件包未列出,则可运行以下命令将其列入白名单:
$ adb shell settings put global autofill_compat_mode_allowed_packages pkg1[resId1]:pkg2[resId1,resId2]
…其中 pkgX 指应用的软件包。如果应用是一个浏览器,则使用 resIdx 指定包含所呈现页面 URL 的输入字段的资源 ID。
兼容模式具有以下局限性:
当服务使用 FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE 标记,或调用 setTrigger() 方法时,将触发保存请求。FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE 在使用兼容模式时默认设置。
onSaveRequest(SaveRequest, SaveCallback) 方法中可能不包含节点的文本值。
如需了解有关兼容模式及其局限性的详细信息,请参阅 AutofillService 类引用。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2021-10-27。