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

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

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

Kotlin

override fun onFillRequest(
    request: FillRequest,
    cancellationSignal: CancellationSignal,
    callback: FillCallback
) {
    // Get the structure from the request
    val context: List<FillContext> = request.fillContexts
    val structure: AssistStructure = context[context.size - 1].structure

    // Traverse the structure looking for nodes to fill out
    val parsedStructure: ParsedStructure = parseStructure(structure)

    // Fetch user data that matches the fields
    val (username: String, password: String) = fetchUserData(parsedStructure)

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

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

    // If there are no errors, call onSuccess() and pass the response
    callback.onSuccess(fillResponse)
}

data class ParsedStructure(var usernameId: AutofillId, var passwordId: AutofillId)

data class UserData(var username: String, var password: String)

Java

@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
    // Get the structure from the request
    List<FillContext> context = request.getFillContexts();
    AssistStructure structure = context.get(context.size() - 1).getStructure();

    // Traverse the structure looking for nodes to fill out
    ParsedStructure parsedStructure = parseStructure(structure);

    // Fetch user data that matches the fields
    UserData userData = fetchUserData(parsedStructure);

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

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

    // If there are no errors, call onSuccess() and pass the response
    callback.onSuccess(fillResponse);
}

class ParsedStructure {
    AutofillId usernameId;
    AutofillId passwordId;
}

class UserData {
    String username;
    String password;
}

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

Kotlin

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

Java

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

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

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

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

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

Kotlin

fun traverseStructure(structure: AssistStructure) {
    val windowNodes: List<AssistStructure.WindowNode> =
            structure.run {
                (0 until windowNodeCount).map { getWindowNodeAt(it) }
            }

    windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
        val viewNode: ViewNode? = windowNode.rootViewNode
        traverseNode(viewNode)
    }
}

fun traverseNode(viewNode: ViewNode?) {
    if (viewNode?.autofillHints?.isNotEmpty() == true) {
        // If the client app provides autofill hints, you can obtain them using
        // viewNode.getAutofillHints();
    } else {
        // Or use your own heuristics to describe the contents of a view
        // using methods such as getText() or getHint()
    }

    val children: List<ViewNode>? =
            viewNode?.run {
                (0 until childCount).map { getChildAt(it) }
            }

    children?.forEach { childNode: ViewNode ->
        traverseNode(childNode)
    }
}

Java

public void traverseStructure(AssistStructure structure) {
    int nodes = structure.getWindowNodeCount();

    for (int i = 0; i < nodes; i++) {
        WindowNode windowNode = structure.getWindowNodeAt(i);
        ViewNode viewNode = windowNode.getRootViewNode();
        traverseNode(viewNode);
    }
}

public void traverseNode(ViewNode viewNode) {
    if(viewNode.getAutofillHints() != null && viewNode.getAutofillHints().length > 0) {
        // If the client app provides autofill hints, you can obtain them using
        // viewNode.getAutofillHints();
    } else {
        // Or use your own heuristics to describe the contents of a view
        // using methods such as getText() or getHint()
    }

    for(int i = 0; i < viewNode.getChildCount(); i++) {
        ViewNode childNode = viewNode.getChildAt(i);
        traverseNode(childNode);
    }
}

Сохраните данные пользователей

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

Системное диалоговое окно, в котором пользователю предлагается сохранить данные для автозаполнения в сервисе.
Рисунок 2. интерфейс сохранения данных для автозаполнения.

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

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

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

Kotlin

override fun onFillRequest(
    request: FillRequest,
    cancellationSignal: CancellationSignal,
    callback: FillCallback
) {
    // ...
    // Builder object requires a non-null presentation
    val notUsed = RemoteViews(packageName, android.R.layout.simple_list_item_1)

    val fillResponse: FillResponse = FillResponse.Builder()
            .addDataset(
                    Dataset.Builder()
                            .setValue(parsedStructure.usernameId, null, notUsed)
                            .setValue(parsedStructure.passwordId, null, notUsed)
                            .build()
            )
            .setSaveInfo(
                    SaveInfo.Builder(
                            SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                            arrayOf(parsedStructure.usernameId, parsedStructure.passwordId)
                    ).build()
            )
            .build()
    // ...
}

Java

@Override
public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
    // ...
    // Builder object requires a non-null presentation
    RemoteViews notUsed = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);

    FillResponse fillResponse = new FillResponse.Builder()
            .addDataset(new Dataset.Builder()
                    .setValue(parsedStructure.usernameId, null, notUsed)
                    .setValue(parsedStructure.passwordId, null, notUsed)
                    .build())
            .setSaveInfo(new SaveInfo.Builder(
                    SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
                    new AutofillId[] {parsedStructure.usernameId, parsedStructure.passwordId})
                    .build())
            .build();
    // ...
}

Сервис автозаполнения может реализовать логику сохранения пользовательских данных в методе onSaveRequest(), который обычно вызывается после завершения действия клиента или когда клиентское приложение вызывает commit(). В примере кода ниже показано, как использовать метод onSaveRequest().

Kotlin

override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
    // Get the structure from the request
    val context: List<FillContext> = request.fillContexts
    val structure: AssistStructure = context[context.size - 1].structure

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

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

Java

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

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

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

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

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

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

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

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

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

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

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

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

Kotlin

val authPresentation = RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
    setTextViewText(android.R.id.text1, "requires authentication")
}
val authIntent = Intent(this, AuthActivity::class.java).apply {
    // Send any additional data required to complete the request
    putExtra(MY_EXTRA_DATASET_NAME, "my_dataset")
}

val intentSender: IntentSender = PendingIntent.getActivity(
        this,
        1001,
        authIntent,
        PendingIntent.FLAG_CANCEL_CURRENT
).intentSender

// Build a FillResponse object that requires authentication
val fillResponse: FillResponse = FillResponse.Builder()
        .setAuthentication(autofillIds, intentSender, authPresentation)
        .build()

Java

RemoteViews authPresentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
authPresentation.setTextViewText(android.R.id.text1, "requires authentication");
Intent authIntent = new Intent(this, AuthActivity.class);

// Send any additional data required to complete the request
authIntent.putExtra(MY_EXTRA_DATASET_NAME, "my_dataset");
IntentSender intentSender = PendingIntent.getActivity(
                this,
                1001,
                authIntent,
                PendingIntent.FLAG_CANCEL_CURRENT
        ).getIntentSender();

// Build a FillResponse object that requires authentication
FillResponse fillResponse = new FillResponse.Builder()
        .setAuthentication(autofillIds, intentSender, authPresentation)
        .build();

После завершения процесса аутентификации объект activity должен вызвать метод setResult(), передав значение RESULT_OK, и задать дополнительный параметр EXTRA_AUTHENTICATION_RESULT для объекта FillResponse, который включает заполненный набор данных. В примере ниже показано, как вернуть результат после завершения процесса аутентификации.

Kotlin

// The data sent by the service and the structure are included in the intent
val datasetName: String? = intent.getStringExtra(MY_EXTRA_DATASET_NAME)
val structure: AssistStructure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE)
val parsedStructure: ParsedStructure = parseStructure(structure)
val (username, password) = fetchUserData(parsedStructure)

// Build the presentation of the datasets
val usernamePresentation =
        RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
            setTextViewText(android.R.id.text1, "my_username")
        }
val passwordPresentation =
        RemoteViews(packageName, android.R.layout.simple_list_item_1).apply {
            setTextViewText(android.R.id.text1, "Password for my_username")
        }

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

val replyIntent = Intent().apply {
    // Send the data back to the service
    putExtra(MY_EXTRA_DATASET_NAME, datasetName)
    putExtra(EXTRA_AUTHENTICATION_RESULT, fillResponse)
}

setResult(Activity.RESULT_OK, replyIntent)

Java

Intent intent = getIntent();

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

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

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

Intent replyIntent = new Intent();

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

setResult(RESULT_OK, replyIntent);

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

Kotlin

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

// Build the presentation that shows the bank and the last four digits of the
// credit card number, 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();

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

Kotlin

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

// Build a non-null RemoteViews object 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);

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

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

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

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

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

Раздел Поле 1 Поле 2
Учетные данные work_username work_password
personal_username personal_password
Адрес work_street work_city
personal_street personal_city

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

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

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

Сервис автозаполнения может помогать пользователю вводить одноразовые коды, отправленные с помощью SMS Retriever API.

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

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

Сервис автозаполнения может использовать SmsCodeAutofillClient, доступный при вызове SmsCodeRetriever.getAutofillClient() из сервисов Google Play версии 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, будут проверяться сообщения, полученные за последние 1–5 минут.

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

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

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

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

Настройки Chrome с включенным переключателем &quot;Автозаполнение с помощью другого сервиса&quot;
Рисунок 3. Настройки Chrome, в которых включен переключатель "Автозаполнение с помощью другого сервиса".

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

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

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

Укажите максимальную версию Chrome для режима совместимости

Начиная с версии 137, Chrome больше не поддерживает режим совместимости, а использует автозаполнение Android. Если оставить режим совместимости, могут возникнуть проблемы со стабильностью. Укажите максимальную версию пакетов Chrome, которые поддерживают режим совместимости для стабильной работы, как описано ниже.

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

Чтение настроек Chrome

Любое приложение может узнать, использует ли Chrome режим автозаполнения сторонних сервисов, который позволяет ему использовать автозаполнение Android. Для передачи этой информации 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 контента:

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

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

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

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

Используйте автозаполнение в следующих случаях:

Интеграция с клавиатурой

Начиная с Android 11, платформа позволяет клавиатурам и другим редакторам методов ввода (IME) показывать подсказки автозаполнения в строке, а не в раскрывающемся меню. Подробнее о том, как ваш сервис автозаполнения может поддерживать эту функцию, см. Интеграция автозаполнения с клавиатурами.

Разбиение наборов данных на страницы

Большой ответ автозаполнения может превысить допустимый размер транзакции объекта Binder, который представляет удаленный объект, необходимый для обработки запроса. Чтобы избежать исключений в таких случаях, не добавляйте в FillResponse больше 20 объектов Dataset за раз. Если в ответе нужно показать больше наборов данных, вы можете добавить набор данных, который сообщает пользователям о наличии дополнительной информации и при выборе извлекает следующую группу наборов данных. Дополнительную информацию вы найдете в статье addDataset(Dataset).

Сохранение данных, разделенных на несколько экранов

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

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

    Подробнее о том, как сохранять данные, разделенные на несколько экранов…

Добавьте логику инициализации и завершения для каждого запроса.

Каждый раз, когда поступает запрос на автозаполнение, система Android подключается к сервису и вызывает его метод onConnected(). После того как сервис обработает запрос, система Android вызовет метод 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.