Device Trust from Android Enterprise - Integration guide

  • The AMAPI SDK enables your application to access device trust signals from the Android Device Policy app for computing device trust state and enacting business logic.

  • Access to device trust signals requires integration with the AMAPI SDK version 1.3.0+ and declaring necessary permissions like ACCESS_NETWORK_STATE and REQUEST_PASSWORD_COMPLEXITY for specific signals.

  • Before accessing signals, you must verify the client environment using EnvironmentClient to ensure the Android Device Policy app is installed and up to date, preparing the environment with prepareEnvironment if necessary.

  • Accessing device trust signals involves using a DeviceClient instance to request the Device object once the environment is ready and up to date.

  • You can track user interaction during the Android Device Policy app installation by implementing a NotificationReceiverService and overriding getPrepareEnvironmentListener.

This document should be your primary guide for usage of the AMAPI SDK for the purposes of receiving device trust signals.

The AMAPI SDK enables your application to access device trust signals from the Android Device Policy (ADP) app. Your app can then use these signals to compute the device's trust state and enact business logic as chosen.

Prerequisites

  • Your application must be authorized to read device trust signals:
    • Companion apps on AMAPI-managed devices: Applications assigned the COMPANION_APP role in policy can access device trust signals without being on the allowlist on fully managed devices or from inside a work profile (with access limited to signals from the work profile). For details, see Manage application roles, Policy Configuration, and Access signals from a companion app.
    • All other applications and environments: Accessing device trust signals from inside a personal profile, on a non-managed device, on a device managed by a custom DPC, or without the COMPANION_APP role requires your application to be on the allowlist. For information on how to apply, go to the device trust signals access page.
  • Android Enterprise recommends integrating the Play Integrity suite of APIs into your client application and referring to the result prior to reading and relying on the device trust signals. Devices that fail Play Integrity API checks shouldn't be trusted, nor any signals derived from the device used to make trust posture determinations. You can refer to Play Integrity's documentation for more details.

Integrate with the AMAPI SDK in your application

To access the device trust signals your application has to integrate with the AMAPI SDK. You can find more information about this library and how to add it to your application in the AMAPI SDK integration guide.

Add required permissions

A few of the signals returned from the Device Trust from Android Enterprise API requires that the app declares the same permission that would be required to access this information in the first place, in particular:

Signal Required permission
Network State ACCESS_NETWORK_STATE
Screen lock complexity REQUEST_PASSWORD_COMPLEXITY

If these permissions are not included in the app's AndroidManifest.xml, the Device Trust from Android Enterprise API will return PERMISSION_ISSUE in the metadata of the related signal:

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

For additional details see the list of the available device trust signals.

Steps to access the device trust signals

Applications on the allowlist (including apps running inside a personal profile or on a non-managed device) must verify that the client environment is up to date and prepare it if necessary before requesting signals.

The steps for applications on the allowlist to access the device trust signals are:

Steps to access the device trust signals

Verify the client environment

The following code example shows how to use getEnvironment to read the current state of the ADP app. Your application can then use a cached deviceClient instance to access the device trust signals if the environment is ready and up to date (see Access device trust signals).

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

If the ADP app is installed but not up to date, your application should call prepareEnvironment to silently update the ADP app without user intervention.

If the ADP app is not installed, your application can call prepareEnvironment to prompt the user to install the ADP app. See Prepare the client environment.

Prepare the client environment

  • If the ADP app is already installed, the API will silently update it with no user intervention.

  • If the ADP app is not installed, the API will prompt the user to accept the installation of the ADP app.

Install Android Device Policy

It is possible to register a callback to monitor the user choice. See Track user interaction during the ADP app installation for additional details.

We recommend that the prepareEnvironment call is done from a foreground process, during the onboarding UX flow to avoid surprising the user with the Install Android Device Policy modal dialog. If calling from a foreground process is not possible, because this is a Web flow and the Android component has no UI, calling from background is permitted with the requirement that this happens during the onboarding UX flow.

Once the environment is correctly set up, it is possible to access the device trust Signals. See Access device trust signals.

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

Access device trust signals

DeviceClient usage recommendation

Creating a deviceClient instance (or an EnvironmentClient instance) establishes a communication channel with the Android Device Policy (ADP) app that automatically unbinds when idle. Because client instances don't expose an explicit shutdown method, creating and discarding multiple instances leaves orphaned channels to be garbage-collected.

To optimize performance and avoid orphaned channels, create a single instance of DeviceClient at application scope using DeviceClientFactory.create(context.applicationContext) and cache it for reuse throughout your application—for example, as an application-scoped singleton or using dependency injection.

To access the device trust signals you are interested in, use your cached deviceClient instance to request the Device object:

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

Access signals from a companion app

On fully managed devices or from inside a work profile managed with the Android Management API (AMAPI), an application assigned the COMPANION_APP role can read device trust signals without being registered on the allowlist.

When called from inside a work profile (on either a company-owned or personally-owned device), your app must run inside the work profile, and access is limited to signals from the work profile (for example, management state, screen lock complexity, and installed applications reflect only the work profile).

1. Assign the COMPANION_APP role in policy

Configure your EMM policy to grant roleType: "COMPANION_APP" to your application in the applications policy list:

2. Read signals directly with DeviceClient

Create and cache a single deviceClient instance at application scope using DeviceClientFactory.create (see DeviceClient usage recommendation), then request the Device object directly:

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

Handle errors

If your app makes a device trust request and the call fails, your app receives an exception. These exceptions can happen for various reasons, such as:

Retry strategies

For exceptions due to a transient condition implement a retry strategy with an exponential backoff. After the first failure, start with an initial delay of 5 seconds before retrying.

Implement a retry strategy with a maximum number of attempts as an exit condition using an exponentially increased delay each time (10s, 20s, etc.).

Track user interaction during ADP app installation

If the device needs to install the ADP app during the prepareEnvironment your application can track the user interaction implementing a NotificationReceiverService to receive notifications overriding 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");
        }
    }
}

Known issues

There are no known issues at the moment.