Приложения-расширения и локальные команды

Android Management API (AMAPI) SDK позволяет указанному EMM приложению-расширению напрямую взаимодействовать с Android Device Policy (ADP) и выполнять Commands на устройстве.

Подробнее о том, как интегрировать AMAPI SDK…

После интеграции SDK приложение-расширение сможет взаимодействовать с ADP, чтобы:

Выполнить команду

Приложение-расширение может запрашивать выполнение команд с помощью ADP. IssueCommandRequest содержит объект запроса, в котором будут указаны подробные сведения о команде и ее параметрах.

Удалить данные приложений

В следующем фрагменте кода показано, как отправить запрос на удаление данных пакета:

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

В примере выше показано, как отправить запрос на удаление данных приложения указанных пакетов и дождаться его выполнения. Если запрос будет выполнен успешно, возвратится объект Command с текущим статусом команды и ее идентификатором, который можно использовать для запроса статуса любых длительных команд.

Удаление данных с устройства или из рабочего профиля

Сопутствующее приложение может выполнить локальную команду Wipe, если системный администратор назначит роль COMPANION_APP с дополнительной функцией LOCAL_DEVICE_WIPE_COMMAND_ALLOWED. На корпоративных устройствах это приведет к сбросу настроек, а на личных устройствах с рабочим профилем – к удалению рабочего профиля. Если у приложения нет этой роли и дополнительной возможности, запрос завершится ошибкой. Подробнее о настройке правил…

Оба параметра Wipe необязательны. Если вы не хотите указывать значение, не используйте сеттер или передайте пустую коллекцию или строку (не передавайте null).

  • Значения WipeDataFlag определяют, какие данные будут удалены:
    • PRESERVE_RESET_PROTECTION_DATA – сохраняет данные защиты после сброса настроек, которые по умолчанию удаляются. Применяется только к корпоративным устройствам.
    • WIPE_EXTERNAL_STORAGE – также удаляет данные с внешних носителей, например SD-карт. Применяется только для корпоративных устройств.
    • WIPE_ESIMS – удаляет все eSIM с корпоративных устройств. На личных устройствах удаляются только управляемые eSIM (на Android 16 и более поздних версиях управляемые eSIM всегда удаляются вместе с рабочим профилем, независимо от того, установлен ли этот флаг).
  • wipeReason – короткое сообщение (до 200 символов), которое показывается пользователю при удалении рабочего профиля с личного устройства. Оно не влияет на корпоративные устройства.

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

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

Получить команду

Приложение-расширение может запрашивать статус ранее отправленных команд. Чтобы получить статус команды, вам понадобится ее идентификатор (доступен в запросе на выполнение команды). В следующем фрагменте кода показано, как отправить GetCommandRequest в 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());
  }
  ...

Как отслеживать обратные вызовы об изменении статуса команды

Приложение-расширение может зарегистрировать обратный вызов, чтобы получать обновления об изменении статуса долго выполняющихся команд. Для этого сделайте следующее:

  1. Об изменении статуса команды сообщается в CommandListener. Реализуйте этот интерфейс в своем приложении и укажите, как обрабатывать полученные обновления статуса.
  2. Расширьте NotificationReceiverService и предоставьте экземпляр CommandListener с помощью метода getCommandListener.
  3. Настройте правило приложения, чтобы назначить ему роль COMPANION_APP (см. раздел Настройка правила).

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

Настройка правил

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

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

Чтобы разрешить сопутствующему приложению выполнять локальную команду Wipe, задайте для правила extraCapabilities.localDeviceWipeCommand значение 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"
       }
     }
   ]
 }]

Дополнительные варианты описаны в статье Как настроить устройство с помощью правил ролей приложений.

Сервис, который отслеживает изменения статуса команд, автоматически обнаруживается ADP с помощью метаданных в манифесте приложения. Его больше не нужно указывать в правилах как notificationReceiver. Подробнее о том, как подготовить приложение и манифест…

Тестирование

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

LocalCommandClient – это интерфейс, который позволяет использовать тестируемую реализацию.

Тестирование интеграции

Для тестирования с ADP потребуется следующая информация:

  1. Название пакета приложения-расширения.
  2. Хеш SHA-256 в кодировке Base64 для подписи, связанной с пакетом приложения.
  3. Если вы тестируете обратный вызов, укажите полное название сервиса, который его поддерживает. (в примере – полное имя CommandService).