Device Trust de Android Enterprise: Guía de integración

Este documento debe ser tu guía principal para usar el SDK de AMAPI con el objetivo de recibir indicadores de confianza del dispositivo.

El SDK de AMAPI permite que tu aplicación acceda a los indicadores de confianza del dispositivo desde la app de Android Device Policy (ADP). Luego, tu app puede usar estos indicadores para calcular el estado de confianza del dispositivo y aplicar la lógica empresarial que se elija.

Requisitos previos

  • Tu aplicación debe tener autorización para leer los indicadores de confianza del dispositivo:
    • Apps complementarias en dispositivos administrados por AMAPI: Las aplicaciones a las que se les asigna el rol COMPANION_APP en la política pueden acceder a los indicadores de confianza del dispositivo sin estar en la lista de entidades permitidas en dispositivos completamente administrados o desde un perfil de trabajo (con acceso limitado a los indicadores del perfil de trabajo). Para obtener más detalles, consulta Administra roles de aplicación, Configuración de políticas y Accede a indicadores desde una app complementaria.
    • Todas las demás aplicaciones y entornos: Para acceder a los indicadores de confianza del dispositivo desde un perfil personal, en un dispositivo no administrado, en un dispositivo administrado por un DPC personalizado o sin el rol COMPANION_APP, se requiere que tu aplicación esté en la lista de entidades permitidas. Para obtener información sobre cómo postularte, ve a la página de acceso a los indicadores de confianza del dispositivo.
  • Android Enterprise recomienda integrar el paquete de APIs de Play Integrity en tu aplicación cliente y consultar el resultado antes de leer y confiar en los indicadores de confianza del dispositivo. No se debe confiar en los dispositivos que no aprueben las verificaciones de la API de Play Integrity ni en los indicadores derivados del dispositivo que se usen para determinar la postura de confianza. Puedes consultar la documentación de Play Integrity para obtener más detalles.

Integra el SDK de AMAPI en tu aplicación

Para acceder a los indicadores de confianza del dispositivo, tu aplicación debe integrarse con el SDK de AMAPI. Puedes encontrar más información sobre esta biblioteca y cómo agregarla a tu aplicación en la guía de integración del SDK de AMAPI.

Agrega los permisos necesarios

Algunos de los indicadores que se muestran desde la API de Device Trust de Android Enterprise requieren que la app declare el mismo permiso que se necesitaría para acceder a esta información en primer lugar, en particular:

Indicador Permiso necesario
Estado de la red ACCESS_NETWORK_STATE
Complejidad del bloqueo de pantalla REQUEST_PASSWORD_COMPLEXITY

Si estos permisos no se incluyen en el AndroidManifest.xml de la app, la API de Device Trust de Android Enterprise devolverá PERMISSION_ISSUE en los metadatos del indicador relacionado:

internalDeviceSettings=DeviceSettings{
  screenLockComplexity=COMPLEXITY_UNSPECIFIED,
  internalScreenLockComplexityMetadata=Metadata{
    dataIssues=[
      DataIssue{
        issueType=PERMISSION_ISSUE,
        issueLevel=WARNING,
        issueDetails=IssueDetailsCase{none}
      }
    ]
  },

Para obtener más detalles, consulta la lista de indicadores de confianza de dispositivos disponibles.

Pasos para acceder a los indicadores de confianza del dispositivo

Las aplicaciones de la lista de entidades permitidas (incluidas las apps que se ejecutan dentro de un perfil personal o en un dispositivo no administrado) deben verificar que el entorno del cliente esté actualizado y prepararlo, si es necesario, antes de solicitar indicadores.

Los pasos para que las aplicaciones de la lista de entidades permitidas accedan a los indicadores de confianza del dispositivo son los siguientes:

Pasos para acceder a los indicadores de confianza del dispositivo

Verifica el entorno del cliente

En el siguiente ejemplo de código, se muestra cómo usar getEnvironment para leer el estado actual de la app de ADP. Luego, tu aplicación puede usar una instancia de deviceClient almacenada en caché para acceder a los indicadores de confianza del dispositivo si el entorno está listo y actualizado (consulta Cómo acceder a los indicadores de confianza del dispositivo).

Kotlin

import com.google.android.managementapi.common.model.Role
import com.google.android.managementapi.device.DeviceClient
import com.google.android.managementapi.device.DeviceClientFactory
import com.google.android.managementapi.device.model.GetDeviceRequest
import com.google.android.managementapi.environment.EnvironmentClient
import com.google.android.managementapi.environment.EnvironmentClientFactory
import com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.INSTALLED
import com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.NOT_INSTALLED
import com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.READY
import com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.Version.UP_TO_DATE
import com.google.android.managementapi.environment.model.GetEnvironmentRequest
import com.google.android.managementapi.environment.model.PrepareEnvironmentRequest

try {
    val context = applicationContext

    val roles = listOf(Role.builder().setRoleType(Role.RoleType.IDENTITY_PROVIDER).build())
    val request = GetEnvironmentRequest.builder().setRoles(roles).build()
    val environmentClient = EnvironmentClientFactory.create(context)
    val environmentResponse = environmentClient.getEnvironment(request)

    if (environmentResponse.hasAndroidDevicePolicyEnvironment()) {
        val adpEnvironment = environmentResponse.androidDevicePolicyEnvironment

        if (adpEnvironment.state == READY && adpEnvironment.version == UP_TO_DATE) {
            // AMAPI Environment State OK, Version OK. Requesting Device signals..
            checkDevice(deviceClient)
        } else if (adpEnvironment.state == INSTALLED) {
            // prepareEnvironment should be called, calling
            // prepareEnvironment won't show the UI
            prepareEnvironment(context, environmentClient)
        } else if (adpEnvironment.state == NOT_INSTALLED) {
            // prepareEnvironment should be called, calling
            // prepareEnvironment will show the UI
            prepareEnvironment(context, environmentClient)
        }
    }
} catch (e: Exception) {
    Log.e(TAG, "Exception", e)
}

Java

import static com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.INSTALLED;
import static com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.NOT_INSTALLED;
import static com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.State.READY;
import static com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment.Version.UP_TO_DATE;

import com.google.android.managementapi.common.model.Role;
import com.google.android.managementapi.device.DeviceClient;
import com.google.android.managementapi.device.DeviceClientFactory;
import com.google.android.managementapi.device.model.Device;
import com.google.android.managementapi.device.model.GetDeviceRequest;
import com.google.android.managementapi.environment.EnvironmentClient;
import com.google.android.managementapi.environment.EnvironmentClientFactory;
import com.google.android.managementapi.environment.model.Environment;
import com.google.android.managementapi.environment.model.Environment.AndroidDevicePolicyEnvironment;
import com.google.android.managementapi.environment.model.GetEnvironmentRequest;
import com.google.android.managementapi.environment.model.PrepareEnvironmentRequest;
import com.google.android.managementapi.environment.model.PrepareEnvironmentResponse;

try {
    Context context = getApplicationContext();

    ImmutableList roles = new ImmutableList.Builder()
            .add(Role.builder()
                    .setRoleType(Role.RoleType.IDENTITY_PROVIDER)
                    .build())
            .build();

    EnvironmentClient environmentClient = EnvironmentClientFactory.create(context);
    GetEnvironmentRequest request = GetEnvironmentRequest.builder()
            .setRoles(roles)
            .build();

    ListenableFuture getEnvironmentFuture = environmentClient.getEnvironmentAsync(request);
    Futures.addCallback(getEnvironmentFuture, new FutureCallback<>() {
        @Override
        public void onSuccess(Environment environment) {
            AndroidDevicePolicyEnvironment adpEnvironment = environment.getAndroidDevicePolicyEnvironment();

            AndroidDevicePolicyEnvironment.State state = adpEnvironment.getState();
            AndroidDevicePolicyEnvironment.Version version = adpEnvironment.getVersion();
            if (state == READY && version == UP_TO_DATE) {
                // AMAPI Environment State OK, Version OK. Requesting Device signals..
                checkDevice(deviceClient);
            } else if (state == INSTALLED) {
                // prepareEnvironment should be called, calling
                // prepareEnvironment won't show the UI
                prepareEnvironment(context, environmentClient);
            } else if (state == NOT_INSTALLED) {
                // prepareEnvironment should be called, calling
                // prepareEnvironment will show the UI
                prepareEnvironment(context, environmentClient);
            }
        }

        @Override
        public void onFailure(Throwable t) {
            Log.d(TAG, t.toString());
        }
    }, MoreExecutors.directExecutor());
} catch (Exception e) {
    Log.d(TAG, e.toString());
}

Si la app de ADP está instalada, pero no actualizada, tu aplicación debe llamar a prepareEnvironment para actualizarla en segundo plano sin la intervención del usuario.

Si la app de ADP no está instalada, tu aplicación puede llamar a prepareEnvironment para solicitarle al usuario que la instale. Consulta Prepara el entorno del cliente.

Prepara el entorno del cliente

  • Si la app de ADP ya está instalada, la API la actualizará de forma silenciosa sin intervención del usuario.

  • Si la app de ADP no está instalada, la API le solicitará al usuario que acepte la instalación de la app de ADP.

Instala Android Device Policy

Es posible registrar una devolución de llamada para supervisar la selección del usuario. Consulta Cómo hacer un seguimiento de la interacción del usuario durante la instalación de la app de ADP para obtener más detalles.

Te recomendamos que la llamada a prepareEnvironment se realice desde un proceso en primer plano, durante el flujo de UX de incorporación, para evitar sorprender al usuario con el diálogo modal Instalar política de dispositivo Android. Si no es posible realizar la llamada desde un proceso en primer plano, porque se trata de un flujo web y el componente de Android no tiene IU, se permite realizar la llamada desde el segundo plano con el requisito de que esto ocurra durante el flujo de UX de incorporación.

Una vez que el entorno esté configurado correctamente, será posible acceder a los indicadores de confianza del dispositivo. Consulta Cómo acceder a los indicadores de confianza del dispositivo.

Kotlin

try {
    val myNotificationReceiverService = ComponentName(
        context, MyNotificationReceiverService::class.java
    )

    val roles = listOf(Role.builder().setRoleType(Role.RoleType.IDENTITY_PROVIDER).build())
    val request = PrepareEnvironmentRequest.builder().setRoles(roles).build()

    val response =
        environmentClient.prepareEnvironment(request, myNotificationReceiverService)

    val environment = response.environment
    val adpEnvironment = environment.androidDevicePolicyEnvironment

    val state = adpEnvironment.state
    val version = adpEnvironment.version
    if (state == READY && version == UP_TO_DATE) {
        // Environment is prepared, access device posture signals using
        // DeviceClient.
        checkDevice(deviceClient)
    } else {
        // The prepareEnvironment call failed to prepare
        Log.w(
            TAG, "AMAPI environment was not ready: " + state + " - " + version
        )
    }

} catch (e: java.lang.Exception) {
    Log.d(TAG, e.toString())
}

Java

try {
    ComponentName myNotificationReceiverService = new ComponentName(
            context,
            MyNotificationReceiverService.class
    );

    ImmutableList roles = new ImmutableList.Builder()
            .add(Role.builder()
                    .setRoleType(Role.RoleType.IDENTITY_PROVIDER)
                    .build())
            .build();


    PrepareEnvironmentRequest request = PrepareEnvironmentRequest.builder()
            .setRoles(roles)
            .build();

    ListenableFuture environmentFuture =
            environmentClient.prepareEnvironmentAsync(
                    request,
                    myNotificationReceiverService
            );

    Futures.addCallback(environmentFuture, new FutureCallback<>() {
        @Override
        public void onSuccess(PrepareEnvironmentResponse response) {
            Environment environment = response.getEnvironment();
            AndroidDevicePolicyEnvironment adpEnvironment = environment.getAndroidDevicePolicyEnvironment();

            AndroidDevicePolicyEnvironment.State state = adpEnvironment.getState();
            AndroidDevicePolicyEnvironment.Version version = adpEnvironment.getVersion();
            if (state == READY && version == UP_TO_DATE) {
                // AMAPI Environment State OK, Version OK. Requesting Device signals..
                checkDevice(deviceClient);
            } else {
                // The prepareEnvironment call failed to prepare
                Log.w(
                        TAG, "AMAPI environment was not ready: "
                        + adpEnvironment.getState() + " - " + adpEnvironment.getVersion()
                );
            }
        }

        @Override
        public void onFailure(@NonNull Throwable t) {
            // Handle the error
            Log.d(TAG, "AMAPI response did not contain an ADP environment");
        }
    }, MoreExecutors.directExecutor());
} catch (Exception e) {
    Log.d(TAG, e.toString());
}

Accede a los indicadores de confianza del dispositivo

Recomendación de uso de DeviceClient

Cuando se crea una instancia de deviceClient (o una instancia de EnvironmentClient), se establece un canal de comunicación con la app de política de dispositivo Android (ADP) que se desvincula automáticamente cuando está inactiva. Debido a que las instancias de cliente no exponen un método de cierre explícito, la creación y el descarte de varias instancias deja canales huérfanos para que se recolecten como elementos no utilizados.

Para optimizar el rendimiento y evitar canales huérfanos, crea una sola instancia de DeviceClient en el alcance de la aplicación con DeviceClientFactory.create(context.applicationContext) y almacénala en caché para reutilizarla en toda la aplicación, por ejemplo, como un singleton con alcance de aplicación o con la inserción de dependencias.

Para acceder a los indicadores de confianza del dispositivo que te interesan, usa tu instancia de deviceClient almacenada en caché para solicitar el objeto Device:

Kotlin

runCatching {
    deviceClient.getDeviceAwait(GetDeviceRequest.getDefaultInstance())
}.onFailure { t ->
    Log.d(TAG, t.toString())
}.onSuccess { device ->
    // Access device posture signals available in device
    val deviceString = device.toString()
    Log.d(TAG, deviceString)
}

Java

ListenableFuture deviceFuture =
        deviceClient.getDevice(GetDeviceRequest.getDefaultInstance());

Futures.addCallback(deviceFuture, new FutureCallback() {
    @Override
    public void onSuccess(Device device) {
        // Access device posture signals available in device
        String deviceString = device.toString();
        Log.d(TAG, deviceString);
    }

    @Override
    public void onFailure(Throwable t) {
        Log.d(TAG, t.toString());
    }
}, MoreExecutors.directExecutor());

Cómo acceder a los indicadores de una app complementaria

En los dispositivos completamente administrados o desde un perfil de trabajo administrado con la API de Android Management (AMAPI), una aplicación a la que se le asigna el rol COMPANION_APP puede leer los indicadores de confianza del dispositivo sin estar registrada en la lista de entidades permitidas.

Cuando se llama desde un perfil de trabajo (en un dispositivo de la empresa o personal), la app debe ejecutarse dentro del perfil de trabajo, y el acceso se limita a los indicadores del perfil de trabajo (por ejemplo, el estado de administración, la complejidad del bloqueo de pantalla y las aplicaciones instaladas reflejan solo el perfil de trabajo).

1. Asigna el rol COMPANION_APP en la política

Configura tu política de EMM para otorgar roleType: "COMPANION_APP" a tu aplicación en la lista de políticas applications:

2. Lee indicadores directamente con DeviceClient

Crea y almacena en caché una sola instancia de deviceClient en el alcance de la aplicación con DeviceClientFactory.create (consulta Recomendación de uso de DeviceClient) y, luego, solicita el objeto Device directamente:

Kotlin

import android.content.Context
import android.util.Log
import com.google.android.managementapi.device.DeviceClient
import com.google.android.managementapi.device.DeviceClientFactory
import com.google.android.managementapi.device.model.GetDeviceRequest

// Instantiate once at application scope (for example, using dependency injection)
// to reuse a single DeviceClient instance across your app.
class DeviceTrustRepository(context: Context) {
    private val deviceClient: DeviceClient =
        DeviceClientFactory.create(context.applicationContext)

    suspend fun readSignalsFromCompanionApp() {
        runCatching {
            deviceClient.getDeviceAwait(GetDeviceRequest.getDefaultInstance())
        }.onSuccess { device ->
            Log.d(TAG, "Device signals: $device")
        }.onFailure { t ->
            Log.e(TAG, "Failed to read device signals", t)
        }
    }

    companion object {
        private const val TAG = "DeviceTrustRepository"
    }
}

Java

import android.content.Context;
import android.util.Log;
import com.google.android.managementapi.device.DeviceClient;
import com.google.android.managementapi.device.DeviceClientFactory;
import com.google.android.managementapi.device.model.Device;
import com.google.android.managementapi.device.model.GetDeviceRequest;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;

// Instantiate once at application scope (for example, using dependency injection)
// to reuse a single DeviceClient instance across your app.
public class DeviceTrustRepository {
    private static final String TAG = "DeviceTrustRepository";
    private final DeviceClient deviceClient;

    public DeviceTrustRepository(Context context) {
        this.deviceClient = DeviceClientFactory.create(context.getApplicationContext());
    }

    public void readSignalsFromCompanionApp() {
        ListenableFuture deviceFuture =
                deviceClient.getDevice(GetDeviceRequest.getDefaultInstance());

        Futures.addCallback(deviceFuture, new FutureCallback() {
            @Override
            public void onSuccess(Device device) {
                Log.d(TAG, "Device signals: " + device);
            }

            @Override
            public void onFailure(Throwable t) {
                Log.e(TAG, "Failed to read device signals", t);
            }
        }, MoreExecutors.directExecutor());
    }
}

Soluciona errores

Si tu app realiza una solicitud de confianza del dispositivo y la llamada falla, la app recibe una excepción. Estas excepciones pueden ocurrir por varios motivos, como los siguientes:

Estrategias de reintento

Para las excepciones debido a una condición transitoria, implementa una estrategia de reintento con una retirada exponencial. Después de la primera falla, comienza con un retraso inicial de 5 segundos antes de reintentarlo.

Implementa una estrategia de reintento con una cantidad máxima de intentos como condición de salida usando un aumento exponencial del retraso cada vez (10 s, 20 s, etcétera).

Cómo hacer un seguimiento de la interacción del usuario durante la instalación de la app de ADP

Si el dispositivo necesita instalar la app de ADP durante el prepareEnvironment, tu aplicación puede hacer un seguimiento de la interacción del usuario implementando un NotificationReceiverService para recibir notificaciones que anulen getPrepareEnvironmentListener:

Kotlin

import android.util.Log
import com.google.android.managementapi.environment.EnvironmentListener
import com.google.android.managementapi.environment.model.EnvironmentEvent.EventCase.Kind.ANDROID_DEVICE_POLICY_INSTALL_CONSENT_ACCEPTED
import com.google.android.managementapi.environment.model.EnvironmentEvent
import com.google.android.managementapi.notification.NotificationReceiverService

class MyNotificationReceiverService : NotificationReceiverService() {
    override fun getPrepareEnvironmentListener(): EnvironmentListener {
        return MyEnvironmentListener()
    }
}

class MyEnvironmentListener : EnvironmentListener {
    override fun onEnvironmentEvent(
        event: EnvironmentEvent
    ) {
        if (event.event.kind == ANDROID_DEVICE_POLICY_INSTALL_CONSENT_ACCEPTED) {
            Log.d(TAG, "User provided install consent")
        } else {
            Log.d(TAG, "User rejected install consent")
        }
    }

    companion object {
        private val TAG: String = MyEnvironmentListener::class.java.simpleName
    }
}

Java

import static com.google.android.managementapi.environment.model.EnvironmentEvent.EventCase.Kind.ANDROID_DEVICE_POLICY_INSTALL_CONSENT_ACCEPTED;

import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.managementapi.environment.EnvironmentListener;
import com.google.android.managementapi.environment.model.EnvironmentEvent;
import com.google.android.managementapi.notification.NotificationReceiverService;

class MyNotificationReceiverService extends NotificationReceiverService {
    @NonNull
    @Override
    protected EnvironmentListener getPrepareEnvironmentListener() {
        return new MyEnvironmentListener();
    }
}
class MyEnvironmentListener implements EnvironmentListener {
    final private String TAG = MyEnvironmentListener.class.getSimpleName();

    @Override
    public void onEnvironmentEvent(EnvironmentEvent event) {
        if (event.getEvent().getKind() == ANDROID_DEVICE_POLICY_INSTALL_CONSENT_ACCEPTED)
        {
            Log.d(TAG, "User provided install consent");
        } else {
            Log.d(TAG, "User rejected install consent");
        }
    }
}

Problemas conocidos

Por el momento, no hay problemas conocidos.