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

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

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)

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

Котлин

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

Котлин

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 , как показано в следующем примере кода:

Котлин

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

Котлин

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

Java

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

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

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

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

Котлин

// 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-код:

Котлин

// 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-код:

Котлин

// 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 рабочий_пароль
личное_имя_пользователя личный_пароль
Адрес рабочая_улица рабочий_город
personal_street персональный_город

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

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

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

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

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

  • Сервис автозаполнения работает на Android 9 (уровень API 28) или выше.
  • Пользователь дает согласие на использование вашей службы автозаполнения для считывания одноразовых кодов из SMS-сообщений.
  • Приложение, для которого вы предоставляете функцию автозаполнения, еще не использует 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 включен переключатель «Автозаполнение с использованием другого сервиса».
Рисунок 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 режим автозаполнения 3P, позволяющий ему использовать автозаполнение 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 контента:

Котлин

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, где пользователи могут включить автозаполнение от сторонних сервисов, используйте Android Intent . Обязательно настройте действие и категории, как показано в этом примере:

Котлин

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