Tworzenie usług autouzupełniania

Usługa autouzupełniania to aplikacja, która ułatwia użytkownikom wypełnianie formularzy przez wstawianie danych do widoków innych aplikacji. Usługi autouzupełniania mogą też pobierać dane użytkownika z widoków w aplikacji i przechowywać je do późniejszego wykorzystania. Usługi autouzupełniania są zwykle udostępniane przez aplikacje, które zarządzają danymi użytkownika, np. menedżery haseł.

Android ułatwia wypełnianie formularzy dzięki platformie autouzupełniania dostępnej w Androidzie 8.0 (interfejs API na poziomie 26) i nowszych wersjach. Użytkownicy mogą korzystać z funkcji autouzupełniania tylko wtedy, gdy na urządzeniu jest aplikacja, która udostępnia usługi autouzupełniania.

Na tej stronie dowiesz się, jak zaimplementować w aplikacji usługę autouzupełniania. Jeśli szukasz przykładowego kodu, który pokazuje, jak zaimplementować usługę, zapoznaj się z przykładem AutofillFramework w języku Java lub Kotlin. Więcej informacji o działaniu usług autouzupełniania znajdziesz na stronach referencyjnych klas AutofillService i AutofillManager.

Deklaracje i uprawnienia w pliku manifestu

Aplikacje, które udostępniają usługi autouzupełniania, muszą zawierać deklarację opisującą implementację usługi. Aby określić deklarację, dodaj element <service> w manifeście aplikacji. Element <service> musi zawierać te atrybuty i elementy:

  • Atrybut android:name, który wskazuje podklasę AutofillService w aplikacji implementującej usługę.
  • Atrybut android:permission, który deklaruje uprawnienie BIND_AUTOFILL_SERVICE.
  • Element <intent-filter>, którego obowiązkowy element podrzędny <action> określa działanie android.service.autofill.AutofillService.
  • Opcjonalny element <meta-data>, którego możesz użyć do podania dodatkowych parametrów konfiguracji usługi.

Poniższy przykład pokazuje deklarację usługi autouzupełniania:

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

Element <meta-data> zawiera atrybut android:resource, który wskazuje zasób XML z dodatkowymi informacjami o usłudze. Zasób service_configuration w poprzednim przykładzie określa aktywność, która umożliwia użytkownikom skonfigurowanie usługi. Poniższy przykład pokazuje zasób XML service_configuration:

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

Więcej informacji o zasobach XML znajdziesz w artykule Omówienie zasobów aplikacji.

Prośba o włączenie usługi

Aplikacja jest używana jako usługa autouzupełniania po zadeklarowaniu uprawnienia BIND_AUTOFILL_SERVICE i włączeniu jej przez użytkownika w ustawieniach urządzenia. Aplikacja może sprawdzić, czy jest włączoną usługą, wywołując metodę hasEnabledAutofillServices() klasy AutofillManager.

Jeśli aplikacja nie jest bieżącą usługą autouzupełniania, może poprosić użytkownika o zmianę ustawień autouzupełniania za pomocą intencji ACTION_REQUEST_SET_AUTOFILL_SERVICE. Intencja zwraca wartość RESULT_OK, jeśli użytkownik wybierze usługę autouzupełniania, która pasuje do pakietu elementu wywołującego.

Wypełnianie widoków klienta

Usługa autouzupełniania otrzymuje żądania wypełnienia widoków klienta, gdy użytkownik wchodzi w interakcję z innymi aplikacjami. Jeśli usługa autouzupełniania ma dane użytkownika, które spełniają wymagania żądania, wysyła je w odpowiedzi. System Android wyświetla interfejs autouzupełniania z dostępnymi danymi, jak pokazano na rysunku 1:

Menu sugestii autouzupełniania z dostępnym zbiorem danych
Rysunek 1. Interfejs autouzupełniania wyświetlający zbiór danych.

Platforma autouzupełniania definiuje przepływ pracy, który umożliwia wypełnianie widoków i ma na celu skrócenie czasu, przez który system Android jest powiązany z usługą autouzupełniania. W każdym żądaniu system Android wysyła do usługi obiekt AssistStructure, wywołując metodę onFillRequest().

Usługa autouzupełniania sprawdza, czy może spełnić żądanie, używając wcześniej zapisanych danych użytkownika. Jeśli usługa może spełnić żądanie, pakuje dane w obiekty Dataset. Usługa wywołuje metodę onSuccess(), przekazując obiekt FillResponse, który zawiera obiekty Dataset. Jeśli usługa nie ma danych, które mogłyby spełnić żądanie, przekazuje null do metody onSuccess(). Jeśli podczas przetwarzania żądania wystąpi błąd, usługa wywołuje metodę onFailure(). Szczegółowe omówienie przepływu pracy znajdziesz w opisie na stronie referencyjnej AutofillService.

Poniższy kod pokazuje przykład metody 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;
}

Usługa może mieć więcej niż 1 zbiór danych, który spełnia warunki żądania. W takim przypadku system Android wyświetla w interfejsie autouzupełniania wiele opcji – po jednej dla każdego zbioru danych. Poniższy przykładowy kod pokazuje, jak podać w odpowiedzi kilka zbiorów danych:

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

Usługi autouzupełniania mogą poruszać się po obiektach ViewNode w AssistStructure, aby pobierać dane autouzupełniania wymagane do realizacji żądania. Usługa może pobierać dane autouzupełniania za pomocą metod klasy ViewNode, takich jak getAutofillId().

Usługa musi być w stanie opisać zawartość widoku, aby sprawdzić, czy może spełnić żądanie. Użycie atrybutu autofillHints to pierwszy sposób, w jaki usługa musi opisać zawartość widoku. Jednak aplikacje klienckie muszą wyraźnie podać atrybut w swoich widokach, zanim będzie on dostępny dla usługi.

Jeśli aplikacja kliencka nie udostępnia atrybutu autofillHints, usługa musi użyć własnych metod heurystycznych do opisania zawartości. Usługa może używać metod z innych klas, takich jak getText() lub getHint(), aby uzyskać informacje o zawartości widoku. Więcej informacji znajdziesz w artykule Podawanie wskazówek dotyczących autouzupełniania.

Poniższy przykład pokazuje, jak przejść przez AssistStructure i pobrać dane autouzupełniania z obiektu 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);
    }
}

Zapisywanie danych użytkowników

Usługa autouzupełniania potrzebuje danych użytkownika, aby wypełniać widoki w aplikacjach. Gdy użytkownicy ręcznie wypełniają widok, pojawia się prośba o zapisanie danych w bieżącej usłudze autouzupełniania, jak pokazano na rysunku 2.

Okno systemowe z prośbą o zapisanie danych autouzupełniania w usłudze
Rysunek 2. Interfejs zapisywania autouzupełniania.

Aby zapisać dane, usługa musi wskazać, że jest zainteresowana ich przechowywaniem do wykorzystania w przyszłości. Zanim system Android wyśle żądanie zapisania danych, pojawia się żądanie wypełnienia, w ramach którego usługa może wypełnić widoki. Aby wskazać, że chce zapisać dane, usługa dołącza obiekt SaveInfo do odpowiedzi na żądanie wypełnienia. Obiekt SaveInfo zawiera co najmniej te dane:

  • Typ zapisanych danych użytkownika. Listę dostępnych wartości SAVE_DATA znajdziesz w SaveInfo.
  • Minimalny zestaw widoków, które należy zmienić, aby wywołać żądanie zapisu. Na przykład formularz logowania zwykle wymaga od użytkownika zaktualizowania widoków username i password, aby wywołać żądanie zapisu.

Obiekt SaveInfo jest powiązany z obiektem FillResponse, jak w tym przykładzie kodu:

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

Usługa autouzupełniania może implementować logikę, aby zachować dane użytkownika w metodzie onSaveRequest(), która jest zwykle wywoływana po zakończeniu aktywności klienta lub gdy aplikacja kliencka wywołuje commit(). Poniższy kod pokazuje przykład metody 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();
}

Usługi autouzupełniania muszą szyfrować dane wrażliwe przed ich zapisaniem. Dane użytkownika mogą jednak zawierać etykiety lub dane, które nie są wrażliwe. Na przykład konto użytkownika może zawierać etykietę oznaczającą dane jako konto służbowe lub osobiste. Usługi nie mogą szyfrować etykiet. Jeśli etykiety nie są szyfrowane, usługi mogą ich używać w widokach prowadzącego, gdy użytkownik nie jest uwierzytelniony. Następnie usługi mogą zastąpić etykiety rzeczywistymi danymi po uwierzytelnieniu użytkownika.

Odłóż interfejs zapisywania autouzupełniania

Jeśli od Androida 10 używasz wielu ekranów do implementacji procesu autouzupełniania – na przykład jednego ekranu dla pola nazwy użytkownika i drugiego dla hasła – możesz odłożyć zapisywanie interfejsu autouzupełniania, używając flagi SaveInfo.FLAG_DELAY_SAVE.

Jeśli ta flaga jest ustawiona, interfejs zapisywania autouzupełniania nie jest aktywowany, gdy kontekst autouzupełniania powiązany z odpowiedzią SaveInfo jest zatwierdzony. Zamiast tego możesz użyć osobnej aktywności w ramach tego samego zadania, aby dostarczać przyszłe żądania wypełnienia, a następnie wyświetlać interfejs za pomocą żądania zapisu. Więcej informacji: SaveInfo.FLAG_DELAY_SAVE.

Wymagaj uwierzytelniania użytkowników

Usługi autouzupełniania mogą zapewniać dodatkowy poziom bezpieczeństwa, wymagając od użytkownika uwierzytelnienia, zanim będzie mógł wypełnić widoki. Uwierzytelnianie użytkowników warto wdrożyć w tych przypadkach:

  • Dane użytkownika w aplikacji muszą zostać odblokowane za pomocą hasła głównego lub odcisku palca.
  • Odblokowanie określonego zbioru danych, np. danych karty kredytowej, za pomocą kodu weryfikacyjnego karty (CVC).

W sytuacji, gdy usługa wymaga uwierzytelnienia użytkownika przed odblokowaniem danych, może ona wyświetlić dane standardowe lub etykietę i określić Intent, który obsługuje uwierzytelnianie. Jeśli po zakończeniu procesu uwierzytelniania potrzebujesz dodatkowych danych do przetworzenia żądania, możesz dodać je do intencji. Aktywność uwierzytelniania może następnie zwrócić dane do klasy AutofillService w aplikacji.

Poniższy przykładowy kod pokazuje, jak określić, że żądanie wymaga uwierzytelnienia:

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

Po zakończeniu procesu uwierzytelniania aktywność musi wywołać metodę setResult(), przekazując wartość RESULT_OK, i ustawić dodatkowy element EXTRA_AUTHENTICATION_RESULT na obiekt FillResponse, który zawiera wypełniony zbiór danych. Poniższy kod pokazuje, jak zwrócić wynik po zakończeniu procesu uwierzytelniania:

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

W sytuacji, gdy trzeba odblokować zbiór danych karty kredytowej, usługa może wyświetlić interfejs z prośbą o podanie kodu CVC. Możesz ukryć dane do momentu odblokowania zbioru danych, wyświetlając standardowe dane, takie jak nazwa banku i ostatnie 4 cyfry numeru karty kredytowej. Poniższy przykład pokazuje, jak wymagać uwierzytelnienia w przypadku zbioru danych i ukrywać dane, dopóki użytkownik nie poda kodu 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();

Gdy aktywność zweryfikuje kod CVC, powinna wywołać metodę setResult(), przekazując wartość RESULT_OK, i ustawić dodatkowy parametr EXTRA_AUTHENTICATION_RESULT na obiekt Dataset zawierający numer karty kredytowej i datę ważności. Nowy zbiór danych zastępuje zbiór danych, który wymaga uwierzytelniania, a widoki są natychmiast wypełniane. Poniższy kod pokazuje, jak zwrócić zbiór danych, gdy użytkownik poda kod 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);

Uporządkuj dane w grupy logiczne.

Usługi autouzupełniania muszą porządkować dane w grupy logiczne, które izolują pojęcia z różnych domen. Na tej stronie te grupy logiczne są nazywane partycjami. Poniżej znajdziesz typowe przykłady partycji i pól:

  • Dane logowania, które obejmują pola nazwy użytkownika i hasła.
  • Adres, który obejmuje pola ulicy, miasta, stanu i kodu pocztowego.
  • Dane do płatności, które obejmują numer karty kredytowej, datę ważności i pola kodu weryfikacyjnego.

Usługa autouzupełniania, która prawidłowo partycjonuje dane, może lepiej chronić dane użytkowników, ponieważ nie ujawnia danych z więcej niż 1 partycji w zbiorze danych. Na przykład zbiór danych zawierający dane logowania nie musi zawierać informacji o płatnościach. Porządkowanie danych w partycjach pozwala usłudze ujawniać minimalną ilość istotnych informacji wymaganych do spełnienia żądania.

Porządkowanie danych w partycjach umożliwia usługom wypełnianie aktywności, które mają widoki z wielu partycji, przy jednoczesnym wysyłaniu minimalnej ilości odpowiednich danych do aplikacji klienckiej. Rozważmy na przykład aktywność, która obejmuje widoki nazwy użytkownika, hasła, ulicy i miasta, oraz usługę autouzupełniania, która zawiera te dane:

Partycja Pole 1 Pole 2
Dane logowania work_username work_password
personal_username personal_password
Adres work_street work_city
personal_street personal_city

Usługa może przygotować zbiór danych, który zawiera partycję danych logowania zarówno dla konta służbowego, jak i osobistego. Gdy użytkownik wybierze zbiór danych, kolejna odpowiedź autouzupełniania może zawierać adres służbowy lub prywatny, w zależności od pierwszego wyboru użytkownika.

Usługa może zidentyfikować pole, z którego pochodzi żądanie, wywołując metodę isFocused() podczas przechodzenia przez obiekt AssistStructure. Dzięki temu usługa może przygotować FillResponse z odpowiednimi danymi partycji.

Autouzupełnianie jednorazowych kodów z SMS-ów

Usługa autouzupełniania może pomóc użytkownikowi w wypełnianiu kodów jednorazowych wysyłanych za pomocą interfejsu SMS Retriever API.

Aby korzystać z tej funkcji, musisz spełniać te wymagania:

  • Usługa autouzupełniania działa na Androidzie 9 (poziom interfejsu API 28) lub nowszym.
  • Użytkownik wyraża zgodę na odczytywanie przez usługę autouzupełniania jednorazowych kodów z SMS-ów.
  • Aplikacja, dla której udostępniasz autouzupełnianie, nie używa jeszcze interfejsu SMS Retriever API do odczytywania kodów jednorazowych.

Usługa autouzupełniania może używać SmsCodeAutofillClient, które jest dostępne po wywołaniu SmsCodeRetriever.getAutofillClient() z Usług Google Play w wersji 19.0.56 lub nowszej.

Główne kroki korzystania z tego interfejsu API w usłudze autouzupełniania:

  1. W usłudze autouzupełniania użyj hasOngoingSmsRequest z SmsCodeAutofillClient, aby określić, czy są aktywne żądania dotyczące nazwy pakietu aplikacji, którą autouzupełniasz. Usługa autouzupełniania musi wyświetlać prośbę o sugestię tylko wtedy, gdy zwraca wartość false.
  2. W usłudze autouzupełniania użyj metody checkPermissionState z SmsCodeAutofillClient, aby sprawdzić, czy usługa autouzupełniania ma uprawnienia do autouzupełniania kodów jednorazowych. Stan uprawnień może być NONE, GRANTED lub DENIED. Usługa autouzupełniania musi wyświetlać prośbę o sugestię w stanach NONE i GRANTED.
  3. W ramach aktywności uwierzytelniania autouzupełniania użyj uprawnienia SmsRetriever.SEND_PERMISSION, aby zarejestrować BroadcastReceiver nasłuchujący SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION, aby otrzymać wynik kodu SMS, gdy będzie dostępny.
  4. Aby zacząć nasłuchiwać kodów jednorazowych wysyłanych SMS-em, wywołaj startSmsCodeRetriever na SmsCodeAutofillClient. Jeśli użytkownik przyzna usłudze autouzupełniania uprawnienia do pobierania kodów jednorazowych z SMS-ów, usługa będzie wyszukiwać SMS-y otrzymane w ciągu ostatnich 1–5 minut.

    Jeśli usługa autouzupełniania musi poprosić użytkownika o zgodę użytkownika na odczytywanie kodów jednorazowych, obiekt Task zwracany przez metodę startSmsCodeRetriever może zakończyć się niepowodzeniem i zwrócić błąd ResolvableApiException. W takim przypadku musisz wywołać metodę ResolvableApiException.startResolutionForResult(), aby wyświetlić okno z prośbą o zgodę na przetwarzanie danych osobowych.

  5. Odbierz wynik kodu SMS z intencji, a następnie zwróć kod SMS jako odpowiedź autouzupełniania.

Włączanie autouzupełniania w Chrome

Chrome umożliwia usługom autouzupełniania innych firm natywne autouzupełnianie formularzy, co zapewnia użytkownikom płynniejsze i prostsze działanie. Aby korzystać z usług autouzupełniania innych firm do automatycznego uzupełniania haseł, kluczy dostępu i innych informacji, takich jak adresy i dane karty, użytkownicy muszą wybrać opcję Autouzupełnianie przy użyciu innej usługi w ustawieniach Chrome.

Ustawienia Chrome z włączoną opcją „Autouzupełnianie przy użyciu innej usługi”
Rysunek 3. Ustawienia Chrome z włączonym przełącznikiem „Autouzupełnianie przy użyciu innej usługi”.

Aby zapewnić użytkownikom jak najlepsze działanie autouzupełniania w Twojej usłudze i w Chrome na Androidzie, dostawcy usług autouzupełniania powinni zachęcać użytkowników do określenia preferowanego dostawcy usług w ustawieniach Chrome.

Aby ułatwić użytkownikom włączenie przełącznika, deweloperzy mogą:

  • Sprawdź ustawienia Chrome i dowiedz się, czy użytkownik chce korzystać z usługi autouzupełniania innej firmy.
  • Precyzyjny link do strony ustawień Chrome, na której użytkownicy mogą włączyć usługi autouzupełniania innych firm.

Określ maksymalne wersje Chrome dla trybu zgodności

Chrome przestał obsługiwać tryb zgodności od wersji 137 na rzecz autouzupełniania na Androidzie. Pozostawienie trybu zgodności może powodować problemy ze stabilnością. Określ maksymalną wersję pakietów Chrome, które obsługują tryb zgodności zapewniający stabilność, w następujący sposób.

<autofill-service>
  ...
  <compatibility-package android:name="com.android.chrome" android:maxLongVersionCode="711900039" />
  <compatibility-package android:name="com.chrome.beta" android:maxLongVersionCode="711900039" />
  <compatibility-package android:name="com.chrome.dev" android:maxLongVersionCode="711900039" />
  <compatibility-package android:name="com.chrome.canary" android:maxLongVersionCode="711900039" />
  ...
</autofill-service>

Odczytywanie ustawień Chrome

Każda aplikacja może odczytać, czy Chrome używa trybu autouzupełniania innej firmy, który umożliwia korzystanie z autouzupełniania na Androidzie. Chrome używa ContentProvider Androida do przekazywania tych informacji. Zadeklaruj w pliku manifestu Androida, z których kanałów chcesz odczytywać ustawienia:

<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
<queries>
 <!-- To Query Chrome Beta: -->
 <package android:name="com.chrome.beta" />

 <!-- To Query Chrome Stable: -->
 <package android:name="com.android.chrome" />
</queries>

Następnie użyj ContentResolver Androida, aby poprosić o te informacje, tworząc identyfikator URI treści:

Kotlin

val CHROME_CHANNEL_PACKAGE = "com.android.chrome" // Chrome Stable.
val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"

val uri = Uri.Builder()
    .scheme(ContentResolver.SCHEME_CONTENT)
    .authority(CHROME_CHANNEL_PACKAGE + CONTENT_PROVIDER_NAME)
    .path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
    .build()

val cursor = contentResolver.query(
    uri,
    arrayOf(THIRD_PARTY_MODE_COLUMN), // projection
    null, // selection
    null, // selectionArgs
    null  // sortOrder
)

if (cursor == null) {
  // Terminate now! Chromium versions older than this don't provide this information.
}

cursor?.use { // Use the safe call operator and the use function for auto-closing
    if (it.moveToFirst()) { // Check if the cursor has any rows
        val index = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
        if (index != -1) { // Check if the column exists
          val value = it.getInt(index)
          if (0 == value) {
              // 0 means that the third party mode is turned off. Chrome uses its built-in
              // password manager. This is the default for new users.
          } else {
              // 1 means that the third party mode is turned on. Chrome forwards all
              // autofill requests to Android Autofill. Users have to opt-in for this.
          }
        } else {
          // Handle the case where the column doesn't exist.  Log a warning, perhaps.
          Log.w("Autofill", "Column $THIRD_PARTY_MODE_COLUMN not found in cursor")
        }
    }
} // The cursor is automatically closed here

Java

final String CHROME_CHANNEL_PACKAGE = "com.android.chrome";  // Chrome Stable.
final String CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider";
final String THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state";
final String THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode";

final Uri uri = new Uri.Builder()
                  .scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(CHROME_CHANNEL_PACKAGE + CONTENT_PROVIDER_NAME)
                  .path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
                  .build();

final Cursor cursor = getContentResolver().query(
                  uri,
                  /*projection=*/new String[] {THIRD_PARTY_MODE_COLUMN},
                  /*selection=*/ null,
                  /*selectionArgs=*/ null,
                  /*sortOrder=*/ null);

if (cursor == null) {
  // Terminate now! Chromium versions older than this don't provide this information.
}

cursor.moveToFirst(); // Retrieve the result;

int index = cursor.getColumnIndex(THIRD_PARTY_MODE_COLUMN);

if (0 == cursor.getInt(index)) {
  // 0 means that the third party mode is turned off. Chrome uses its built-in
  // password manager. This is the default for new users.
} else {
  // 1 means that the third party mode is turned on. Chrome forwards all
  // autofill requests to Android Autofill. Users have to opt-in for this.
}

Aby utworzyć precyzyjny link do strony ustawień Chrome, na której użytkownicy mogą włączyć usługi autouzupełniania innych firm, użyj Intent na Androidzie. Skonfiguruj działanie i kategorie, jak pokazano w tym przykładzie:

Kotlin

val autofillSettingsIntent = Intent(Intent.ACTION_APPLICATION_PREFERENCES)
autofillSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT)
autofillSettingsIntent.addCategory(Intent.CATEGORY_APP_BROWSER)
autofillSettingsIntent.addCategory(Intent.CATEGORY_PREFERENCE)

// Invoking the intent with a chooser allows users to select the channel they
// want to configure. If only one browser reacts to the intent, the chooser is
// skipped.
val chooser = Intent.createChooser(autofillSettingsIntent, "Pick Chrome Channel")
startActivity(chooser)

// If the caller knows which Chrome channel they want to configure,
// they can instead add a package hint to the intent, e.g.
val specificChromeIntent = Intent(Intent.ACTION_APPLICATION_PREFERENCES) // Create a *new* intent
specificChromeIntent.addCategory(Intent.CATEGORY_DEFAULT)
specificChromeIntent.addCategory(Intent.CATEGORY_APP_BROWSER)
specificChromeIntent.addCategory(Intent.CATEGORY_PREFERENCE)
specificChromeIntent.setPackage("com.android.chrome") // Set the package on the *new* intent
startActivity(specificChromeIntent) // Start the *new* intent

Java

Intent autofillSettingsIntent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES);
autofillSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT);
autofillSettingsIntent.addCategory(Intent.CATEGORY_APP_BROWSER);
autofillSettingsIntent.addCategory(Intent.CATEGORY_PREFERENCE);

// Invoking the intent with a chooser allows users to select the channel they
// want to configure. If only one browser reacts to the intent, the chooser is
// skipped.
Intent chooser = Intent.createChooser(autofillSettingsIntent, "Pick Chrome Channel");
startActivity(chooser);

// If the caller knows which Chrome channel they want to configure,
// they can instead add a package hint to the intent, e.g.
autofillSettingsIntent.setPackage("com.android.chrome");
startActivity(autofillSettingsIntent);

Zaawansowane scenariusze autouzupełniania

Używaj autouzupełniania w tych sytuacjach:

Integracja z klawiaturą

Od Androida 11 platforma umożliwia klawiaturom i innym edytorom metod wprowadzania (IME) wyświetlanie sugestii autouzupełniania w tekście zamiast w menu rozwijanym. Więcej informacji o tym, jak usługa autouzupełniania może obsługiwać tę funkcję, znajdziesz w artykule Integracja autouzupełniania z klawiaturami.

Podział zbiorów danych na strony

Duża odpowiedź autouzupełniania może przekroczyć dozwolony rozmiar transakcji obiektu Binder, który reprezentuje obiekt zdalny wymagany do przetworzenia żądania. Aby zapobiec zgłaszaniu wyjątku przez system Android w takich sytuacjach, możesz ograniczyć rozmiar FillResponse, dodając nie więcej niż 20 obiektów Dataset naraz. Jeśli odpowiedź wymaga więcej zbiorów danych, możesz dodać zbiór danych, który informuje użytkowników, że dostępnych jest więcej informacji, i po wybraniu pobiera następną grupę zbiorów danych. Więcej informacji: addDataset(Dataset).

Zapisywanie danych podzielonych na kilka ekranów

Aplikacje często dzielą dane użytkownika na kilka ekranów w ramach tej samej aktywności, np. podczas tworzenia nowych kont użytkowników. Na przykład na pierwszym ekranie może pojawić się prośba o nazwę użytkownika, a na drugim – o hasło. W takich sytuacjach usługa autouzupełniania musi poczekać, aż użytkownik wprowadzi dane we wszystkich odpowiednich polach, zanim wyświetli interfejs zapisywania autouzupełniania. Aby poradzić sobie z takimi sytuacjami, wykonaj te czynności:

  1. W pierwszym żądaniu wypełnienia dodaj w odpowiedzi pakiet stanu klienta zawierający identyfikatory autouzupełniania częściowych pól obecnych na ekranie.
  2. W drugim żądaniu wypełnienia pobierz pakiet stanu klienta, pobierz identyfikatory autouzupełniania ustawione w poprzednim żądaniu ze stanu klienta i dodaj te identyfikatory oraz flagę FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE do obiektu SaveInfo używanego w drugiej odpowiedzi.
  3. W żądaniu zapisu użyj odpowiednich obiektów FillContext, aby uzyskać wartość każdego pola. Na każde żądanie wypełnienia przypada jeden kontekst wypełnienia.

    Więcej informacji znajdziesz w artykule Zapisywanie danych podzielonych na kilka ekranów.

Zapewnij logikę inicjowania i likwidowania dla każdego żądania.

Za każdym razem, gdy pojawia się żądanie autouzupełniania, system Android wiąże się z usługą i wywołuje jej metodę onConnected(). Gdy usługa przetworzy żądanie, system Android wywołuje metodę onDisconnected() i odłącza się od usługi. Możesz zaimplementować onConnected(), aby udostępnić kod, który jest uruchamiany przed przetworzeniem żądania, oraz onDisconnected(), aby udostępnić kod, który jest uruchamiany po przetworzeniu żądania.

Dostosowywanie interfejsu zapisywania autouzupełniania

Usługi autouzupełniania mogą dostosowywać interfejs zapisywania autouzupełniania, aby pomóc użytkownikom zdecydować, czy chcą zezwolić usłudze na zapisywanie ich danych. Usługi mogą dostarczać dodatkowe informacje o tym, co jest zapisywane, za pomocą tekstu lub dostosowanego widoku. Usługi mogą też zmieniać wygląd przycisku, który anuluje prośbę o zapisanie, i otrzymywać powiadomienie, gdy użytkownik kliknie ten przycisk. Więcej informacji znajdziesz na stronie SaveInfo.

Tryb zgodności

Tryb zgodności umożliwia usługom autouzupełniania korzystanie z wirtualnej struktury ułatwień dostępu na potrzeby autouzupełniania. Jest to szczególnie przydatne w przypadku funkcji autouzupełniania w przeglądarkach, które nie implementują bezpośrednio interfejsów API autouzupełniania.

Aby przetestować usługę autouzupełniania w trybie zgodności, dodaj przeglądarkę lub aplikację do listy dozwolonych, która wymaga trybu zgodności. Aby sprawdzić, które pakiety są już na liście dozwolonych, uruchom to polecenie:

$ adb shell settings get global autofill_compat_mode_allowed_packages

Jeśli testowanego pakietu nie ma na liście, dodaj go, uruchamiając to polecenie, gdzie pkgX to pakiet aplikacji:

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

Jeśli aplikacja jest przeglądarką, użyj resIdx, aby określić identyfikator zasobu pola do wprowadzania danych, które zawiera adres URL renderowanej strony.

Tryb zgodności ma te ograniczenia:

  • Żądanie zapisu jest wywoływane, gdy usługa używa flagi FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE lub wywoływana jest metoda setTrigger(). Wartość FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE jest ustawiana domyślnie, gdy używasz trybu zgodności.
  • Wartość tekstowa węzłów może być niedostępna w metodzie onSaveRequest(SaveRequest, SaveCallback).

Więcej informacji o trybie zgodności, w tym o związanych z nim ograniczeniach, znajdziesz w dokumentacji klasy AutofillService.