Apps de extensão e comandos locais

O SDK da API Android Management (AMAPI) permite que um app de extensão especificado pelo EMM se comunique diretamente com o Android Device Policy (ADP) e execute Commands no dispositivo.

A página Integrar com o SDK da AMAPI fornece mais informações sobre essa biblioteca e como adicioná-la ao seu aplicativo.

Depois de integrar o SDK, seu app de extensão poderá se comunicar com o ADP para:

Emitir comando

Um app de extensão pode solicitar que comandos sejam emitidos usando o ADP. IssueCommandRequest contém o objeto de solicitação que terá detalhes sobre o comando a ser emitido e parâmetros específicos.

Limpar dados do app

O snippet a seguir mostra como emitir uma solicitação para limpar os dados do pacote:

import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
import com.google.android.managementapi.commands.model.Command;
import com.google.android.managementapi.commands.model.GetCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest.ClearAppsData;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void issueClearAppDataCommand(ImmutableList<String> packageNames) {
    Futures.addCallback(
        LocalCommandClientFactory.create(getContext())
            .issueCommand(createClearAppRequest(packageNames)),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            // Process the returned command result here
            Log.i(TAG, "Successfully issued command");
          }

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

  IssueCommandRequest createClearAppRequest(ImmutableList<String> packageNames) {
    return IssueCommandRequest.builder()
        .setClearAppsData(
            ClearAppsData.builder()
                .setPackageNames(packageNames)
                .build()
        )
        .build();
  }
...

O exemplo anterior mostra como emitir uma solicitação de limpeza de dados do app para pacotes especificados e aguardar até que o comando seja emitido. Se emitido, um objeto Command será retornado com o status do comando atual e o ID do comando, que pode ser usado posteriormente para consultar o status de qualquer comando de longa duração.

Excluir permanentemente o dispositivo ou o perfil de trabalho

Um app complementar pode emitir um comando Wipe local quando o admin de TI atribui o papel COMPANION_APP com o recurso extra LOCAL_DEVICE_WIPE_COMMAND_ALLOWED. Em dispositivos da empresa, isso redefine o dispositivo para as configurações de fábrica. Em dispositivos pessoais com um perfil de trabalho, ele remove o perfil de trabalho. Se o app não tiver esse papel e essa capacidade extra, a solicitação vai falhar. Para mais detalhes, consulte Configuração de políticas.

Os dois parâmetros Wipe são opcionais. Se você não quiser especificar um valor, omita o setter do builder ou transmita uma coleção ou string vazia (não transmita null).

  • WipeDataFlag controlam quais dados são excluídos permanentemente:
    • PRESERVE_RESET_PROTECTION_DATA: mantém os dados de proteção contra redefinição de fábrica, que são apagados por padrão. Aplicável apenas a dispositivos da empresa.
    • WIPE_EXTERNAL_STORAGE: também limpa o armazenamento externo, como cartões SD. Aplicável apenas a dispositivos da empresa.
    • WIPE_ESIMS: remove todos os eSIMs dos dispositivos da empresa. Em dispositivos pessoais, remove apenas os eSIMs gerenciados (no Android 16 ou mais recente, os eSIMs gerenciados são sempre removidos com o perfil de trabalho, independentemente de essa flag estar definida ou não).
  • wipeReason: uma mensagem curta, de até 200 caracteres, que é mostrada ao usuário quando o perfil de trabalho é removido de um dispositivo pessoal. Ela não afeta os dispositivos da empresa.

O snippet a seguir mostra como emitir uma solicitação para limpar o dispositivo ou o perfil de trabalho:

Kotlin

import android.content.Context
import android.util.Log
...
import com.google.android.managementapi.commands.CommandException
import com.google.android.managementapi.commands.LocalCommandClientFactory
import com.google.android.managementapi.commands.model.IssueCommandRequest
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe.WipeDataFlag

...
  suspend fun issueWipeCommand(
      context: Context,
      wipeDataFlags: List<WipeDataFlag>,
      wipeReason: String,
  ) {
    try {
      val command =
          LocalCommandClientFactory.create(context)
              .issueCommandAwait(createWipeRequest(wipeDataFlags, wipeReason))
      Log.i(TAG, "Successfully issued wipe command: ${command.commandId}")
    } catch (e: CommandException) {
      Log.e(TAG, "Failed to issue wipe command", e)
    }
  }

  fun createWipeRequest(
      wipeDataFlags: List<WipeDataFlag>,
      wipeReason: String,
  ): IssueCommandRequest =
      IssueCommandRequest.builder()
          .setWipe(
              Wipe.builder()
                  .setWipeDataFlags(wipeDataFlags)
                  .setWipeReason(wipeReason)
                  .build()
          )
          .build()
...

Java

import android.content.Context;
import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
import com.google.android.managementapi.commands.model.Command;
import com.google.android.managementapi.commands.model.IssueCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe;
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe.WipeDataFlag;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void issueWipeCommand(
      Context context,
      ImmutableList<WipeDataFlag> wipeDataFlags,
      String wipeReason) {
    Futures.addCallback(
        LocalCommandClientFactory.create(context)
            .issueCommand(createWipeRequest(wipeDataFlags, wipeReason)),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            Log.i(TAG, "Successfully issued wipe command: " + result.getCommandId());
          }

          @Override
          public void onFailure(Throwable t) {
            Log.e(TAG, "Failed to issue wipe command", t);
          }
        },
        MoreExecutors.directExecutor());
  }

  IssueCommandRequest createWipeRequest(
      ImmutableList<WipeDataFlag> wipeDataFlags, String wipeReason) {
    return IssueCommandRequest.builder()
        .setWipe(
            Wipe.builder()
                .setWipeDataFlags(wipeDataFlags)
                .setWipeReason(wipeReason)
                .build())
        .build();
  }
...

Receber comando

Um app de extensão pode consultar o status de solicitações de comando emitidas anteriormente. Para recuperar o status de um comando, você vai precisar do ID do comando (disponível na solicitação de comando de problema). O snippet a seguir mostra como enviar um GetCommandRequest para o ADP.

import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
...
import com.google.android.managementapi.commands.model.GetCommandRequest;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void getCommand(String commandId) {
    Futures.addCallback(
        LocalCommandClientFactory.create(getApplication())
            .getCommand(GetCommandRequest.builder().setCommandId(commandId).build()),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            // Process the returned command result here
            Log.i(Constants.TAG, "Successfully issued command");
          }

          @Override
          public void onFailure(Throwable t) {
            Log.e(Constants.TAG, "Failed to issue command", t);
          }
        },
        MoreExecutors.directExecutor());
  }
  ...

Detectar callbacks de mudança de status do comando

Um app de extensão pode registrar um callback para receber atualizações sobre mudanças de status de comandos de longa duração seguindo estas etapas:

  1. As mudanças de status do comando são notificadas para CommandListener. Implemente essa interface no app e forneça uma implementação sobre como processar as atualizações de status recebidas.
  2. Estenda NotificationReceiverService e forneça uma instância de CommandListener usando o método getCommandListener.
  3. Configure a política do aplicativo para atribuir o papel COMPANION_APP ao seu app (consulte Configuração da política).

    import com.google.android.managementapi.commands.CommandListener;
    import com.google.android.managementapi.notification.NotificationReceiverService;
    
    ...
    
    public class SampleCommandService extends NotificationReceiverService {
    
     @Override
     public CommandListener getCommandListener() {
       // return the concrete implementation from previous step
       return ...;
     }
    }
    

Configuração da política

Para permitir que o app complementar se comunique diretamente com o ADP, o EMM precisa atribuir o papel COMPANION_APP ao app usando o campo roles na política do aplicativo:

 "applications": [{
   "packageName": "com.amapi.extensibility.demo",
   "installType": "FORCE_INSTALLED",
   "roles": [
     { "roleType": "COMPANION_APP" }
   ]
 }]

Para permitir que o app complementar emita um comando Wipe local, defina extraCapabilities.localDeviceWipeCommand como LOCAL_DEVICE_WIPE_COMMAND_ALLOWED:

 "applications": [{
   "packageName": "com.amapi.extensibility.demo",
   "installType": "FORCE_INSTALLED",
   "roles": [
     {
       "roleType": "COMPANION_APP",
       "extraCapabilities": {
         "localDeviceWipeCommand": "LOCAL_DEVICE_WIPE_COMMAND_ALLOWED"
       }
     }
   ]
 }]

Para mais opções disponíveis, consulte Provisionar o dispositivo com políticas de papéis de apps.

O serviço que detecta mudanças no status do comando é descoberto automaticamente pelo ADP usando metadados no manifesto do app e não precisa mais ser especificado na política como notificationReceiver. Para saber como preparar o app e o manifesto, consulte Gerenciar papéis do aplicativo.

Teste

Teste de unidade

LocalCommandClient é uma interface e, portanto, permite fornecer uma implementação testável.

Teste de integração

As seguintes informações serão necessárias para testar com o ADP:

  1. Nome do pacote do app de extensão.
  2. O hash SHA-256 codificado em Base64 da assinatura associada ao pacote do app.
  3. Opcionalmente, se estiver testando o callback, o nome totalmente qualificado do serviço recém-introduzido para oferecer suporte ao callback. Nome totalmente qualificado de CommandService no exemplo.