Tạo dịch vụ tự động điền

Dịch vụ tự động điền là ứng dụng giúp người dùng điền vào các biểu mẫu dễ dàng hơn bằng cách chèn dữ liệu vào thành phần hiển thị của ứng dụng khác. Dịch vụ tự động điền cũng có thể truy xuất dữ liệu người dùng trên thành phần hiển thị trong ứng dụng và lưu trữ để sử dụng sau này. Dịch vụ tự động điền thường do các ứng dụng quản lý dữ liệu người dùng (chẳng hạn như trình quản lý mật khẩu) cung cấp.

Android giúp việc điền biểu mẫu trở nên dễ dàng hơn nhờ khung tự động điền có trong Android 8.0 (API cấp 26) trở lên. Người dùng chỉ có thể tận dụng các tính năng tự động điền nếu có ứng dụng cung cấp dịch vụ tự động điền trên thiết bị.

Trang này cho biết cách triển khai dịch vụ tự động điền trong ứng dụng. Nếu bạn đang tìm mã mẫu cho thấy cách triển khai một dịch vụ, hãy xem mẫu AutofillFramework trong Java hoặc Kotlin. Để biết thêm thông tin về cách thức hoạt động của dịch vụ tự động điền, hãy xem các trang tham khảo dành cho các lớp AutofillServiceAutofillManager.

Quyền và nội dung khai báo trong tệp kê khai

Ứng dụng cung cấp dịch vụ tự động điền phải đưa ra nội dung khai báo mô tả cách triển khai dịch vụ. Để chỉ định nội dung khai báo, hãy đưa phần tử <service> vào tệp kê khai ứng dụng. Phần tử <service> phải bao gồm các thuộc tính và phần tử sau đây:

  • Thuộc tính android:name trỏ đến lớp con của AutofillService trong ứng dụng triển khai dịch vụ.
  • Thuộc tính android:permission khai báo quyền BIND_AUTOFILL_SERVICE.
  • Phần tử <intent-filter> có phần tử con <action> bắt buộc chỉ định thao tác android.service.autofill.AutofillService.
  • Phần tử <meta-data> (không bắt buộc) mà bạn có thể sử dụng để cung cấp thêm tham số cấu hình cho dịch vụ.

Ví dụ sau đây cho thấy một đoạn mã khai báo dịch vụ tự động điền:

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

Phần tử <meta-data> bao gồm cả một thuộc tính android:resource trỏ đến một tài nguyên XML có thông tin chi tiết khác về dịch vụ. Tài nguyên service_configuration trong ví dụ trước chỉ định một hoạt động cho phép người dùng định cấu hình dịch vụ. Ví dụ sau đây cho thấy tài nguyên XML service_configuration:

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

Để biết thêm thông tin về tài nguyên XML, hãy xem phần Tổng quan về tài nguyên ứng dụng.

Lời nhắc bật dịch vụ

Một ứng dụng được dùng làm dịch vụ tự động điền sau khi khai báo quyền BIND_AUTOFILL_SERVICE và người dùng bật ứng dụng đó trong phần cài đặt thiết bị. Một ứng dụng có thể xác minh xem đó có phải là dịch vụ đang bật hay không bằng cách gọi phương thức hasEnabledAutofillServices() của lớp AutofillManager.

Nếu không phải là dịch vụ tự động điền hiện tại thì ứng dụng có thể yêu cầu người dùng thay đổi chế độ cài đặt tự động điền bằng cách sử dụng ý định ACTION_REQUEST_SET_AUTOFILL_SERVICE. Ý định sẽ trả về giá trị RESULT_OK nếu người dùng chọn dịch vụ tự động điền khớp với gói của phương thức gọi.

Điền vào thành phần hiển thị ứng dụng khách

Dịch vụ tự động điền nhận các yêu cầu điền vào thành phần hiển thị ứng dụng khi người dùng tương tác với ứng dụng khác. Nếu có dữ liệu người dùng đáp ứng yêu cầu thì dịch vụ tự động điền sẽ gửi dữ liệu đó trong phản hồi. Hệ thống Android cho thấy một giao diện người dùng tự động điền kèm theo dữ liệu có sẵn, như trong hình 1:

Giao diện người dùng tự động điền

Hình 1. Giao diện người dùng tự động điền cho thấy một tập dữ liệu.

Khung tự động điền xác định một quy trình để điền thông tin vào thành phần hiển thị nhằm giảm thiểu thời gian hệ thống Android liên kết với dịch vụ tự động điền. Trong mỗi yêu cầu, hệ thống Android gửi một đối tượng AssistStructure đến dịch vụ bằng cách gọi phương thức onFillRequest().

Dịch vụ tự động điền sẽ kiểm tra xem có thể đáp ứng yêu cầu bằng dữ liệu người dùng mà dịch vụ này từng lưu trữ hay không. Nếu có thể đáp ứng yêu cầu thì dịch vụ sẽ gói dữ liệu trong các đối tượng Dataset. Dịch vụ gọi phương thức onSuccess(), truyền đối tượng FillResponse có chứa đối tượng Dataset. Nếu không có dữ liệu để đáp ứng yêu cầu thì dịch vụ sẽ truyền null đến phương thức onSuccess().

Nếu có lỗi khi xử lý yêu cầu thì dịch vụ sẽ gọi phương thức onFailure(). Để nắm được nội dung giải thích chi tiết về quy trình này, hãy xem phần mô tả trên trang tham khảo về AutofillService.

Đoạn mã sau đây cho thấy ví dụ về phương thức 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;
}

Một dịch vụ có thể có nhiều tập dữ liệu đáp ứng yêu cầu. Trong trường hợp này, hệ thống Android sẽ hiển thị nhiều lựa chọn (mỗi tập dữ liệu là một lựa chọn) trong giao diện người dùng tự động điền. Đoạn mã ví dụ sau đây cho thấy cách cung cấp nhiều tập dữ liệu trong một phản hồi:

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

Dịch vụ tự động điền có thể di chuyển các đối tượng ViewNode trong AssistStructure nhằm truy xuất dữ liệu tự động điền cần thiết để thực hiện yêu cầu. Dịch vụ có thể truy xuất dữ liệu tự động điền bằng các phương thức của lớp ViewNode, chẳng hạn như getAutofillId().

Dịch vụ phải mô tả được nội dung của thành phần hiển thị (view) để kiểm tra xem nội dung đó có đáp ứng được yêu cầu hay không. Thuộc tính autofillHints là phương pháp đầu tiên mà dịch vụ nên sử dụng để mô tả nội dung của thành phần hiển thị. Tuy nhiên, ứng dụng khách phải cung cấp rõ ràng thuộc tính đó trong thành phần hiển thị trước khi thuộc tính đó được sử dụng trong dịch vụ.

Nếu ứng dụng khách không cung cấp thuộc tính autofillHints thì dịch vụ phải sử dụng thông tin tự phỏng đoán để mô tả nội dung. Dịch vụ có thể sử dụng phương thức của các lớp khác, chẳng hạn như getText() hoặc getHint(), để nhận thông tin về nội dung của thành phần hiển thị. Để biết thêm thông tin, hãy xem phần Đưa ra gợi ý để tự động điền.

Ví dụ sau đây cho thấy cách truyền tải AssistStructure và truy xuất dữ liệu tự động điền qua đối tượng 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);
    }
}

Lưu dữ liệu người dùng

Dịch vụ tự động điền cần dữ liệu người dùng để điền vào thành phần hiển thị trong ứng dụng. Khi người dùng điền vào thành phần hiển thị theo cách thủ công, họ sẽ được nhắc lưu dữ liệu vào dịch vụ tự động điền hiện tại, như minh hoạ trong hình 2.

Giao diện người dùng lưu thông tin tự động điền

Hình 2. Giao diện người dùng lưu thông tin tự động điền.

Để lưu dữ liệu, dịch vụ phải cho biết ý định lưu trữ dữ liệu để sử dụng sau này. Trước khi gửi yêu cầu lưu dữ liệu, hệ thống Android sẽ yêu cầu dịch vụ điền thông tin vào các thành phần hiển thị. Để cho biết ý định lưu dữ liệu, dịch vụ đưa vào một đối tượng SaveInfo trong phản hồi cho yêu cầu điền. Đối tượng SaveInfo ít nhất phải chứa dữ liệu sau:

  • Loại dữ liệu người dùng được lưu. Để biết danh sách giá trị SAVE_DATA hiện có, hãy xem SaveInfo.
  • Tập hợp thành phần hiển thị tối thiểu cần được thay đổi để kích hoạt yêu cầu lưu. Ví dụ: biểu mẫu đăng nhập thường yêu cầu người dùng cập nhật các thành phần hiển thị usernamepassword để kích hoạt yêu cầu lưu.

Đối tượng SaveInfo được liên kết với đối tượng FillResponse, như trong mã ví dụ sau đây:

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

Dịch vụ tự động điền có thể triển khai logic để duy trì dữ liệu người dùng trong phương thức onSaveRequest(). Phương thức này thường được gọi sau khi hoạt động ứng dụng kết thúc hoặc khi ứng dụng khách gọi commit(). Đoạn mã sau đây cho thấy ví dụ về phương thức 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();
}

Dịch vụ tự động điền phải mã hoá dữ liệu nhạy cảm trước khi duy trì sử dụng. Tuy nhiên, dữ liệu người dùng có thể bao gồm cả nhãn hoặc dữ liệu không nhạy cảm. Ví dụ: tài khoản người dùng có thể bao gồm cả nhãn đánh dấu dữ liệu là tài khoản công việc hoặc tài khoản cá nhân. Dịch vụ không được mã hoá nhãn. Bằng cách không mã hoá nhãn, dịch vụ có thể sử dụng nhãn trong thành phần hiển thị bản trình bày nếu người dùng chưa xác thực. Tiếp đó, các dịch vụ có thể thay thế nhãn bằng dữ liệu thực tế sau khi người dùng xác thực.

Trì hoãn giao diện người dùng lưu thông tin tự động điền

Kể từ Android 10, nếu sử dụng nhiều màn hình để triển khai một quy trình tự động điền (ví dụ: một màn hình cho trường tên người dùng và một màn hình khác cho mật khẩu), thì bạn có thể trì hoãn giao diện người dùng lưu thông tin tự động điền bằng cách sử dụng cờ SaveInfo.FLAG_DELAY_SAVE.

Nếu bạn thiết lập cờ này thì giao diện người dùng lưu thông tin tự động điền sẽ không được kích hoạt khi ngữ cảnh tự động điền liên kết với phản hồi SaveInfo được cam kết. Thay vào đó, bạn có thể sử dụng một hoạt động riêng biệt trong cùng một nhiệm vụ nhằm thực hiện các yêu cầu điền sau này, rồi hiển thị giao diện người dùng lưu thông tin tự động điền qua yêu cầu lưu. Để biết thêm thông tin, hãy xem SaveInfo.FLAG_DELAY_SAVE.

Yêu cầu xác thực người dùng

Dịch vụ tự động điền có thể có thêm một lớp bảo mật bằng cách yêu cầu người dùng xác thực trước khi có thể điền vào các thành phần hiển thị. Sau đây là một số tình huống nên triển khai phương thức xác thực người dùng:

  • Dữ liệu người dùng trong ứng dụng cần được mở khoá bằng mật khẩu chính hoặc quét vân tay.
  • Một tập dữ liệu cụ thể cần được mở khoá (chẳng hạn như thông tin thẻ tín dụng) bằng cách sử dụng mã xác minh thẻ (CVC).

Trong trường hợp dịch vụ yêu cầu xác thực người dùng trước khi mở khoá dữ liệu, dịch vụ có thể trình bày dữ liệu nguyên mẫu (boilerplate) hoặc nhãn và chỉ định Intent để xử lý hoạt động xác thực. Nếu cần thêm dữ liệu để xử lý yêu cầu sau khi quy trình xác thực hoàn tất thì bạn có thể thêm dữ liệu như vậy vào ý định. Sau đó, hoạt động xác thực của bạn có thể trả về dữ liệu cho lớp AutofillService trong ứng dụng.

Ví dụ về mã sau đây cho thấy cách chỉ định rằng yêu cầu cần được xác thực:

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

Khi hoàn tất quy trình xác thực, hoạt động phải gọi phương thức setResult(), truyền một giá trị RESULT_OK và đặt EXTRA_AUTHENTICATION_RESULT bổ sung vào đối tượng FillResponse chứa tập dữ liệu điền sẵn. Mã sau đây là ví dụ về cách trả về kết quả sau khi quy trình xác thực hoàn tất:

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

Trong trường hợp cần mở khoá một tập dữ liệu thẻ tín dụng, dịch vụ có thể cho thấy giao diện người dùng yêu cầu cấp thông tin CVC. Bạn có thể ẩn dữ liệu cho đến khi tập dữ liệu được mở khoá bằng cách cho thấy dữ liệu tạo sẵn, chẳng hạn như tên ngân hàng và bốn số cuối của số thẻ tín dụng. Ví dụ sau đây cho thấy cách yêu cầu xác thực một tập dữ liệu và ẩn dữ liệu cho đến khi người dùng cung cấp 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();

Sau khi xác thực CVC, hoạt động sẽ gọi phương thức setResult() truyền một giá trị RESULT_OK và đặt EXTRA_AUTHENTICATION_RESULT bổ sung vào một đối tượng Dataset chứa số thẻ tín dụng và ngày hết hạn. Tập dữ liệu mới thay thế tập dữ liệu yêu cầu xác thực, đồng thời các thành phần hiển thị được điền vào ngay lập tức. Mã sau đây cho thấy ví dụ về cách trả về tập dữ liệu sau khi người dùng cung cấp 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);

Sắp xếp dữ liệu theo nhóm logic

Dịch vụ tự động điền phải sắp xếp dữ liệu theo nhóm logic để tách biệt các khái niệm với nhiều miền. Trên trang này, các nhóm logic như vậy được gọi là phân vùng. Danh sách sau đây cho thấy các ví dụ điển hình về phân vùng và trường:

  • Thông tin xác thực, bao gồm cả trường tên người dùng và mật khẩu.
  • Địa chỉ, bao gồm cả trường đường, thành phố, tiểu bang và mã bưu chính.
  • Thông tin thanh toán, bao gồm cả trường số thẻ tín dụng, ngày hết hạn và mã xác minh.

Dịch vụ tự động điền phân vùng dữ liệu chính xác có thể tăng khả năng bảo vệ dữ liệu của người dùng bằng cách không để lộ dữ liệu trên nhiều phân vùng trong một tập dữ liệu. Ví dụ: tập dữ liệu chứa thông tin xác thực không nhất thiết phải chứa thông tin thanh toán. Việc sắp xếp dữ liệu trong các phân vùng cho phép dịch vụ đưa ra lượng thông tin tối thiểu có liên quan và cần thiết để đáp ứng yêu cầu.

Việc sắp xếp dữ liệu trong các phân vùng cho phép dịch vụ điền dữ liệu vào các hoạt động có thành phần hiển thị trên nhiều phân vùng trong khi gửi lượng dữ liệu tối thiểu có liên quan đến ứng dụng khách. Ví dụ: hãy xem xét một hoạt động bao gồm các thành phần hiển thị cho tên người dùng, mật khẩu, đường phố và thành phố, cũng như một dịch vụ tự động điền có những dữ liệu sau:

Phân vùng Trường 1 Trường 2
Thông tin xác thực work_username work_password
personal_username personal_password
Địa chỉ work_street work_city
personal_street personal_city

Dịch vụ có thể chuẩn bị một tập dữ liệu bao gồm cả phân vùng thông tin xác thực cho cả tài khoản công việc và tài khoản cá nhân. Khi người dùng chọn một tập dữ liệu, phản hồi tự động điền tiếp theo có thể cung cấp địa chỉ cơ quan hoặc cá nhân, tuỳ thuộc vào lựa chọn đầu tiên của người dùng.

Dịch vụ có thể xác định trường đã đưa ra yêu cầu bằng cách gọi phương thức isFocused() trong khi truyền tải đối tượng AssistStructure. Điều này cho phép dịch vụ chuẩn bị một FillResponse có dữ liệu phân vùng thích hợp.

Tự động điền mã một lần qua SMS

Dịch vụ tự động điền của bạn có thể hỗ trợ người dùng điền mã một lần gửi qua SMS bằng cách sử dụng SMS Retriever API.

Để sử dụng tính năng này, bạn phải đáp ứng các yêu cầu sau đây:

  • Dịch vụ tự động điền đang chạy trên Android 9 (API cấp 28) trở lên
  • Người dùng đồng ý cho phép dịch vụ tự động điền của bạn đọc mã một lần qua SMS.
  • Ứng dụng mà bạn cung cấp dịch vụ tự động điền chưa sử dụng SMS Retriever API để đọc mã một lần.

Dịch vụ tự động điền có thể sử dụng SmsCodeAutofillClient hiện có bằng cách gọi SmsCodeRetriever.getAutofillClient() trên Dịch vụ Google Play 19.0.56 trở lên.

Sau đây là các bước chính để sử dụng API này trong dịch vụ tự động điền:

  1. Trong dịch vụ tự động điền, hãy sử dụng hasOngoingSmsRequest trên SmsCodeAutofillClient để xác định xem có yêu cầu nào đang áp dụng cho tên gói của ứng dụng mà bạn tự động điền hay không. Dịch vụ tự động điền chỉ đưa ra lời nhắc đề xuất nếu giá trị trả về là false.
  2. Trong dịch vụ tự động điền, hãy sử dụng checkPermissionState trên SmsCodeAutofillClient để kiểm tra xem dịch vụ tự động điền có quyền tự động điền mã một lần hay không. Trạng thái của quyền này có thể là NONE, GRANTED hoặc DENIED. Dịch vụ Tự động điền phải cho thấy lời nhắc đề xuất đối với các trạng thái NONEGRANTED.
  3. Trong hoạt động xác thực tự động điền, hãy sử dụng quyền SmsRetriever.SEND_PERMISSION để đăng ký tính năng nghe BroadcastReceiver cho SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION và nhận kết quả mã SMS khi có.
  4. Gọi startSmsCodeRetriever trên SmsCodeAutofillClient để bắt đầu nghe mã một lần được gửi qua SMS. Nếu người dùng cấp quyền cho dịch vụ tự động điền của bạn để truy xuất mã một lần qua SMS, thì hệ thống sẽ tìm các tin nhắn SMS nhận được trong khoảng thời gian từ 1 đến 5 phút vừa xong.

    Nếu dịch vụ tự động điền của bạn cần yêu cầu người dùng cho phép để đọc mã một lần, thì Task do startSmsCodeRetriever trả về có thể không thành công và kèm theo một ResolvableApiException được trả về. Nếu tình trạng này xảy ra thì bạn phải gọi phương thức ResolvableApiException.startResolutionForResult() để cho thấy hộp thoại đồng ý đối với yêu cầu cấp quyền.

  5. Nhận kết quả mã SMS của ý định rồi trả về mã SMS để dùng làm phản hồi tự động điền.

Các trường hợp tự động điền nâng cao

Tích hợp với bàn phím
Kể từ Android 11, nền tảng này cho phép bàn phím và các trình chỉnh sửa phương thức nhập khác (IME) đưa ra đề xuất tự động điền cùng dòng thay vì sử dụng trình đơn kéo xuống. Để biết thêm thông tin về cách dịch vụ tự động điền có thể hỗ trợ chức năng này, hãy xem phần Tích hợp tính năng tự động điền cho bàn phím.
Phân trang tập dữ liệu
Một phản hồi lớn trong phần tự động điền có thể vượt quá kích thước giao dịch được phép của đối tượng Binder đại diện cho đối tượng có thể điều khiển từ xa cần thiết để xử lý yêu cầu. Để ngăn hệ thống Android loại bỏ một trường hợp ngoại lệ trong những trường hợp như vậy, bạn có thể giữ kích thước FillResponse bằng cách thêm tối đa 20 đối tượng Dataset cùng lúc. Nếu câu trả lời của bạn cần thêm tập dữ liệu, thì bạn có thể thêm tập dữ liệu để người dùng biết rằng có thông tin khác và truy xuất nhóm tập dữ liệu tiếp theo khi được chọn. Để biết thêm thông tin, hãy xem addDataset(Dataset).
Lưu dữ liệu được chia tách trên nhiều màn hình

Thường thì ứng dụng sẽ chia tách dữ liệu người dùng thành nhiều màn hình trong cùng một hoạt động, đặc biệt là trong các hoạt động dùng để tạo tài khoản người dùng mới. Ví dụ: màn hình đầu tiên yêu cầu đưa ra tên người dùng và nếu tên người dùng có sẵn thì màn hình thứ hai sẽ yêu cầu đưa ra mật khẩu. Trong những trường hợp như vậy, dịch vụ tự động điền phải đợi cho đến khi người dùng nhập cả hai trường trước khi cho thấy giao diện người dùng lưu thông tin tự động điền. Hãy làm theo các bước sau đây để xử lý những trường hợp như vậy:

  1. Trong yêu cầu điền đầu tiên, hãy thêm gói trạng thái ứng dụng (client state bundle) trong phản hồi có chứa mã nhận dạng tự động điền của các trường xuất hiện một phần trên màn hình.
  2. Trong yêu cầu điền thứ hai, hãy truy xuất gói trạng thái ứng dụng, lấy mã nhận dạng tự động điền đã đặt trong yêu cầu trước đó của trạng thái ứng dụng rồi thêm các mã nhận dạng này để FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE gắn cờ đối tượng SaveInfo dùng trong phản hồi thứ hai.
  3. Trong yêu cầu lưu, hãy sử dụng các đối tượng FillContext phù hợp để nhận giá trị của từng trường. Mỗi yêu cầu điền có một ngữ cảnh điền.

Để biết thêm thông tin, hãy xem phần Lưu khi dữ liệu được chia tách trên nhiều màn hình.

Cung cấp logic khởi động và chia nhỏ cho mỗi yêu cầu

Mỗi khi có yêu cầu tự động điền, hệ thống Android sẽ liên kết với dịch vụ và gọi phương thức onConnected(). Sau khi dịch vụ xử lý yêu cầu này, hệ thống Android sẽ gọi phương thức onDisconnected() và huỷ liên kết với dịch vụ. Bạn có thể triển khai onConnected() để cung cấp mã chạy trước khi xử lý yêu cầu và onDisconnected() để cung cấp mã chạy sau khi xử lý yêu cầu.

Tuỳ chỉnh giao diện người dùng lưu thông tin tự động điền

Dịch vụ tự động điền có thể tuỳ chỉnh giao diện người dùng lưu thông tin tự động điền để giúp người dùng quyết định xem họ có muốn cho phép dịch vụ lưu dữ liệu của họ hay không. Dịch vụ có thể cung cấp thêm thông tin về nội dung được lưu thông qua một văn bản đơn giản hoặc một thành phần hiển thị tuỳ chỉnh. Dịch vụ cũng có thể thay đổi giao diện của nút huỷ yêu cầu lưu và nhận thông báo khi người dùng nhấn vào nút đó. Để biết thêm thông tin, hãy xem trang tham khảo về SaveInfo.

Chế độ tương thích

Chế độ tương thích cho phép dịch vụ tự động điền sử dụng cấu trúc ảo hỗ trợ tiếp cận (accessibility virtual structure) cho mục đích tự động điền. Điều này đặc biệt hữu ích khi cung cấp chức năng tự động điền trong các trình duyệt chưa triển khai rõ ràng API tự động điền.

Để kiểm tra dịch vụ tự động điền bằng chế độ tương thích, hãy thể hiện rõ việc đưa trình duyệt hoặc ứng dụng yêu cầu chế độ tương thích vào danh sách cho phép. Bạn có thể kiểm tra gói nào đã có trong danh sách cho phép bằng cách chạy lệnh sau:

$ adb shell settings get global autofill_compat_mode_allowed_packages

Nếu gói bạn đang kiểm thử không có trong danh sách, hãy thêm gói đó bằng cách chạy lệnh sau đây, trong đó pkgX là gói của ứng dụng:

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

Nếu ứng dụng là trình duyệt, hãy sử dụng resIdx để chỉ định mã nhận dạng tài nguyên của trường nhập dữ liệu chứa URL của trang hiển thị.

Chế độ tương thích có một số hạn chế như sau:

  • Yêu cầu lưu được kích hoạt khi dịch vụ sử dụng cờ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE hoặc phương thức setTrigger() được gọi. FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE được đặt theo mặc định khi sử dụng chế độ tương thích.
  • Giá trị văn bản của các nút có thể không có trong phương thức onSaveRequest(SaveRequest, SaveCallback).

Để biết thêm thông tin về chế độ tương thích, bao gồm cả những giới hạn liên quan đến chế độ này, hãy xem tài liệu tham khảo về lớp AutofillService.