建構自動填入服務

自動填入服務應用程式可將資料插入其他應用程式的檢視畫面,讓使用者更容易填寫表單。另外,自動填入服務也可以從應用程式的檢視畫面擷取使用者資料,並儲存資料供日後使用。自動填入服務通常是由管理使用者資料的應用程式提供,例如密碼管理工具。

Android 在 Android 8.0 (API 級別 26) 以上版本中提供自動填入架構,讓填寫表單變得更加容易。裝置需要具備提供自動填入服務的應用程式,使用者才能運用自動填入功能。

本頁說明如何在應用程式中實作自動填入服務。若要查看說明如何實作服務的程式碼範例,請參閱 AutofillFramework 範例 Java | Kotlin。若要進一步瞭解自動填入服務的運作方式,請參閱 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 屬性,其中包含該服務的相關詳細資料。上述範例中的 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 所示:

自動填入 UI

圖 1. 顯示資料集的自動填入 UI。

自動填入架構會定義用來填寫檢視畫面的工作流程,藉此縮短將 Android 系統繫結至自動填入服務的時間。在每個要求中,Android 系統會透過呼叫 onFillRequest() 方法,將 AssistStructure 物件傳送至服務。自動填入服務會透過先前儲存的使用者資料,確認其是否能滿足要求。如果能滿足要求,服務就會將資料封裝在 Dataset 物件中。服務會呼叫 onSuccess() 方法傳遞 FillResponse 物件,其中包含 Dataset 物件。如果服務沒有滿足要求的資料,就會將 null 傳遞至 onSuccess() 方法。如果處理要求時發生錯誤,服務會改為呼叫 onFailure() 方法。如需工作流程的詳細說明,請參閱基本用法

以下程式碼為 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();

自動填入服務可瀏覽 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 所示。

自動填入儲存 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,要求提供信用卡驗證碼。在透過顯示樣板資料 (例如銀行名稱和信用卡號碼末四碼) 解鎖資料集之前,您可以隱藏資料。以下範例說明如何要求資料集驗證,並在使用者提供信用卡驗證碼之前隱藏資料:

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

活動驗證信用卡驗證碼後,應呼叫 setResult() 方法傳遞 RESULT_OK 值,並將 EXTRA_AUTHENTICATION_RESULT 額外項目設為包含信用卡號碼和到期間的 Dataset 物件。新資料集會取代需要驗證的資料集,且立即填寫檢視畫面。以下程式碼示範在使用者提供信用卡驗證碼後如何回傳資料集:

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

整理邏輯群組中的資料

自動填入服務應將資料整理成邏輯群組,以便隔離來自不同網域的概念。這類邏輯群組在本頁中稱為「分區」。下列清單為分區和欄位的標準範例:

  • 憑證,其中包括使用者名稱和密碼欄位。
  • 地址,其中包括街道、城市、州和郵遞區號欄位。
  • 付款資訊,其中包括信用卡號碼、到期日和驗證碼欄位。

如果自動填入服務可正確建立資料分區,就不會在資料集中公開來自一個以上分區的資料,藉此更有效保護其使用者的資料。例如,包含憑證的資料集不一定要包含付款資訊。將資料整理成分區,可讓服務僅公開滿足要求所需的最低資訊量。

將資料整理成分區,可讓服務填入具備多個分區檢視畫面的活動,同時僅將最低限度的資料量傳送至用戶端應用程式。例如,假設活動包含使用者名稱、密碼、街道和城市的檢視畫面,而且自動填入服務具備下列資料:

分區 欄位 1 欄位 2
憑證 work_username work_password
personal_username personal_password
地址 work_street work_city
personal_street personal_city

此服務可準備資料集,其中包括工作及個人帳戶的憑證分區。使用者選擇一項時,後續的自動填入回應就能根據使用者的第一個選擇,提供公司或個人地址。

服務可透過呼叫 isFocused() 方法,並同時呼叫 AssistStructure 物件,藉此識別產生要求的欄位。如此一來,服務就能利用適當的分區資料來準備 FillResponse

簡訊動態密碼自動填入

您的自動填入服務可協助使用者使用 SMS Retriever API 填寫透過簡訊傳送的一次性代碼。

若要使用這項功能,需要滿足下列需求:

  • 在 Android 9 (API 級別 28) 以上版本上執行自動填入服務
  • 使用者已同意自動填入服務讀取簡訊中的一次性代碼
  • 自動填入功能適用的應用程式尚未使用 SMS Retriever API 讀取一次性代碼

自動填入服務可以使用 SmsCodeAutofillClient,只要從 Google Play 服務 19.0.56 以上版本呼叫 SmsCodeRetriever.getAutofillClient() 即可。

在自動填入服務中使用這個 API 的主要步驟如下:

  1. 在自動填入服務中,使用來自 SmsCodeAutofillClienthasOngoingSmsRequest 判斷是否已有針對自動填入功能適用的應用程式套件名稱的任何有效要求。只有在回傳 false 時,自動填入服務才能顯示建議提示。
  2. 在自動填入服務中,使用來自 SmsCodeAutofillClientcheckPermissionState 檢查自動填入服務是否具備自動填入一次性代碼的權限。這個權限狀態可為 NONEGRANTEDDENIED。自動填入服務應顯示 NONEGRANTED 狀態的建議提示。
  3. 在自動填入驗證活動中,請使用 SmsRetriever.SEND_PERMISSION 權限為 SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION 註冊 BroadcastReceiver 監聽功能,以便在可使用時,接收簡訊代碼結果。
  4. SmsCodeAutofillClient 上呼叫 startSmsCodeRetriever,就能開始監聽透過簡訊傳送的一次性代碼。如果使用者已授權自動填入服務擷取簡訊中的一次性代碼,系統將搜尋過去 1 分到接下來 5 分鐘這段期間內收到的簡訊。

    如果自動填入服務需要要求使用者權限以讀取一次性代碼,startSmsCodeRetriever 回傳的 Task 可能會失敗,並回傳 ResolvableApiException。如果發生此情況,應呼叫 ResolvableApiExceptionstartResolutionForResult() 方法,顯示權限要求的同意聲明對話方塊。

  5. 接收來自意圖的簡訊代碼結果,然後回傳簡訊代碼,做為自動填入回應。

進階自動填入情況

與鍵盤整合
自 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

如果測試中的套件不在清單中,請執行下列指令以新增套件:

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

…其中 pkgX 是應用程式套件。如果應用程式是瀏覽器,請使用 resIdx 為包含轉譯頁面網址的輸入欄位指定資源 ID。

相容性模式有下列限制:

若要進一步瞭解相容性模式,包括與其相關的限制,請參閱 AutofillService 類別參考資料。