Как использовать фреймворк Autofill в Android Oreo

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

Новая версия Android, Android 8.0 Oreo, обеспечивает аналогичный функционал для приложений. Другими словами, Android теперь может помогать пользователя заполнять формы в приложениях, которые установлены на устройстве. Это была долгожданная функция, потому что набирать на виртуальной клавиатуре, как правило, довольно сложно.

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

Необходимые условия

Чтобы выполнить все этапы в этой статье, вам понадобится:

  • Android Studio 2.4 Preview 7 или выше;
  • Эмулятор или устройство с Android Oreo.

1. Создаём новый проект

Запустите Android Studio и создайте новый проект с пустой активностью. Не забудьте в целевом SDK выбрать API 26.

Для реализации нам понадобятся несколько виджетов из библиотеки дизайна, поэтому в файл build.gradle модуля app добавим следующую зависимость.

compile 'com.android.support:design:26.+'

Наконец нажмите Sync now для обновления проекта.

2. Создаём активность с настройками

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

Наш сервис, очевидно, должен знать, что такое адрес электронной почты. Поэтому давайте создадим активность, в которой пользователь сможет ввести и сохранить два адреса.

Шаг 1. Создание разметки

Как и следовало ожидать, разметка активности будет содержать два виджета EditText, в которые пользователь может ввести адреса электронной почты. Если вы хотите придерживаться принципов Material Design, то размещение этих виджетов внутри контейнеров TextInputLayout является хорошей идеей.

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

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
    >

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
  <EditText
      android:id="@+id/primary"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Ваш основной email-адрес"
      android:inputType="textEmailAddress"
      />
</android.support.design.widget.TextInputLayout>

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
  <EditText
      android:id="@+id/secondary"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="Ваш дополнительный email-адрес"
      android:inputType="textEmailAddress"
      />
</android.support.design.widget.TextInputLayout>

<Button
    android:id="@+id/save_button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="saveEmailAddresses"
    android:text="Сохранить"
    style="@style/Widget.AppCompat.Button.Colored"
    />
</LinearLayout>

В приведенном выше коде можно увидеть, что у компонента Button в атрибуте onClick указан метод. Нажмите на жёлтую лампочку рядом с этим атрибутом в Android Studio, чтобы создать заглушку этого метода в классе соответствующей активности.

public void saveEmailAddresses(View view) {
  // код будет добавлен в будущем
}

Шаг 2. Сохранение email адресов

Мы будем использовать файл общих предпочтений EMAIL_STORAGE, чтобы сохранить наши данные. Вы можете использовать метод getSharedPreferences() вашей активности для доступа к файлу. Кроме того, чтобы иметь возможность писать в файл, вы должны вызвать его метод edit(), который генерирует объект SharedPreferences.Editor.

Соответственно, добавьте следующий код внутри метода saveEmailAddresses():

SharedPreferences.Editor editor = getSharedPreferences("EMAIL_STORAGE", MODE_PRIVATE).edit();

Чтобы получить адреса электронной почты, введенные пользователем в EditText, вам сначала нужно получить ссылки на них с помощью метода findViewById(), а затем вызвать их методы getText().

String primaryEmailAddress = ((EditText) findViewById(R.id.primary)).getText().toString();
String secondaryEmailAddress = ((EditText) findViewById(R.id.secondary)).getText().toString();

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

editor.putString("PRIMARY_EMAIL", primaryEmailAddress);
editor.putString("SECONDARY_EMAIL", secondaryEmailAddress);
editor.commit();

Шаг 3. Создание файла метаданных

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

Создайте новый XML-файл с именем email_address_filler.xml в папке res/xml проекта. Внутри добавьте тег <autofill-service> и установите значение его атрибута attributeActivity для имени вашей активности.

<?xml version="1.0" encoding="utf-8"?>
<autofill-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsActivity="com.tutsplus.simplefill.MainActivity"/>

Теперь вы можете запустить приложение, ввести два адреса электронной почты и нажать кнопку «Сохранить», чтобы сохранить их.

3. Создаём сервис автозаполнения

Любой класс, который расширяет абстрактный класс AutoFillService, может служить для автозаполнения. Поэтому начните с создания нового класса Java с помощью File — New — Java Class. В появившемся диалоговом окне назовите класс EmailAddressFiller и убедитесь, что вы установили значение поля Superclass в AutofillService.

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

@Override public void onFillRequest(@NonNull FillRequest request,
    @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback) {

}

Шаг 1. Анализ иерархии View

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

Если вы никогда не видели такого дерева, я предлагаю вам использовать инструмент uiautomatorviewer, который является частью Android SDK, для анализа иерархии компоновки нескольких приложений. Например, вот как выглядит иерархия раскладки нашего приложения для Android:

Естественно, чтобы проаналазировать все узлы, разветвления, вам нужен рекурсивный метод. Давайте создадим его сейчас:

void identifyEmailFields(AssistStructure.ViewNode node,
    List<AssistStructure.ViewNode> emailFields) {
  // Пишем код здесь
}

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

Теперь вам может быть интересно, как вы можете программным образом определить, ожидает ли поле ввода адрес электронной почты. Ну, на самом деле нет надежного подхода, за которым вы можете следовать. На данный момент мы предполагаем, что все разработчики приложений всегда предоставляют идентификаторы ресурсов для своих полей ввода. Исходя из этого предположения, мы можем просто выбрать все поля ввода, в которых идентификаторы ресурсов содержат строки, такие как «email» и «username«.

Соответственно, добавьте следующий код в метод:

if (node.getClassName() != null && node.getClassName().contains("EditText")) {
  String viewId = node.getIdEntry();
  if (viewId != null && (viewId.contains("email") || viewId.contains("username"))) {
    emailFields.add(node);
    return;
  }
}

Далее, всякий раз, когда мы сталкиваемся с объектом ViewNode, который содержит больше объектов ViewNode, мы должны рекурсивно вызывать метод identEmailFields() для анализа всех его дочерних элементов. В следующем коде показано, как это сделать:

for (int i = 0; i < node.getChildCount(); i++) {
  identifyEmailFields(node.getChildAt(i), emailFields);
}

На этом этапе мы можем вызвать метод identEmailFields() внутри метода onFillRequest() и передать ему корневой узел иерархии представлений.

AssistStructure structure =
    request.getFillContexts().get(request.getFillContexts().size() - 1).getStructure();
// Создаем пустой список
List<AssistStructure.ViewNode> emailFields = new ArrayList<>();

// Заполняем список
identifyEmailFields(structure.getWindowNodeAt(0).getRootViewNode(), emailFields);

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

if (emailFields.size() == 0) return;

Шаг 2. Создаём  и заполняем удалённые представления

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

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

Чтобы инициализировать объект RemoteViews, вам понадобится XML-файл разметки. Давайте создадим один из них и назовём email_suggestion.xml. На данный момент он может содержать только один виджет TextView для отображения адреса электронной почты.

Соответственно, добавьте следующий код в email_suggestion.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/email_suggestion_item"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="5dp"
    android:textSize="18sp"
    android:textStyle="bold"
    >
</TextView>

Теперь вы можете вернуться к методу onFillRequest() и создать два объекта RemoteViews: один для основного электронного письма, а другой для дополнительного.

RemoteViews rvPrimaryEmail = new RemoteViews(getPackageName(), R.layout.email_suggestion);
RemoteViews rvSecondaryEmail = new RemoteViews(getPackageName(), R.layout.email_suggestion);

Виджеты TextView внутри объектов RemoteViews должны отображать два адреса электронной почты, которые мы сохранили в файле на активности настроек ранее. Чтобы открыть файл, снова используйте метод getSharedPreferences(). После его открытия вы можете использовать метод getString() для получения обоих адресов.

Наконец, чтобы поменять содержимое удаленных TextView, вы должны использовать метод setTextViewText().

// Загружаем адреса из настроек
SharedPreferences sharedPreferences = getSharedPreferences("EMAIL_STORAGE", MODE_PRIVATE);

String primaryEmail = sharedPreferences.getString("PRIMARY_EMAIL", "");
String secondaryEmail = sharedPreferences.getString("SECONDARY_EMAIL", "");

// Обновляет TextView
rvPrimaryEmail.setTextViewText(R.id.email_suggestion_item, primaryEmail);
rvSecondaryEmail.setTextViewText(R.id.email_suggestion_item, secondaryEmail);

Шаг 3. Создание наборов данных

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

AssistStructure.ViewNode emailField = emailFields.get(0);

Набор данных автозаполнения является всего лишь экземпляром класса Dataset и может быть построен с использованием класса Dataset.Builder.

Когда пользователь выбирает один из адресов электронной почты, показывающийся в раскрывающемся списке нашего сервиса, он должен установить содержимое связанного поля ввода с помощью метода setValue() класса Dataset.Builder. Однако вы не можете передать объект ViewNode методу setValue(). Он ожидает идентификатор автозаполнения, который должен быть получен путем вызова метода getAutofillId() объекта ViewNode.

Кроме того, чтобы указать текст, который должен быть записан в поле ввода, вы должны использовать метод AutofillValue.forText(). В следующем коде показано, как это сделать:

Dataset primaryEmailDataSet =
    new Dataset.Builder(rvPrimaryEmail).setValue(emailField.getAutofillId(),
        AutofillValue.forText(primaryEmail)).build();

Dataset secondaryEmailDataSet =
    new Dataset.Builder(rvSecondaryEmail).setValue(emailField.getAutofillId(),
        AutofillValue.forText(secondaryEmail)).build();

Прежде чем отправлять наборы данных в приложение, вы должны добавить их в объект FillResponse, который может быть создан с использованием класса FillResponse.Builder. Дважды вызовите его метод addDataset(), чтобы добавить оба набора данных.

Когда объект FillResponse готов, передайте его как аргумент методу onSuccess() объекта FillCallback, который является одним из параметров метода onFillRequest().

FillResponse response = new FillResponse.Builder().addDataset(primaryEmailDataSet)
    .addDataset(secondaryEmailDataSet)
    .build();

callback.onSuccess(response);

В результате код класса EmailAddressFiller будет выглядеть следующим образом:

public class EmailAddressFiller extends AutofillService {
  private static final String TAG = "EmailAddressFiller";
  
  @Override public void onFillRequest(@NonNull FillRequest request,
      @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback) {
    AssistStructure structure =
        request.getFillContexts().get(request.getFillContexts().size() - 1).getStructure();
    // Создаем пустой список
    List<AssistStructure.ViewNode> emailFields = new ArrayList<>();

    // Заполняем список
    identifyEmailFields(structure.getWindowNodeAt(0).getRootViewNode(), emailFields);

    if (emailFields.size() == 0) return;

    RemoteViews rvPrimaryEmail = new RemoteViews(getPackageName(), R.layout.email_suggestion);
    RemoteViews rvSecondaryEmail = new RemoteViews(getPackageName(), R.layout.email_suggestion);

    // Загружаем адреса из настроек
    SharedPreferences sharedPreferences = getSharedPreferences("EMAIL_STORAGE", MODE_PRIVATE);

    String primaryEmail = sharedPreferences.getString("PRIMARY_EMAIL", "");
    String secondaryEmail = sharedPreferences.getString("SECONDARY_EMAIL", "");

    // Обновляет TextView
    rvPrimaryEmail.setTextViewText(R.id.email_suggestion_item, primaryEmail);
    rvSecondaryEmail.setTextViewText(R.id.email_suggestion_item, secondaryEmail);

    AssistStructure.ViewNode emailField = emailFields.get(0);

    Dataset primaryEmailDataSet =
        new Dataset.Builder(rvPrimaryEmail).setValue(emailField.getAutofillId(),
            AutofillValue.forText(primaryEmail)).build();

    Dataset secondaryEmailDataSet =
        new Dataset.Builder(rvSecondaryEmail).setValue(emailField.getAutofillId(),
            AutofillValue.forText(secondaryEmail)).build();

    FillResponse response = new FillResponse.Builder().addDataset(primaryEmailDataSet)
        .addDataset(secondaryEmailDataSet)
        .build();

    callback.onSuccess(response);
  }

  @Override
  public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {

  }

  void identifyEmailFields(AssistStructure.ViewNode node,
      List<AssistStructure.ViewNode> emailFields) {
    if (node.getClassName() != null && node.getClassName().contains("EditText")) {
      String viewId = node.getIdEntry();
      if (viewId != null && (viewId.contains("email") || viewId.contains("username"))) {
        emailFields.add(node);
        return;
      }
    }

    for (int i = 0; i < node.getChildCount(); i++) {
      identifyEmailFields(node.getChildAt(i), emailFields);
    }
  }

  @Override public void onConnected() {
    Log.d(TAG, "onConnected");
  }

  @Override public void onDisconnected() {
    Log.d(TAG, "onDisconnected");
  }
}

Шаг 4. Обновляем манифест

Как и все сервисы, автозаполнение должно быть объявлено в файле AndroidManifest.xml проекта. При этом вы должны убедиться, что он защищен разрешением android.permission.BIND_AUTOFILL.

Этот сервис также нуждается в теге <intent-filter>, который позволяет ему отвечать на действие android.service.autofill.AutofillService и тег <meta-data>, который указывает на XML-файл метаданных, который мы создали на более раннем этапе.

Соответственно, добавьте следующие строки в файл манифеста:

<service
    android:name=".EmailAddressFiller"
    android:label="Autofill Service"
    android:permission="android.permission.BIND_AUTOFILL">
  <meta-data
      android:name="android.autofill"
      android:resource="@xml/email_address_filler"/>
  <intent-filter>
    <action android:name="android.service.autofill.AutofillService"/>
  </intent-filter>
</service>

Теперь наше приложение и сервис готовы. Соберите проект и установите приложение на своё устройство или эмулятор.

4. Активируем и используем сервис автозаполнения

Для того, чтобы активировать сервис автозаполнения, нужно зайти в Настройки — Система — Язык и ввод — Расширенные настройки — Autofill service (в текущей версии не переведено) и выбрать сервис приложения.

Теперь вы можете открыть любое приложение, требующее ввести адрес электронной почты, чтобы проверить работу сервиса. Например, Gmail.

Нашли ошибку в тексте?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *