Apps de extensiones y comandos locales

El SDK de la API de Android Management (AMAPI) permite que una app de extensión especificada por el EMM se comunique directamente con Android Device Policy (ADP) y ejecute Commands en el dispositivo.

En Cómo realizar la integración con el SDK de AMAPI, se proporciona más información sobre esta biblioteca y cómo agregarla a tu aplicación.

Una vez que se integra el SDK, tu app de extensión puede comunicarse con ADP para lo siguiente:

Emisión de comandos

Una app de extensión puede solicitar que se emitan comandos con ADP. IssueCommandRequest contiene el objeto de solicitud que incluirá detalles sobre el comando que se emitirá y los parámetros específicos.

Borra los datos de apps

En el siguiente fragmento, se muestra cómo emitir una solicitud para borrar los datos del paquete:

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

En el ejemplo anterior, se muestra cómo emitir una solicitud de borrado de datos de app para los paquetes especificados y esperar hasta que el comando se haya emitido correctamente. Si se emite correctamente, se devolverá un objeto Command con el estado actual del comando y el ID del comando, que se puede usar más adelante para consultar el estado de cualquier comando de larga duración.

Borra el dispositivo o el perfil de trabajo

Una app complementaria puede emitir un comando Wipe local cuando el administrador de TI asigna el rol COMPANION_APP con la capacidad adicional LOCAL_DEVICE_WIPE_COMMAND_ALLOWED. En los dispositivos de la empresa, se restablece la configuración de fábrica; en los dispositivos personales con un perfil de trabajo, se quita el perfil de trabajo. Si la app no tiene este rol ni la capacidad adicional, la solicitud falla. Para obtener más detalles, consulta Configuración de la política.

Ambos parámetros Wipe son opcionales. Si no quieres especificar un valor, omite el método set del compilador o pasa una colección vacía o una cadena vacía (no pases null).

  • Los valores WipeDataFlag controlan qué datos se borran:
    • PRESERVE_RESET_PROTECTION_DATA: Conserva los datos de la protección contra el restablecimiento de la configuración de fábrica, que se borran de forma predeterminada. Solo se aplica a los dispositivos de la empresa.
    • WIPE_EXTERNAL_STORAGE: También borra el almacenamiento externo, como las tarjetas SD. Solo se aplica a los dispositivos de la empresa.
    • WIPE_ESIMS: Quita todas las eSIM de los dispositivos de la empresa. En los dispositivos de propiedad personal, solo se quitan las eSIM administradas (en Android 16 o versiones posteriores, las eSIM administradas siempre se quitan con el perfil de trabajo, independientemente de si se establece este parámetro o no).
  • wipeReason: Es un mensaje breve, de hasta 200 caracteres, que se muestra al usuario cuando se quita el perfil de trabajo de un dispositivo personal. No tiene ningún efecto en los dispositivos de la empresa.

En el siguiente fragmento, se muestra cómo emitir una solicitud para borrar los datos del dispositivo o del perfil de trabajo:

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

Obtener comando

Una app de extensión puede consultar el estado de las solicitudes de comandos emitidas anteriormente. Para recuperar el estado de un comando, necesitarás el ID del comando (disponible en la solicitud de emisión del comando). En el siguiente fragmento, se muestra cómo enviar un GetCommandRequest a 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());
  }
  ...

Escucha las devoluciones de llamada de cambio de estado de Command

De manera opcional, una app de extensión puede registrar una devolución de llamada para recibir actualizaciones de los cambios de estado de los comandos de larga duración siguiendo estos pasos:

  1. Los cambios de estado de los comandos se notifican a CommandListener. Implementa esta interfaz en tu app y proporciona una implementación sobre cómo controlar las actualizaciones de estado recibidas.
  2. Extiende NotificationReceiverService y proporciona una instancia de CommandListener a través del método getCommandListener.
  3. Configura la política de la aplicación para asignar el rol COMPANION_APP a tu app (consulta Configuración de la 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 ...;
     }
    }
    

Configuración de la política

Para permitir que la app complementaria se comunique directamente con ADP, la EMM debe asignar el rol COMPANION_APP a la app con el campo roles en la política de la aplicación:

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

Para permitir que la app complementaria emita un comando Wipe local, establece extraCapabilities.localDeviceWipeCommand en 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 conocer las opciones adicionales disponibles, consulta Aprovisiona el dispositivo con políticas de roles de la app.

ADP detecta automáticamente el servicio que escucha los cambios de estado de los comandos con los metadatos del manifiesto de la app, y ya no es necesario especificarlo en la política como notificationReceiver. Para obtener detalles sobre cómo preparar tu app y el manifiesto, consulta Administra los roles de la aplicación.

Pruebas

Pruebas de unidades

LocalCommandClient es una interfaz y, por lo tanto, permite proporcionar una implementación comprobable.

Pruebas de integración

Para realizar pruebas con ADP, se necesitará la siguiente información:

  1. Es el nombre del paquete de la app de extensión.
  2. Es el hash SHA-256 codificado en Base64 de la firma asociada con el paquete de la app.
  3. De manera opcional, si se prueba la devolución de llamada, el nombre completamente calificado del servicio del servicio recién introducido para admitir la devolución de llamada. (nombre completamente calificado de CommandService en el ejemplo).