Создание сервисов автозаполнения

Служба автозаполнения — это приложение, которое упрощает пользователям заполнение форм, добавляя данные в представления других приложений. Службы автозаполнения также могут извлекать пользовательские данные из представлений приложения и сохранять их для использования в будущем. Службы автозаполнения обычно предоставляются приложениями, управляющими пользовательскими данными, например, менеджерами паролей.

Android упрощает заполнение форм благодаря фреймворку автозаполнения, доступному в Android 8.0 (API уровня 26) и выше. Пользователи могут воспользоваться функциями автозаполнения только при наличии на их устройстве приложения, предоставляющего такую услугу.

На этой странице показано, как реализовать службу автозаполнения в вашем приложении. Если вы ищете пример кода, демонстрирующий реализацию службы, см. пример AutofillFramework на Java или Kotlin . Подробнее о работе служб автозаполнения см. на справочных страницах по классам AutofillService и AutofillManager .

Манифест деклараций и разрешений

Приложения, предоставляющие услуги автозаполнения, должны включать декларацию, описывающую реализацию службы. Чтобы указать декларацию, включите элемент <service> в манифест приложения . Элемент <service> должен включать следующие атрибуты и элементы:

  • Атрибут android:name , который указывает на подкласс AutofillService в приложении, реализующем службу.
  • Атрибут android:permission , который объявляет разрешение BIND_AUTOFILL_SERVICE .
  • Элемент <intent-filter> , обязательный дочерний элемент <action> которого определяет действие android.service.autofill.AutofillService .
  • Дополнительный элемент <meta-data> , который можно использовать для предоставления дополнительных параметров конфигурации для службы.

В следующем примере показано объявление службы автозаполнения:

<service
    android:name=".MyAutofillService"
    android:label="My Autofill Service"
    android:permission="android.permission.BIND_AUTOFILL_SERVICE">
    <intent-filter>
        <action android:name="android.service.autofill.AutofillService" />
    </intent-filter>
    <meta-data
        android:name="android.autofill"
        android:resource="@xml/service_configuration" />
</service>

Элемент <meta-data> включает атрибут android:resource , который указывает на XML-ресурс с дополнительной информацией о сервисе. Ресурс service_configuration в предыдущем примере определяет действие, позволяющее пользователям настраивать сервис. В следующем примере показан XML-ресурс service_configuration :

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

Дополнительную информацию о ресурсах XML см. в разделе Обзор ресурсов приложения .

Запрос на включение услуги

Приложение используется в качестве службы автозаполнения после того, как оно объявляет разрешение BIND_AUTOFILL_SERVICE , а пользователь включает его в настройках устройства. Приложение может проверить, включена ли эта служба в данный момент, вызвав метод hasEnabledAutofillServices() класса AutofillManager .

Если приложение не является текущей службой автозаполнения, оно может запросить у пользователя изменение настроек автозаполнения, используя намерение ACTION_REQUEST_SET_AUTOFILL_SERVICE . Намерение возвращает значение RESULT_OK , если пользователь выбирает службу автозаполнения, соответствующую пакету вызывающего объекта.

Заполнить клиентские представления

Служба автозаполнения получает запросы на заполнение клиентских представлений при взаимодействии пользователя с другими приложениями. Если у службы автозаполнения есть пользовательские данные, удовлетворяющие запросу, она отправляет их в ответе. Система Android отображает интерфейс автозаполнения с доступными данными, как показано на рисунке 1:

Интерфейс автозаполнения

Рисунок 1. Интерфейс автозаполнения, отображающий набор данных.

Фреймворк автозаполнения определяет рабочий процесс заполнения представлений, призванный минимизировать время, которое система Android использует для работы со службой автозаполнения. В каждом запросе система Android отправляет службе объект AssistStructure , вызывая метод onFillRequest() .

Служба автозаполнения проверяет, может ли она удовлетворить запрос пользовательскими данными, которые она ранее сохранила. Если запрос выполнен, служба упаковывает данные в объекты Dataset . Служба вызывает метод onSuccess() , передавая объект FillResponse , содержащий объекты Dataset . Если у службы нет данных для удовлетворения запроса, она передаёт null методу onSuccess() .

Вместо этого сервис вызывает метод onFailure() , если при обработке запроса возникает ошибка. Подробное описание рабочего процесса см. на странице справки AutofillService .

В следующем коде показан пример метода onFillRequest() :

Котлин

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)

Ява

@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 отображает несколько вариантов — по одному для каждого набора данных — в интерфейсе автозаполнения. В следующем примере кода показано, как предоставить несколько наборов данных в ответе:

Котлин

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

Ява

// Add multiple datasets to the response
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user1Data.username), username1Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user1Data.password), password1Presentation)
                .build())
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(user2Data.username), username2Presentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(user2Data.password), password2Presentation)
                .build())
        .build();

Службы автозаполнения могут перемещаться по объектам ViewNode в AssistStructure для получения данных автозаполнения, необходимых для выполнения запроса. Служба может получать данные автозаполнения, используя методы класса ViewNode , например, getAutofillId() .

Сервис должен иметь возможность описать содержимое представления, чтобы проверить, может ли он удовлетворить запрос. Использование атрибута autofillHints — это первый подход, который сервис должен использовать для описания содержимого представления. Однако клиентские приложения должны явно указать этот атрибут в своих представлениях, прежде чем он станет доступен сервису.

Если клиентское приложение не предоставляет атрибут autofillHints , служба должна использовать собственную эвристику для описания содержимого. Служба может использовать методы других классов, например getText() или getHint() , для получения информации о содержимом представления. Подробнее см. в разделе Предоставление подсказок для автозаполнения .

В следующем примере показано, как обойти AssistStructure и извлечь данные автозаполнения из объекта ViewNode :

Котлин

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

Ява

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.

Автозаполнение сохранить пользовательский интерфейс

Рисунок 2. Интерфейс сохранения автозаполнения.

Чтобы сохранить данные, сервис должен указать, что он заинтересован в их сохранении для будущего использования. Прежде чем система Android отправит запрос на сохранение данных, сервису предоставляется возможность заполнить представления. Чтобы указать, что он заинтересован в сохранении данных, сервис включает объект SaveInfo в ответ на запрос на заполнение. Объект SaveInfo содержит как минимум следующие данные:

  • Тип сохраняемых пользовательских данных. Список доступных значений SAVE_DATA см. в SaveInfo .
  • Минимальный набор представлений, которые необходимо изменить для запуска запроса на сохранение. Например, форма входа обычно требует от пользователя обновить username и password , чтобы запустить запрос на сохранение.

Объект SaveInfo связан с объектом FillResponse , как показано в следующем примере кода:

Котлин

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

Ява

@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() :

Котлин

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

Ява

@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
    // Get the structure from the request
    List<FillContext> context = request.getFillContexts();
    AssistStructure structure = context.get(context.size() - 1).getStructure();

    // Traverse the structure looking for data to save
    traverseStructure(structure);

    // Persist the data - if there are no errors, call onSuccess()
    callback.onSuccess();
}

Сервисы автозаполнения должны шифровать конфиденциальные данные перед их сохранением. Однако пользовательские данные могут включать метки или данные, которые не являются конфиденциальными. Например, учётная запись пользователя может включать метку, которая отмечает данные как рабочие или личные . Сервисы не должны шифровать метки. Не шифруя метки, сервисы могут использовать их в представлениях, если пользователь не прошёл аутентификацию. Затем сервисы могут заменить метки фактическими данными после аутентификации пользователя.

Отложить сохранение автозаполнения пользовательского интерфейса

Начиная с Android 10, если вы используете несколько экранов для реализации рабочего процесса автозаполнения (например, один экран для поля имени пользователя и другой для пароля), вы можете отложить сохранение пользовательского интерфейса автозаполнения с помощью флага SaveInfo.FLAG_DELAY_SAVE .

Если этот флаг установлен, интерфейс сохранения автозаполнения не активируется при фиксации контекста автозаполнения, связанного с ответом SaveInfo . Вместо этого можно использовать отдельное действие в той же задаче для отправки будущих запросов на заполнение, а затем отобразить интерфейс через запрос на сохранение. Подробнее см. SaveInfo.FLAG_DELAY_SAVE .

Требовать аутентификацию пользователя

Службы автозаполнения могут обеспечить дополнительный уровень безопасности, требуя от пользователя аутентификации перед заполнением полей. В следующих сценариях рекомендуется реализовать аутентификацию пользователя:

  • Данные пользователя в приложении необходимо разблокировать с помощью основного пароля или сканирования отпечатка пальца.
  • Необходимо разблокировать определенный набор данных, например данные кредитной карты, используя код проверки карты (CVC).

В сценарии, когда сервис требует аутентификации пользователя перед разблокировкой данных, он может предоставить шаблонные данные или метку и указать Intent , которое отвечает за аутентификацию. Если вам требуются дополнительные данные для обработки запроса после завершения аутентификации, вы можете добавить их в намерение. Действие аутентификации затем может вернуть эти данные классу AutofillService в вашем приложении.

В следующем примере кода показано, как указать, что запрос требует аутентификации:

Котлин

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

Ява

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 , содержащего заполненный набор данных. Следующий код показывает пример возврата результата после завершения процесса аутентификации:

Котлин

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

Ява

Intent intent = getIntent();

// The data sent by the service and the structure are included in the intent
String datasetName = intent.getStringExtra(MY_EXTRA_DATASET_NAME);
AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE);
ParsedStructure parsedStructure = parseStructure(structure);
UserData userData = fetchUserData(parsedStructure);

// Build the presentation of the datasets
RemoteViews usernamePresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
usernamePresentation.setTextViewText(android.R.id.text1, "my_username");
RemoteViews passwordPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
passwordPresentation.setTextViewText(android.R.id.text1, "Password for my_username");

// Add the dataset to the response
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                .setValue(parsedStructure.usernameId,
                        AutofillValue.forText(userData.username), usernamePresentation)
                .setValue(parsedStructure.passwordId,
                        AutofillValue.forText(userData.password), passwordPresentation)
                .build())
        .build();

Intent replyIntent = new Intent();

// Send the data back to the service
replyIntent.putExtra(MY_EXTRA_DATASET_NAME, datasetName);
replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse);

setResult(RESULT_OK, replyIntent);

В случае необходимости разблокировки набора данных кредитной карты сервис может отобразить пользовательский интерфейс с запросом CVC-кода. Вы можете скрыть данные до разблокировки набора данных, указав шаблонные данные, например, название банка и последние четыре цифры номера кредитной карты. В следующем примере показано, как потребовать аутентификацию для набора данных и скрыть данные до тех пор, пока пользователь не предоставит CVC-код:

Котлин

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

Ява

// Parse the structure and fetch payment data
ParsedStructure parsedStructure = parseStructure(structure);
Payment paymentData = fetchPaymentData(parsedStructure);

// Build the presentation that shows the bank and the last four digits of the
// credit card number, such as 'Bank-1234'
String maskedPresentation = paymentData.bank + "-" +
    paymentData.creditCardNumber.subString(paymentData.creditCardNumber.length - 4);
RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
authPresentation.setTextViewText(android.R.id.text1, maskedPresentation);

// Prepare an intent that displays the UI that asks for the CVC
Intent cvcIntent = new Intent(this, CvcActivity.class);
IntentSender cvcIntentSender = PendingIntent.getActivity(
        this,
        1001,
        cvcIntent,
        PendingIntent.FLAG_CANCEL_CURRENT
).getIntentSender();

// Build a FillResponse object that includes a Dataset that requires authentication
FillResponse fillResponse = new FillResponse.Builder()
        .addDataset(new Dataset.Builder()
                // The values in the dataset are replaced by the actual
                // data once the user provides the CVC
                .setValue(parsedStructure.creditCardId, null, authPresentation)
                .setValue(parsedStructure.expDateId, null, authPresentation)
                .setAuthentication(cvcIntentSender)
                .build())
        .build();

После проверки CVC-кода действие должно вызвать метод setResult() , передав значение RESULT_OK , и установить дополнительный параметр EXTRA_AUTHENTICATION_RESULT для объекта Dataset , содержащего номер кредитной карты и срок действия. Новый набор данных заменяет набор данных, требующий аутентификации, и представления заполняются немедленно. В следующем коде показан пример возврата набора данных после ввода CVC-кода пользователем:

Котлин

// 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)
}

Ява

// Parse the structure and fetch payment data.
ParsedStructure parsedStructure = parseStructure(structure);
Payment paymentData = fetchPaymentData(parsedStructure);

// Build a non-null RemoteViews object to use as the presentation when
// creating the Dataset object. This presentation isn't actually used, but the
// Builder object requires a non-null presentation.
RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);

// Create a dataset with the credit card number and expiration date.
Dataset responseDataset = new Dataset.Builder()
        .setValue(parsedStructure.creditCardId,
                AutofillValue.forText(paymentData.creditCardNumber), notUsed)
        .setValue(parsedStructure.expDateId,
                AutofillValue.forText(paymentData.expirationDate), notUsed)
        .build();

Intent replyIntent = new Intent();
replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, responseDataset);

Организуйте данные в логические группы

Службы автозаполнения должны организовывать данные в логические группы, изолирующие понятия из разных предметных областей. На этой странице эти логические группы называются разделами . Ниже приведены типичные примеры разделов и полей:

  • Учетные данные, включающие поля имени пользователя и пароля.
  • Адрес, включающий поля улицы, города, штата и почтового индекса.
  • Платежная информация, включающая номер кредитной карты, срок действия и поля проверочного кода.

Сервис автозаполнения, который правильно разделяет данные, способен лучше защищать данные своих пользователей, не раскрывая данные из нескольких разделов набора данных. Например, набор данных, содержащий учётные данные, не обязан включать платёжную информацию. Организация данных по разделам позволяет вашему сервису предоставлять минимальное количество релевантной информации, необходимое для выполнения запроса.

Организация данных в разделы позволяет сервисам заполнять действия, содержащие представления из нескольких разделов, отправляя при этом минимальное количество релевантных данных в клиентское приложение. Например, рассмотрим действие, включающее представления имени пользователя, пароля, улицы и города, а также сервис автозаполнения со следующими данными:

Раздел Поле 1 Поле 2
Реквизиты для входа work_username рабочий_пароль
персональное_имя_пользователя личный_пароль
Адрес рабочая_улица work_city
персональная_улица личный_город

Сервис может подготовить набор данных, включающий раздел с учётными данными как для рабочих, так и для личных учётных записей. При выборе набора данных последующий ответ автозаполнения может содержать рабочий или личный адрес, в зависимости от первоначального выбора пользователя.

Служба может определить поле, из которого был отправлен запрос, вызвав метод isFocused() при обходе объекта AssistStructure . Это позволяет службе подготовить FillResponse с соответствующими данными раздела.

Автозаполнение одноразового кода SMS

Ваша служба автозаполнения может помочь пользователю заполнять одноразовые коды, отправленные по SMS, с помощью API SMS Retriever.

Для использования этой функции необходимо выполнить следующие требования:

  • Служба автозаполнения работает на базе Android 9 (уровень API 28) или выше.
  • Пользователь дает согласие на то, чтобы ваш сервис автозаполнения считывал одноразовые коды из СМС.
  • Приложение, для которого вы обеспечиваете автозаполнение, еще не использует API SMS Retriever для считывания одноразовых кодов.

Ваша служба автозаполнения может использовать SmsCodeAutofillClient , доступный путем вызова SmsCodeRetriever.getAutofillClient() из Google Play Services 19.0.56 или выше.

Основные шаги по использованию этого API в сервисе автозаполнения:

  1. В службе автозаполнения используйте hasOngoingSmsRequest из SmsCodeAutofillClient , чтобы определить, есть ли активные запросы на имя пакета приложения, которое вы заполняете. Служба автозаполнения должна отображать подсказку только в том случае, если функция возвращает false .
  2. В службе автозаполнения используйте checkPermissionState из SmsCodeAutofillClient , чтобы проверить, есть ли у службы автозаполнения разрешение на автоматическое заполнение одноразовых кодов. Это может быть NONE , GRANTED или DENIED . Служба автозаполнения должна отображать подсказку для состояний NONE и GRANTED .
  3. В действии аутентификации автозаполнения используйте разрешение SmsRetriever.SEND_PERMISSION для регистрации BroadcastReceiver , прослушивающего SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION , чтобы получать результат SMS-кода, когда он доступен.
  4. Вызовите startSmsCodeRetriever в SmsCodeAutofillClient , чтобы начать прослушивание одноразовых кодов, отправленных по SMS. Если пользователь предоставляет вашей службе автозаполнения разрешение на извлечение одноразовых кодов из SMS, будет выполнен поиск SMS-сообщений, полученных в течение последних одной-пяти минут.

    Если ваш сервис автозаполнения должен запрашивать у пользователя разрешение на чтение одноразовых кодов, то Task , возвращаемая startSmsCodeRetriever , может завершиться ошибкой с исключением ResolvableApiException . В этом случае необходимо вызвать метод ResolvableApiException.startResolutionForResult() чтобы отобразить диалоговое окно с запросом согласия.

  5. Получите результат SMS-кода из намерения, а затем верните SMS-код в качестве ответа автозаполнения.

Включить автозаполнение в Chrome

Chrome позволяет сторонним сервисам автозаполнения автоматически заполнять формы, что делает работу пользователей более удобной и простой. Чтобы использовать сторонние сервисы автозаполнения для автоматического заполнения паролей, ключей доступа и другой информации, такой как адреса и платежные данные, необходимо выбрать опцию «Автозаполнение с помощью другого сервиса» в настройках Chrome.

Включить автозаполнение в Chrome

Чтобы обеспечить пользователям наилучший опыт автозаполнения с вашим сервисом и Chrome на Android, поставщики услуг автозаполнения должны рекомендовать своим пользователям указывать предпочитаемого поставщика услуг в настройках Chrome.

Чтобы помочь пользователям включить переключатель, разработчики могут:

  • Запросите настройки Chrome и узнайте, хочет ли пользователь использовать сторонний сервис автозаполнения.
  • Глубокая ссылка на страницу настроек Chrome, где пользователи могут включить сторонние службы автозаполнения.

Прочитать настройки Chrome

Любое приложение может узнать, использует ли Chrome режим автозаполнения 3P, позволяющий использовать Android Autofill. Chrome использует ContentProvider Android для передачи этой информации. Укажите в манифесте Android, из каких каналов вы хотите считывать настройки:

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

Затем используйте ContentResolver Android для запроса этой информации, построив URI контента:

Котлин

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! Older versions of Chromium 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 uses 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

Ява

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! Older versions of Chromium 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 uses forwards all
  // autofill requests to Android Autofill. Users have to opt-in for this.
}

Чтобы создать глубокую ссылку на страницу настроек Chrome, где пользователи могут включить сторонние сервисы автозаполнения, используйте Intent Android. Обязательно настройте действие и категории, как показано в этом примере:

Котлин

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

Ява

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

Расширенные сценарии автозаполнения

Интеграция с клавиатурой
Начиная с Android 11, платформа позволяет клавиатурам и другим редакторам методов ввода ( IME ) отображать предложения автозаполнения в строке, а не в раскрывающемся меню. Подробнее о том, как ваша служба автозаполнения может поддерживать эту функцию, см. в статье Интеграция автозаполнения с клавиатурами .
Разбить наборы данных на страницы
Большой ответ автозаполнения может превысить допустимый размер транзакции объекта Binder , представляющего удалённый объект, необходимый для обработки запроса. Чтобы предотвратить возникновение исключения в таких ситуациях в системе Android, можно поддерживать небольшой размер FillResponse , добавляя не более 20 объектов Dataset за раз. Если вашему ответу требуется больше наборов данных, можно добавить набор данных, который сообщит пользователям о наличии дополнительной информации и извлечёт следующую группу наборов данных при выборе. Подробнее см. addDataset(Dataset) .
Сохранение данных, разделенных на несколько экранов

Приложения часто разделяют пользовательские данные на несколько экранов в рамках одного действия, особенно при создании новой учётной записи. Например, на первом экране запрашивается имя пользователя, и, если имя пользователя доступно, на втором экране запрашивается пароль. В таких ситуациях служба автозаполнения должна дождаться, пока пользователь введёт оба поля, прежде чем будет отображен интерфейс сохранения автозаполнения. Для обработки таких ситуаций выполните следующие действия:

  1. В первом запросе на заполнение добавьте в ответ пакет состояния клиента , содержащий идентификаторы автозаполнения частичных полей, представленных на экране.
  2. Во втором запросе на заполнение извлеките пакет состояния клиента, получите идентификаторы автозаполнения, установленные в предыдущем запросе из состояния клиента, и добавьте эти идентификаторы и флаг FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE к объекту SaveInfo , используемому во втором ответе.
  3. В запросе на сохранение используйте соответствующие объекты FillContext для получения значения каждого поля. На каждый запрос на заполнение приходится один контекст заполнения.

Дополнительную информацию см. в разделе Сохранение при разделении данных на несколько экранов .

Предоставить логику инициализации и демонтажа для каждого запроса

При каждом запросе на автозаполнение система Android привязывается к сервису и вызывает его метод onConnected() . После обработки запроса сервис вызывает метод onDisconnected() и отключается от сервиса. Вы можете реализовать метод onConnected() для предоставления кода, выполняемого до обработки запроса, и метод onDisconnected() для предоставления кода, выполняемого после обработки запроса.

Настройте интерфейс сохранения автозаполнения

Сервисы автозаполнения могут настраивать пользовательский интерфейс сохранения данных, чтобы помочь пользователям решить, сохранять ли данные. Сервисы могут предоставлять дополнительную информацию о сохраняемых данных в виде простого текста или в виде настраиваемого представления. Сервисы также могут изменять внешний вид кнопки отмены запроса на сохранение и получать уведомление при нажатии этой кнопки. Подробнее см. на странице справки SaveInfo .

Режим совместимости

Режим совместимости позволяет службам автозаполнения использовать виртуальную структуру доступности для целей автозаполнения. Он особенно полезен для реализации функции автозаполнения в браузерах, которые явно не реализуют API автозаполнения.

Чтобы протестировать службу автозаполнения в режиме совместимости, явно добавьте браузер или приложение, требующее режима совместимости, в разрешённый список. Вы можете проверить, какие пакеты уже добавлены в разрешённый список, выполнив следующую команду:

$ adb shell settings get global autofill_compat_mode_allowed_packages

Если тестируемый вами пакет отсутствует в списке, добавьте его, выполнив следующую команду, где pkgX — пакет приложения:

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

Если приложение является браузером, то используйте resIdx для указания идентификатора ресурса поля ввода, содержащего URL отображаемой страницы.

Режим совместимости имеет следующие ограничения:

  • Запрос на сохранение инициируется, когда служба использует флаг FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE или вызывается метод setTrigger() . FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE устанавливается по умолчанию при использовании режима совместимости.
  • Текстовое значение узлов может быть недоступно в методе onSaveRequest(SaveRequest, SaveCallback) .

Дополнительную информацию о режиме совместимости, включая связанные с ним ограничения, см. в справочнике по классу AutofillService .