Device Trust from Android Enterprise : guide d'intégration

Ce document doit être votre guide principal pour l'utilisation du SDK AMAPI afin de recevoir des signaux de confiance de l'appareil.

Le SDK AMAPI permet à votre application d'accéder aux signaux de confiance des appareils à partir de l'application Règles relatives aux appareils Android (ADP). Votre application peut ensuite utiliser ces signaux pour calculer l'état de confiance de l'appareil et appliquer la logique métier de votre choix.

Prérequis

  • Votre application doit être autorisée à lire les signaux de confiance de l'appareil :
    • Applications associées sur les appareils gérés par l'AMAPI : les applications auxquelles le rôle COMPANION_APP est attribué dans la règle peuvent accéder aux signaux de confiance de l'appareil sans figurer sur la liste d'autorisation sur les appareils entièrement gérés ou depuis un profil professionnel (avec un accès limité aux signaux du profil professionnel). Pour en savoir plus, consultez Gérer les rôles d'application, Configuration des règles et Accéder aux signaux depuis une application associée.
    • Toutes les autres applications et tous les autres environnements : pour accéder aux signaux de confiance de l'appareil depuis un profil personnel, sur un appareil non géré, sur un appareil géré par un DPC personnalisé ou sans le rôle COMPANION_APP, votre application doit figurer sur la liste d'autorisation. Pour savoir comment envoyer une demande, consultez la page Accès aux signaux de confiance des appareils.
  • Android Enterprise recommande d'intégrer la suite d'API Play Integrity à votre application cliente et de vous référer au résultat avant de lire les signaux de confiance de l'appareil et de vous y fier. Les appareils qui échouent aux vérifications de l'API Play Integrity ne doivent pas être considérés comme fiables, pas plus que les signaux provenant de l'appareil utilisé pour déterminer la stratégie de confiance. Pour en savoir plus, consultez la documentation sur Play Integrity.

Intégrer le SDK AMAPI à votre application

Pour accéder aux signaux de confiance de l'appareil, votre application doit s'intégrer au SDK AMAPI. Pour en savoir plus sur cette bibliothèque et sur la façon de l'ajouter à votre application, consultez le guide d'intégration du SDK AMAPI.

Ajouter les autorisations requises

Certains des signaux renvoyés par l'API Device Trust from Android Enterprise nécessitent que l'application déclare la même autorisation que celle requise pour accéder à ces informations, en particulier :

Signal Autorisation requise
État du réseau ACCESS_NETWORK_STATE
Complexité du verrouillage de l'écran REQUEST_PASSWORD_COMPLEXITY

Si ces autorisations ne sont pas incluses dans le fichier AndroidManifest.xml de l'application, l'API Device Trust d'Android Enterprise renvoie PERMISSION_ISSUE dans les métadonnées du signal associé :

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

Pour en savoir plus, consultez la liste des signaux de confiance des appareils disponibles.

Procédure d'accès aux signaux de confiance des appareils

Les applications de la liste d'autorisation (y compris celles exécutées dans un profil personnel ou sur un appareil non géré) doivent vérifier que l'environnement client est à jour et le préparer si nécessaire avant de demander des signaux.

Voici la procédure à suivre pour que les applications de la liste d'autorisation puissent accéder aux signaux de confiance des appareils :

Procédure d'accès aux signaux de confiance des appareils

Vérifier l'environnement client

L'exemple de code suivant montre comment utiliser getEnvironment pour lire l'état actuel de l'application ADP. Votre application peut ensuite utiliser une instance deviceClient mise en cache pour accéder aux signaux de confiance de l'appareil si l'environnement est prêt et à jour (voir Accéder aux signaux de confiance de l'appareil).

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 l'application ADP est installée, mais n'est pas à jour, votre application doit appeler prepareEnvironment pour mettre à jour l'application ADP de manière silencieuse, sans intervention de l'utilisateur.

Si l'application ADP n'est pas installée, votre application peut appeler prepareEnvironment pour inviter l'utilisateur à l'installer. Consultez Préparer l'environnement client.

Préparer l'environnement client

  • Si l'application ADP est déjà installée, l'API la mettra à jour de manière silencieuse, sans intervention de l'utilisateur.

  • Si l'application ADP n'est pas installée, l'API invite l'utilisateur à accepter l'installation de l'application ADP.

Installer Android Device Policy

Il est possible d'enregistrer un rappel pour surveiller le choix utilisateur. Pour en savoir plus, consultez Suivre l'interaction des utilisateurs lors de l'installation de l'application ADP.

Nous vous recommandons d'effectuer l'appel prepareEnvironment à partir d'un processus de premier plan, pendant le flux d'intégration de l'expérience utilisateur, afin d'éviter de surprendre l'utilisateur avec la boîte de dialogue modale Installer Android Device Policy. S'il n'est pas possible d'appeler depuis un processus de premier plan, car il s'agit d'un flux Web et que le composant Android n'a pas d'UI, l'appel depuis l'arrière-plan est autorisé, à condition qu'il se produise pendant le flux d'expérience utilisateur d'intégration.

Une fois l'environnement correctement configuré, vous pouvez accéder aux signaux de confiance de l'appareil. Consultez Accéder aux signaux de confiance des appareils.

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

Accéder aux signaux de confiance des appareils

Recommandation d'utilisation de DeviceClient

La création d'une instance deviceClient (ou d'une instance EnvironmentClient) établit un canal de communication avec l'application Règles relatives aux appareils Android (ADP) qui se dissocie automatiquement lorsqu'elle est inactive. Étant donné que les instances clientes n'exposent pas de méthode d'arrêt explicite, la création et l'abandon de plusieurs instances laissent des canaux orphelins à récupérer.

Pour optimiser les performances et éviter les canaux orphelins, créez une seule instance de DeviceClient au niveau de l'application à l'aide de DeviceClientFactory.create(context.applicationContext) et mettez-la en cache pour la réutiliser dans l'ensemble de votre application, par exemple en tant que singleton au niveau de l'application ou en utilisant l'injection de dépendances.

Pour accéder aux signaux de confiance des appareils qui vous intéressent, utilisez votre instance deviceClient mise en cache pour demander l'objet 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());

Accéder aux signaux depuis une application associée

Sur les appareils entièrement gérés ou depuis un profil professionnel géré avec l'API Android Management (AMAPI), une application à laquelle le rôle COMPANION_APP est attribué peut lire les signaux de confiance de l'appareil sans être enregistrée dans la liste d'autorisation.

Lorsqu'elle est appelée depuis un profil professionnel (sur un appareil appartenant à l'entreprise ou à un particulier), votre application doit s'exécuter dans le profil professionnel, et l'accès est limité aux signaux du profil professionnel (par exemple, l'état de gestion, la complexité du verrouillage de l'écran et les applications installées ne reflètent que le profil professionnel).

1. Attribuer le rôle COMPANION_APP dans la règle

Configurez votre règle EMM pour accorder roleType: "COMPANION_APP" à votre application dans la liste de règles applications :

2. Lire les signaux directement avec DeviceClient

Créez et mettez en cache une seule instance deviceClient au niveau de l'application à l'aide de DeviceClientFactory.create (voir Recommandation d'utilisation de DeviceClient), puis demandez directement l'objet Device :

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

Gérer les erreurs

Si votre application effectue une requête de confiance de l'appareil et que l'appel échoue, elle reçoit une exception. Ces exceptions peuvent se produire pour diverses raisons, par exemple :

Stratégies de nouvelle tentative

Pour les exceptions dues à une condition temporaire, implémentez une stratégie de nouvelle tentative avec un intervalle exponentiel entre les tentatives. Après le premier échec, commencez avec un délai initial de cinq secondes avant de réessayer.

Implémentez une stratégie de nouvelle tentative avec un nombre maximal de tentatives comme condition de sortie en augmentant le délai de manière exponentielle à chaque fois (10 s, 20 s, etc.).

Suivre l'interaction des utilisateurs lors de l'installation de l'application ADP

Si l'appareil doit installer l'application ADP pendant l'prepareEnvironment, votre application peut suivre l'interaction de l'utilisateur en implémentant un NotificationReceiverService pour recevoir des notifications remplaçant 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");
        }
    }
}

Problèmes connus

Il n'y a aucun problème connu pour le moment.