Руководство по интеграции Device Trust от Android Enterprise

Этот документ должен быть вашим основным руководством по использованию AMAPI SDK для получения сигналов доверия устройства.

AMAPI SDK позволяет вашему приложению получать сигналы доверия от приложения Android Device Policy (ADP). Затем ваше приложение может использовать эти сигналы для вычисления состояния доверия устройства и применения выбранной бизнес-логики.

Требования

  • Ваше приложение должно быть авторизовано для чтения сигналов доверия устройства:
    • Сопутствующие приложения на устройствах, управляемых с помощью AMAPI. Приложения, которым в правилах назначена роль COMPANION_APP, могут получать доступ к сигналам доверия устройства, не будучи добавленными в белый список на полностью управляемых устройствах или из рабочего профиля (с доступом, ограниченным сигналами из рабочего профиля). Подробную информацию можно найти в статьях Управление ролями приложений, Настройка правил и Доступ к сигналам из сопутствующего приложения.
    • Все остальные приложения и среды. Чтобы получить доступ к сигналам доверия устройства из личного профиля, на неуправляемом устройстве, на устройстве, управляемом специальным контроллером политики устройств, или без роли COMPANION_APP, ваше приложение должно быть в белом списке. Чтобы узнать, как подать заявку, перейдите на страницу Доступ к сигналам доверия устройств.
  • Android Enterprise рекомендует интегрировать набор API Play Integrity в клиентское приложение и проверять результат перед чтением сигналов доверия устройства и использованием их в качестве основы. Устройства, не прошедшие проверку Play Integrity API, не должны считаться надежными. Это же относится и к любым сигналам, полученным от таких устройств и используемым для определения уровня доверия. Подробную информацию можно найти в документации Play Integrity API.

Интегрируйте AMAPI SDK в свое приложение

Чтобы получить доступ к сигналам доверия устройства, ваше приложение должно быть интегрировано с AMAPI SDK. Дополнительную информацию об этой библиотеке и о том, как добавить ее в приложение, можно найти в руководстве по интеграции AMAPI SDK.

Добавьте необходимые разрешения

Для некоторых сигналов, возвращаемых Device Trust от Android Enterprise API, требуется, чтобы приложение запрашивало то же разрешение, которое необходимо для доступа к этой информации. В частности:

Сигнал Требуемые права доступа
Статус сети ACCESS_NETWORK_STATE
Сложность блокировки экрана REQUEST_PASSWORD_COMPLEXITY

Если эти разрешения не включены в файл AndroidManifest.xml приложения, Device Trust от Android Enterprise API вернет PERMISSION_ISSUE в метаданных связанного сигнала:

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

Дополнительную информацию можно найти в списке доступных сигналов доверия к устройству.

Как получить доступ к сигналам доверия устройства

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

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

Как получить доступ к сигналам доверия устройства

Проверьте среду клиента

В примере кода ниже показано, как использовать getEnvironment, чтобы узнать текущее состояние приложения ADP. Если среда готова и актуальна, ваше приложение может использовать кешированный экземпляр deviceClient, чтобы получить доступ к сигналам доверия устройства (см. раздел Как получить доступ к сигналам доверия устройства).

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

Если приложение ADP установлено, но не актуально, ваше приложение должно вызвать prepareEnvironment, чтобы обновить приложение ADP без вмешательства пользователя.

Если приложение ADP не установлено, ваше приложение может вызвать prepareEnvironment, чтобы предложить пользователю установить приложение ADP. Подробнее о том, как подготовить клиентскую среду…

Подготовка клиентской среды

  • Если приложение ADP уже установлено, API автоматически обновит его без участия пользователя.

  • Если приложение ADP не установлено, API предложит пользователю установить его.

Установите Android Device Policy

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

Рекомендуем выполнять вызов prepareEnvironment из активного процесса во время адаптации пользователя, чтобы модальное диалоговое окно Установить Android Device Policy не стало для него неожиданностью. Если вызов из активного процесса невозможен, потому что это веб-процесс и у компонента Android нет интерфейса, разрешается вызов из фонового процесса, но только во время ознакомления с UX.

После правильной настройки среды можно получить доступ к сигналам доверия устройства. Подробнее о доступе к сигналам доверия устройства…

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

Доступ к сигналам доверия устройства

Рекомендация по использованию DeviceClient

При создании экземпляра deviceClient (или EnvironmentClient) устанавливается канал связи с приложением Android Device Policy (ADP), который автоматически отключается в режиме ожидания. Поскольку экземпляры клиента не предоставляют явного метода завершения работы, создание и удаление нескольких экземпляров оставляет потерянные каналы для сборки мусора.

Чтобы оптимизировать производительность и избежать появления потерянных каналов, создайте один экземпляр DeviceClient в области приложения с помощью DeviceClientFactory.create(context.applicationContext) и кешируйте его для повторного использования во всем приложении, например в качестве синглтона в области приложения или с помощью внедрения зависимостей.

Чтобы получить доступ к нужным сигналам доверия устройства, используйте кешированный экземпляр deviceClient для запроса объекта 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());

Доступ к сигналам из сопутствующего приложения

На полностью управляемых устройствах или в рабочем профиле, управляемом с помощью Android Management API (AMAPI), приложение, которому назначена роль COMPANION_APP, может считывать сигналы доверия устройства, не будучи зарегистрированным в белом списке.

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

1. Назначьте роль COMPANION_APP в правиле

Настройте правило EMM, чтобы предоставить разрешение roleType: "COMPANION_APP" вашему приложению в списке правил applications:

2. Как считывать сигналы напрямую с помощью DeviceClient

Создайте и кешируйте один экземпляр deviceClient в области действия приложения с помощью DeviceClientFactory.create (см. рекомендации по использованию DeviceClient), а затем запросите объект 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());
    }
}

Обработка ошибок

Если приложение отправляет запрос на проверку надежности устройства, который не удается выполнить, возвращается исключение. Это может происходить по разным причинам, например:

Стратегии повторных попыток

Если исключение вызвано временной проблемой, реализуйте стратегию повторных попыток с экспоненциальной выдержкой. После первой ошибки подождите 5 секунд, прежде чем повторить попытку.

Реализуйте стратегию повторных попыток с максимальным количеством попыток в качестве условия выхода, каждый раз используя экспоненциально увеличивающуюся задержку (10 с, 20 с и т. д.).

Отслеживание взаимодействия пользователя во время установки приложения ADP

Если во время prepareEnvironment на устройстве нужно установить приложение ADP, ваше приложение может отслеживать действия пользователя, реализуя NotificationReceiverService для получения уведомлений, переопределяющих 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");
        }
    }
}

Известные проблемы

В настоящее время известных проблем нет.