Cómo crear servicios de autocompletado

Un servicio de autocompletado es una app con la que los usuarios pueden insertar datos en las vistas de otras apps para completar formularios con mayor facilidad. Los servicios Autocompletar también pueden recuperar datos del usuario de las vistas de una app y almacenarlos para usarlos en otro momento. En general, estos servicios los proporcionan las apps que administran datos del usuario, como los administradores de contraseñas.

Android hace que completar formularios sea más fácil con Autofill Framework, disponible en Android 8.0 (nivel de API 26) y versiones posteriores. Los usuarios pueden aprovechar las funciones de autocompletado solo si existe una app que brinde los servicios correspondientes en su dispositivo.

En esta página, se muestra la implementación de un servicio de autocompletado en una app. Si buscas una muestra de código en la que se vea cómo implementar un servicio, consulta el ejemplo de AutofillFramework de Java o Kotlin. Para obtener más detalles sobre el funcionamiento de los servicios Autocompletar, consulta las páginas de referencia de las clases AutofillService y AutofillManager.

Permisos y declaraciones de manifiesto

Las apps que brindan servicios Autocompletar deben incluir una declaración donde se describa la implementación del servicio. Para especificar la declaración, incluye un elemento <service> en el manifiesto de la app. El elemento <service> debe incluir los siguientes atributos y elementos:

  • El atributo android:name que apunta a la subclase de AutofillService en la app que implementa el servicio
  • El atributo android:permission que declara el permiso BIND_AUTOFILL_SERVICE
  • El elemento <intent-filter> con el secundario <action> obligatorio que especifica la acción android.service.autofill.AutofillService
  • El elemento opcional <meta-data> que se puede usar para proporcionar parámetros de configuración adicionales para el servicio

En el siguiente ejemplo, se muestra una declaración del servicio de autocompletado:

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

El elemento <meta-data> incluye un atributo android:resource que apunta a un recurso XML con más detalles sobre el servicio. El recurso service_configuration del ejemplo anterior especifica una actividad que permite que los usuarios configuren el servicio. En el siguiente ejemplo, se muestra el recurso XML service_configuration:

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

Para obtener más información sobre los recursos XML, consulta Descripción general sobre los recursos de las apps.

Solicitud para habilitar el servicio

Una app se utiliza como el servicio de autocompletado después de que declara el permiso BIND_AUTOFILL_SERVICE y el usuario lo habilita en la configuración del dispositivo. Para verificar si es el servicio habilitado actualmente, la app puede llamar al método hasEnabledAutofillServices() de la clase AutofillManager.

Si la app no es el servicio de autocompletado actual, puede solicitar al usuario que modifique la configuración correspondiente con el intent ACTION_REQUEST_SET_AUTOFILL_SERVICE. El intent muestra un valor RESULT_OK si el usuario seleccionó un servicio de autocompletado que coincide con el paquete del llamador.

Cómo completar vistas de cliente

El servicio de autocompletado recibe solicitudes para completar vistas de cliente cuando el usuario interactúa con otras apps. Si el servicio de autocompletado tiene datos del usuario que satisfacen la solicitud, los envía en la respuesta. El sistema Android muestra una IU de Autocompletar con los datos disponibles, como se muestra en la figura 1 a continuación:

IU de Autocompletar

Figura 1: IU de Autocompletar que muestra un conjunto de datos

Autofill Framework define un flujo de trabajo para completar vistas de forma que se minimice el tiempo en el que el sistema Android queda vinculado al servicio de autocompletado. En cada solicitud, el sistema Android envía un objeto AssistStructure al servicio con una llamada al método onFillRequest().

El servicio de autocompletado comprueba si puede satisfacer la solicitud con los datos del usuario que almacenó previamente. Si puede hacerlo, el servicio empaqueta los datos en los objetos Dataset. El servicio llama al método onSuccess() pasando un objeto FillResponse, que contiene los objetos Dataset. Si el servicio no tiene datos para satisfacer la solicitud, pasa null al método onSuccess().

Si se produce un error cuando se procesa la solicitud, el servicio llama al método onFailure(). Para obtener una explicación detallada del flujo de trabajo, consulta la descripción en la página de referencia de AutofillService.

El siguiente código muestra un ejemplo del método 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;
}

Un servicio puede tener más de un conjunto de datos que satisfaga la solicitud. En este caso, el sistema Android muestra varias opciones (una para cada conjunto de datos) en la IU de Autocompletar. El siguiente ejemplo de código muestra cómo proporcionar varios conjuntos de datos en una respuesta:

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

Los servicios Autocompletar pueden navegar por los objetos ViewNode en AssistStructure para recuperar los datos de Autocompletar requeridos y satisfacer la solicitud. Un servicio puede recuperar datos de Autocompletar con los métodos de la clase ViewNode, como getAutofillId().

Un servicio debe poder describir el contenido de una vista para comprobar si puede satisfacer la solicitud. El uso del atributo autofillHints es el primer criterio que un servicio debe utilizar para describir el contenido de una vista. Sin embargo, las apps de cliente deben proporcionar de forma explícita el atributo en sus vistas antes de que esté disponible para el servicio.

Si una app cliente no proporciona el atributo autofillHints, el servicio debe usar su propia heurística para describir el contenido. El servicio puede utilizar métodos de otras clases para obtener información sobre el contenido de la vista, como, getText() o getHint(). Para obtener más información, consulta Cómo proporcionar sugerencias de Autocompletar.

El siguiente ejemplo muestra cómo recorrer AssistStructure y recuperar datos de Autocompletar de un objeto 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);
    }
}

Cómo guardar datos del usuario

El servicio de autocompletado necesita datos del usuario para completar vistas en apps. Cuando los usuarios completan una vista manualmente, se les solicita que guarden los datos en el servicio de autocompletado actual, como se muestra en la figura 2.

IU de guardado de Autocompletar

Figura 2: IU de guardado de Autocompletar

Para guardar los datos, el servicio debe indicar su interés en almacenar los datos para usarlos en el futuro. Antes de que el sistema Android envíe una solicitud para guardar los datos, se muestra una solicitud de relleno en la que el servicio tiene la oportunidad de completar las vistas. Para indicar que le interesa guardar los datos, el servicio incluye un objeto SaveInfo en la respuesta de la solicitud de relleno. El objeto SaveInfo incluye, como mínimo, los siguientes datos:

  • El tipo de datos de usuario que se guardarán (para obtener una lista de los valores SAVE_DATA disponibles, consulta SaveInfo)
  • El conjunto mínimo de vistas que se deben modificar para activar una solicitud de guardado (por ejemplo, un formulario de acceso generalmente requiere que el usuario actualice las vistas username y password para activar una solicitud de guardado)

Se asocia un objeto SaveInfo con un objeto FillResponse, como se muestra en el siguiente ejemplo de código:

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

El servicio de autocompletado puede implementar una lógica para conservar los datos del usuario en el método onSaveRequest(), al que se suele llamar cuando se completa la actividad de cliente o cuando una app cliente llama a commit(). El siguiente código muestra un ejemplo del método 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();
}

Los servicios Autocompletar deben encriptar los datos sensibles antes de conservarlos. Sin embargo, los datos del usuario pueden incluir etiquetas o datos que no son sensibles. Por ejemplo, una cuenta de usuario puede incluir una etiqueta que marque los datos como una cuenta laboral o personal. Los servicios no deben encriptar las etiquetas. Si estas no se encriptan, los servicios pueden usarlas en las vistas de presentación si el usuario no se autenticó. Luego, los servicios pueden sustituir las etiquetas por los datos reales después de que el usuario se autentique.

Cómo posponer la IU de guardado de Autocompletar

A partir de Android 10, si utilizas varias pantallas para implementar un flujo de trabajo de Autocompletar (por ejemplo, una pantalla para el campo de nombre de usuario y otra para el de contraseña), puedes posponer la IU de guardado de Autocompletar con la marca SaveInfo.FLAG_DELAY_SAVE.

Si se establece esta marca, no se activa la IU de guardado de Autocompletar cuando se confirma el contexto de Autocompletar asociado con la respuesta SaveInfo. En su lugar, puedes usar una actividad independiente dentro de la misma tarea para entregar las solicitudes de completado futuras y, posteriormente, mostrar la IU a través de una solicitud de guardado. Para obtener más información, consulta SaveInfo.FLAG_DELAY_SAVE.

Cómo requerir la autenticación del usuario

Los servicios Autocompletar pueden solicitar que el usuario se autentique para poder completar vistas y, así, brindar un nivel de seguridad adicional. Los siguientes casos son buenos candidatos para implementar la autenticación del usuario:

  • Se necesita desbloquear los datos del usuario en la app con una contraseña principal o un escaneo de huella dactilar.
  • Es necesario desbloquear un conjunto de datos específico, por ejemplo, los detalles de una tarjeta de crédito con un código de verificación de tarjeta (CVC).

En un caso en el que el servicio requiere la autenticación del usuario para desbloquear los datos, el servicio puede presentar datos estándar o una etiqueta y especificar el Intent que se ocupa de la autenticación. Si necesitas datos adicionales para procesar la solicitud una vez completado el flujo de autenticación, puedes agregar esos datos al intent. A continuación, la actividad de autenticación puede mostrar los datos a la clase AutofillService de la app.

El siguiente ejemplo de código muestra la forma de especificar que la solicitud requiere autenticación:

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

Una vez que la actividad completa el flujo de autenticación, esta debe llamar al método setResult() pasando un valor RESULT_OK, y establecer el elemento adicional EXTRA_AUTHENTICATION_RESULT en el objeto FillResponse que incluye el conjunto de datos propagado. En el siguiente código, se puede ver un ejemplo de cómo mostrar el resultado cuando se completa el flujo de autenticación:

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

En el caso en el que se debe desbloquear un conjunto de datos de tarjeta de crédito, el servicio puede mostrar la IU para solicitar el CVC. Puedes presentar datos estándar, como el nombre del banco y los últimos cuatro dígitos del número de la tarjeta de crédito, para ocultar los datos hasta que se desbloquee el conjunto de datos. El siguiente ejemplo muestra cómo solicitar la autenticación para un conjunto de datos y ocultar los datos hasta que el usuario proporcione el 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();

Una vez que la actividad valida el CVC, debe llamar al método setResult() pasando un valor RESULT_OK y establecer el elemento EXTRA_AUTHENTICATION_RESULT adicional en un objeto Dataset que contenga el número y la fecha de vencimiento de la tarjeta de crédito. El nuevo conjunto de datos reemplaza al conjunto que requiere autenticación, y las vistas se completan de inmediato. El siguiente código muestra un ejemplo de cómo mostrar el conjunto de datos cuando el usuario proporciona el 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);

Cómo organizar los datos en grupos lógicos

Los servicios Autocompletar deben organizar los datos en grupos lógicos para aislar los conceptos de dominios diferentes. En esta página, estos grupos lógicos se denominan particiones. En la siguiente lista, se muestran ejemplos típicos de particiones y campos:

  • Credenciales, incluidos los campos de nombre de usuario y contraseña
  • Dirección, incluidos los campos de calle, ciudad, estado y código postal
  • Información de pago, incluidos los campos de número de tarjeta de crédito, fecha de vencimiento y código de verificación

Un servicio de autocompletado que separa correctamente los datos puede proteger mejor los datos de sus usuarios, ya que no expone los datos de más de una partición en un conjunto de datos. Por ejemplo, un conjunto de datos que contiene credenciales no necesita incluir información de pago. Cuando se organizan los datos en particiones, el servicio puede exponer la cantidad mínima de información necesaria para satisfacer una solicitud.

La organización de datos en particiones permite que los servicios completen actividades que tienen vistas de múltiples particiones y, al mismo tiempo, permite enviar la cantidad mínima de datos a la app cliente. Por ejemplo, imagina una actividad que incluya vistas para nombre de usuario, contraseña, calle y ciudad, y un servicio de autocompletado que tenga los siguientes datos:

Partición Campo 1 Campo 2
Credenciales nombre_de_usuario_laboral contraseña_laboral
nombre_de_usuario_personal contraseña_personal
Dirección calle_laboral ciudad_laboral
calle_personal ciudad_personal

El servicio puede preparar un conjunto de datos que incluya la partición de credenciales tanto para la cuenta laboral como para la personal. Cuando el usuario elige un conjunto de datos, una respuesta de Autocompletar subsiguiente puede proporcionar la dirección laboral o personal, según la primera elección del usuario.

Para identificar el campo que originó la solicitud, el servicio puede llamar al método isFocused() mientras recorre el objeto AssistStructure. Esto permite que el servicio prepare un elemento FillResponse con los datos de partición correctos.

Cómo autocompletar códigos únicos de SMS

Tu servicio de autocompletado puede ayudar al usuario a completar códigos únicos que se envían por SMS si usa la API de SMS Retriever.

Para usar esta función, se deben cumplir los siguientes requisitos:

  • El servicio de autocompletado se ejecuta en Android 9 (nivel de API 28) o versiones posteriores.
  • El usuario otorga su consentimiento para que tu servicio de autocompletado lea los códigos únicos de SMS.
  • La aplicación para la que proporcionas el servicio Autocompletar todavía no usa la API de SMS Retriever para leer códigos únicos.

Tu servicio de autocompletado puede usar SmsCodeAutofillClient, disponible llamando a SmsCodeRetriever.getAutofillClient() de los Servicios de Google Play 19.0.56 o versiones posteriores.

Estos son los pasos principales para usar esta API en un servicio de autocompletado:

  1. En el servicio de autocompletado, usa hasOngoingSmsRequest de SmsCodeAutofillClient para determinar si ya hay solicitudes activas para el nombre del paquete de la aplicación que quieres autocompletar. El servicio Autocompletar solo debe mostrar una sugerencia si se muestra el estado false.
  2. En el servicio de autocompletado, usa checkPermissionState de SmsCodeAutofillClient para verificar si el servicio de autocompletado tiene permiso para autocompletar códigos únicos. Este estado de permiso puede ser NONE, GRANTED o DENIED. El servicio de autocompletado debe mostrar una sugerencia para los estados NONE y GRANTED.
  3. En la actividad de autenticación de Autocompletar, usa el permiso SmsRetriever.SEND_PERMISSION para registrar un BroadcastReceiver que detecte SmsCodeRetriever.SMS_CODE_RETRIEVED_ACTION y así recibir el resultado del código de SMS cuando esté disponible.
  4. Llama a startSmsCodeRetriever en SmsCodeAutofillClient para comenzar a detectar los códigos únicos que se envían por SMS. Si el usuario otorga permisos para que tu servicio de autocompletado recupere códigos únicos de SMS, se buscan mensajes SMS que se recibieron en los últimos uno a cinco minutos a partir de ahora.

    Si tu servicio de autocompletado necesita solicitar un permiso del usuario para leer códigos únicos, es posible que el objeto Task que muestra startSmsCodeRetriever falle y que se muestre ResolvableApiException. Si esto sucede, debes llamar al método ResolvableApiException.startResolutionForResult() para mostrar un cuadro de diálogo de consentimiento para la solicitud de permiso.

  5. Recibe el resultado de código de SMS del intent y, luego, muestra el código de SMS como una respuesta de Autocompletar.

Casos de autocompletado avanzado

Integración con el teclado
A partir de Android 11, la plataforma permite que los teclados y otros editores de método de entrada (IMEs) muestren sugerencias de Autocompletar intercaladas, en lugar de usar un menú desplegable. Para obtener más información sobre la manera en que tu servicio de autocompletado puede admitir esta funcionalidad, consulta Cómo integrar Autocompletar con teclados.
Cómo paginar conjuntos de datos
Una respuesta de Autocompletar grande puede superar el tamaño de transacción permitido del objeto Binder que representa el objeto de uso remoto requerido para procesar la solicitud. Para evitar que el sistema Android arroje una excepción en estos casos, puedes mantener el elemento FillResponse en un tamaño pequeño agregando un máximo de 20 objetos Dataset a la vez. Si la respuesta requiere más conjuntos de datos, puedes agregar un conjunto que les informe a los usuarios que existe más información y que recupere el siguiente grupo de conjuntos de datos cuando se seleccione. Para obtener más información, consulta addDataset(Dataset).
Cómo guardar datos divididos en varias pantallas

A menudo, las apps dividen los datos del usuario en varias pantallas de la misma actividad, especialmente en las actividades que se utilizan para crear una nueva cuenta de usuario. Por ejemplo, la primera pantalla solicita un nombre de usuario y, si el nombre de usuario está disponible, la segunda pantalla solicita una contraseña. En estas situaciones, el servicio de autocompletado debe esperar hasta que el usuario complete ambos campos para poder mostrar la IU de guardado de Autocompletar. Sigue estos pasos para controlar esas situaciones:

  1. En la primera solicitud de completado, agrega un paquete de estado de cliente a la respuesta que incluyen los IDs de Autocompletar de los campos parciales presentes en la pantalla.
  2. En la segunda solicitud de completado, recupera el conjunto de estado de cliente, obtiene los IDs de Autocompletar establecidos en la solicitud anterior del estado de cliente y agrega estos IDs y la marca FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE al objeto SaveInfo utilizado en la segunda respuesta.
  3. En la solicitud de guardado, utiliza los objetos FillContext apropiados para obtener el valor de cada campo. Existe un contexto de relleno por cada solicitud de completado.

Si quieres obtener más información, consulta el artículo para guardar información cuando los datos se dividen en varias pantallas.

Cómo proporcionar una lógica de inicialización y desmontaje para cada solicitud

Cada vez que se realiza una solicitud de Autocompletar, el sistema Android se vincula al servicio y llama a su método onConnected(). Una vez que el servicio procesa la solicitud, el sistema Android llama al método onDisconnected() y se desvincula del servicio. Puedes implementar onConnected() para proporcionar un código que se ejecute antes de procesar una solicitud y onDisconnected() para proporcionar código que se ejecute después de procesar una solicitud.

Cómo personalizar la IU de guardado de Autocompletar

Los servicios Autocompletar pueden personalizar la IU de guardado de Autocompletar para ayudar a los usuarios a decidir si desean permitir que el servicio guarde sus datos. Los servicios pueden proporcionar información adicional sobre los datos que se guardan mediante un texto simple o una vista personalizada. Además, los servicios pueden modificar el aspecto del botón que cancela la solicitud de guardado y recibir una notificación cuando el usuario presiona ese botón. Para obtener más información, consulta la página de referencia de SaveInfo.

Modo de compatibilidad

El modo de compatibilidad permite que los servicios Autocompletar usen la estructura virtual de accesibilidad para completar información automáticamente. En particular, es útil para brindar la funcionalidad de Autocompletar en navegadores que no implementaron de forma explícita las APIs de Autofill.

Para probar el servicio de autocompletado con el modo de compatibilidad, incluye explícitamente en la lista de permitidos el navegador o la app que requiere el modo de compatibilidad. Para verificar qué paquetes ya se incorporaron a la lista de permitidos, ejecuta el siguiente comando:

$ adb shell settings get global autofill_compat_mode_allowed_packages

Si el paquete que deseas probar no está en la lista, agrégalo con el siguiente comando, en el que pkgX es el paquete de la app:

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

Si la app es un navegador, utiliza resIdx para especificar el ID de recurso del campo de entrada que contiene la URL de la página renderizada.

El modo de compatibilidad tiene las siguientes limitaciones:

  • Se activa una solicitud de guardado cuando el servicio usa la marca FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE o cuando se llama al método setTrigger(). FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE se configura de forma predeterminada cuando utilizas el modo de compatibilidad.
  • Es posible que el valor de texto de los nodos no esté disponible en el método onSaveRequest(SaveRequest, SaveCallback).

Para obtener más información sobre el modo de compatibilidad, incluidas las limitaciones asociadas, consulta la referencia de la clase AutofillService.