The Android Management API (AMAPI) SDK enables an EMM-specified extension app to
communicate directly with Android Device Policy (ADP) and execute [`Commands`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/package-summary)
on the device.

> [!NOTE]
> **Note:** The [`ClearAppsData`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/IssueCommandRequest.ClearAppsData) and [`Wipe`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/IssueCommandRequest.Wipe) commands are supported, as well as custom app commands. For details, see [Manage custom apps with AMAPI](https://developers.google.com/android/management/manage-custom-apps).

[Integrate with the AMAPI SDK](https://developers.google.com/android/management/sdk-integration) provides more information about
this library and how to add it to your application.

Once integrated the SDK, your extension app can communicate with ADP to:

- [send command requests](https://developers.google.com/android/management/sdk-local-commands#issue_command)
- [query the status of command requests](https://developers.google.com/android/management/sdk-local-commands#get_command)
- [receive command status changes](https://developers.google.com/android/management/sdk-local-commands#listen_to_callbacks)

## Issue Command

An extension app can request for commands to be issued using ADP.
[`IssueCommandRequest`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/IssueCommandRequest) contains the request object that will contain detail on
the command to be issued and specific parameters.

### Clear app data

The following snippet shows how to issue a request to clear the package's data:

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

The earlier example shows issuing a clear app data request for specified
packages and waiting until the command has been successfully issued. If
successfully issued, a `Command` object will be returned with the current
command status and the command ID which can later be used to query the status of
any long running commands.

### Wipe the device or work profile

A companion app can issue a local [`Wipe`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/IssueCommandRequest.Wipe) command when the IT admin assigns
the `COMPANION_APP` role with the [`LOCAL_DEVICE_WIPE_COMMAND_ALLOWED`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/common/model/Role.ExtraCapabilities.LocalDeviceWipeCommand#LOCAL_DEVICE_WIPE_COMMAND_ALLOWED) extra
capability. On company-owned devices, this factory-resets the device; on
personally-owned devices with a work profile, it removes the work profile. If
the app doesn't have this role and extra capability, the request fails. For
details, see [Policy Configuration](https://developers.google.com/android/management/sdk-local-commands#policy).

> [!NOTE]
> **Note:** The local `Wipe` command requires [AMAPI SDK 1.9.0](https://developers.google.com/android/management/sdk-release-notes#version-1.9.0) or higher.

Both `Wipe` parameters are optional. If you don't want to specify a value, omit
the builder setter or pass an empty collection or empty string (don't pass
`null`).

- [`WipeDataFlag`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/IssueCommandRequest.Wipe.WipeDataFlag) values control what data is wiped:
  - `PRESERVE_RESET_PROTECTION_DATA`: Keeps the factory reset protection data, which is wiped by default. Applies only to company-owned devices.
  - `WIPE_EXTERNAL_STORAGE`: Also wipes external storage, such as SD cards. Applies only to company-owned devices.
  - `WIPE_ESIMS`: Removes all eSIMs from company-owned devices. On personally-owned devices, removes only managed eSIMs (on Android 16 or higher, managed eSIMs are always removed with the work profile, whether this flag is set or not).
- `wipeReason`: A short message, up to 200 characters, that's shown to the user when the work profile is removed from a personally-owned device. It has no effect on company-owned devices.

The following snippet shows how to issue a request to wipe the device or work
profile:

<br />

### Kotlin

<br />

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

<br />

### Java

<br />

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

<br />

<br />

> [!NOTE]
> **Note:** If ADP accepts the request, the returned `Command` is in the `PENDING` state and the wipe runs asynchronously. Because the wipe also removes your app from the device or work profile, don't rely on [command status callbacks](https://developers.google.com/android/management/sdk-local-commands#listen_to_callbacks) or [`GetCommandRequest`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/GetCommandRequest) to confirm that the wipe completed.

## Get Command

An extension app can query the status of previously issued command requests. To
retrieve a command's status, you will need the command ID (available from the
issue command request). The following snippet shows how to send a
[`GetCommandRequest`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/model/GetCommandRequest) to 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());
      }
      ...

## Listen to Command status change callbacks

An extension app can optionally register a callback to receive updates for
status changes of long running commands following these steps:

1. Command status changes are notified to [`CommandListener`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/CommandListener), implement this interface in your app and provide implementation on how to handle the received status updates.
2. Extend [`NotificationReceiverService`](https://developers.google.com/android/management/sdk-integration#notification_receiver_service) and provide a `CommandListener` instance through the `getCommandListener` method.
3. Configure the application policy to assign the `COMPANION_APP` role to your
   app (see [Policy Configuration](https://developers.google.com/android/management/sdk-local-commands#policy)).

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

## Policy Configuration

To enable the companion app to communicate directly with ADP, the EMM has to
assign the `COMPANION_APP` role to the app using the `roles` field in the
application policy:

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

To allow the companion app to issue a local [`Wipe`](https://developers.google.com/android/management/sdk-local-commands#wipe_device) command, set
[`extraCapabilities.localDeviceWipeCommand`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#Role.FIELDS.extra_capabilities) to
`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"
           }
         }
       ]
     }]

For additional options available see
[Provision the device with app roles policies](https://developers.google.com/android/management/app-roles#provision_the_device_with_app_roles_policies).

The service that listens for command status changes is automatically discovered
by ADP using metadata in the app's manifest, and no longer needs to be
specified in the policy as `notificationReceiver`. For details on how to
prepare your app and manifest, see [Manage application roles](https://developers.google.com/android/management/app-roles).

> [!WARNING]
> **Deprecated:** The [`extensionConfig`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#ExtensionConfig) field is deprecated and shouldn't be used.

### Testing

#### Unit testing

[`LocalCommandClient`](https://developers.google.com/android/management/reference/amapi/com/google/android/managementapi/commands/LocalCommandClient) is an interface and thus allows to provide a testable
implementation.

#### Integration testing

The following information will be needed to test with ADP:

1. Package name of the extension app.
2. The base64-encoded SHA-256 hash of the Signature associated with the app package.
3. Optionally, if testing callback - fully qualified name of the service from the newly introduced service to support callback. (Fully qualified name of `CommandService` in the example).