Device Trust do Android Enterprise: guia de integração

Este documento é o guia principal para usar o SDK da AMAPI com o objetivo de receber indicadores de confiança do dispositivo.

O SDK da AMAPI permite que seu aplicativo acesse indicadores de confiança do dispositivo do app Android Device Policy (ADP). Seu app pode usar esses indicadores para calcular o estado de confiança do dispositivo e aplicar a lógica de negócios escolhida.

Pré-requisitos

  • Seu aplicativo precisa ter autorização para ler os indicadores de confiança do dispositivo:
    • Apps complementares em dispositivos gerenciados pela AMAPI: os aplicativos que recebem o papel COMPANION_APP na política podem acessar indicadores de confiança do dispositivo sem estar na lista de permissões em dispositivos totalmente gerenciados ou em um perfil de trabalho (com acesso limitado aos indicadores do perfil de trabalho). Para mais detalhes, consulte Gerenciar papéis de aplicativos, Configuração de políticas e Acessar indicadores de um app complementar.
    • Todos os outros aplicativos e ambientes: para acessar indicadores de confiança do dispositivo em um perfil pessoal, em um dispositivo não gerenciado, em um dispositivo gerenciado por um DPC personalizado ou sem o papel COMPANION_APP, é necessário que o aplicativo esteja na lista de permissões. Para saber como se inscrever, acesse a página de acesso aos indicadores de confiança do dispositivo.
  • O Android Enterprise recomenda integrar o pacote de APIs Play Integrity ao aplicativo cliente e consultar o resultado antes de ler e confiar nos indicadores de confiança do dispositivo. Os dispositivos que não passam nas verificações da API Play Integrity não são confiáveis, assim como os indicadores derivados do dispositivo usado para determinar a postura de confiança. Consulte a documentação da Play Integrity para mais detalhes.

Integrar o SDK da AMAPI ao seu aplicativo

Para acessar os indicadores de confiança do dispositivo, seu aplicativo precisa se integrar ao SDK da AMAPI. Para mais informações sobre essa biblioteca e como adicioná-la ao seu aplicativo, consulte o guia de integração do SDK da AMAPI.

Adicionar as permissões necessárias

Alguns dos indicadores retornados pela API Device Trust do Android Enterprise exigem que o app declare a mesma permissão que seria necessária para acessar essas informações, em especial:

Indicador Permissão necessária
Estado da rede ACCESS_NETWORK_STATE
Complexidade do bloqueio de tela REQUEST_PASSWORD_COMPLEXITY

Se essas permissões não estiverem incluídas no AndroidManifest.xml do app, a API Device Trust do Android Enterprise vai retornar PERMISSION_ISSUE nos metadados do indicador relacionado:

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

Para mais detalhes, consulte a lista de indicadores de confiabilidade do dispositivo disponíveis.

Etapas para acessar os indicadores de confiança do dispositivo

Os aplicativos na lista de permissões (incluindo apps em execução em um perfil pessoal ou em um dispositivo não gerenciado) precisam verificar se o ambiente do cliente está atualizado e prepará-lo, se necessário, antes de solicitar indicadores.

As etapas para que os aplicativos na lista de permissões acessem os indicadores de confiança do dispositivo são:

Etapas para acessar os indicadores de confiança do dispositivo

Verificar o ambiente do cliente

O exemplo de código a seguir mostra como usar getEnvironment para ler o estado atual do app ADP. Seu aplicativo pode usar uma instância deviceClient em cache para acessar os indicadores de confiança do dispositivo se o ambiente estiver pronto e atualizado (consulte Acessar indicadores de confiança do 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());
}

Se o app ADP estiver instalado, mas não atualizado, seu aplicativo precisará chamar prepareEnvironment para atualizar o app ADP de forma silenciosa, sem intervenção do usuário.

Se o app ADP não estiver instalado, seu aplicativo poderá chamar prepareEnvironment para solicitar que o usuário instale o app ADP. Consulte Preparar o ambiente do cliente.

Preparar o ambiente do cliente

  • Se o app ADP já estiver instalado, a API vai atualizá-lo silenciosamente sem intervenção do usuário.

  • Se o app ADP não estiver instalado, a API vai solicitar que o usuário aceite a instalação dele.

Instalar Android Device Policy

É possível registrar um callback para monitorar a escolha do usuário. Consulte Rastrear a interação do usuário durante a instalação do app ADP para mais detalhes.

Recomendamos que a chamada prepareEnvironment seja feita em um processo em primeiro plano, durante o fluxo de UX de integração, para evitar que o usuário seja surpreendido pela caixa de diálogo modal Instalar o Android Device Policy. Se não for possível fazer a chamada de um processo em primeiro plano, porque esse é um fluxo da Web e o componente do Android não tem interface, a chamada em segundo plano é permitida, desde que isso aconteça durante o fluxo de UX de integração.

Depois que o ambiente estiver configurado corretamente, será possível acessar os indicadores de confiança do dispositivo. Consulte Acessar indicadores de confiança do 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());
}

Acessar indicadores de confiança do dispositivo

Recomendação de uso do DeviceClient

A criação de uma instância deviceClient (ou EnvironmentClient) estabelece um canal de comunicação com o app Android Device Policy (ADP) que é desvinculado automaticamente quando está inativo. Como as instâncias de cliente não expõem um método de encerramento explícito, criar e descartar várias instâncias deixa canais órfãos para serem coletados como lixo.

Para otimizar o desempenho e evitar canais órfãos, crie uma única instância de DeviceClient no escopo do aplicativo usando DeviceClientFactory.create(context.applicationContext) e armazene-a em cache para reutilização em todo o aplicativo, por exemplo, como um singleton com escopo de aplicativo ou usando injeção de dependência.

Para acessar os indicadores de confiança do dispositivo que você quer, use a instância deviceClient em cache para solicitar o 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());

Acessar indicadores de um app complementar

Em dispositivos totalmente gerenciados ou em um perfil de trabalho gerenciado com a API Android Management (AMAPI), um aplicativo com o papel COMPANION_APP pode ler indicadores de confiança do dispositivo sem estar registrado na lista de permissões.

Quando chamado de dentro de um perfil de trabalho (em um dispositivo da empresa ou pessoal), o app precisa ser executado dentro do perfil de trabalho, e o acesso é limitado aos indicadores do perfil de trabalho (por exemplo, o estado de gerenciamento, a complexidade do bloqueio de tela e os aplicativos instalados refletem apenas o perfil de trabalho).

1. Atribuir o papel COMPANION_APP na política

Configure sua política de EMM para conceder roleType: "COMPANION_APP" ao seu aplicativo na lista de políticas applications:

2. Ler indicadores diretamente com o DeviceClient

Crie e armazene em cache uma única instância de deviceClient no escopo do aplicativo usando DeviceClientFactory.create (consulte a recomendação de uso do DeviceClient) e solicite o objeto Device diretamente:

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

Solucionar erros

Se o app fizer uma solicitação de confiança do dispositivo e a chamada falhar, ele vai receber uma exceção. Essas exceções podem acontecer por vários motivos, como:

Estratégias de nova tentativa

Para exceções devido a uma condição temporária, implemente uma estratégia de repetição com uma espera exponencial. Após a primeira falha, comece com um atraso inicial de cinco segundos antes de tentar de novo.

Implemente uma estratégia com um número máximo de tentativas como uma condição de saída. Use um atraso exponencialmente aumentado a cada vez (10 s, 20 s etc.).

Acompanhar a interação do usuário durante a instalação do app ADP

Se o dispositivo precisar instalar o app ADP durante o prepareEnvironment, seu aplicativo poderá rastrear a interação do usuário implementando um NotificationReceiverService para receber notificações que substituem 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 conhecidos

Não há problemas conhecidos no momento.