Extension apps and local commands

The Android Management API (AMAPI) SDK enables an EMM-specified extension app to communicate directly with Android Device Policy (ADP) and execute Commands on the device.

Integrate with the AMAPI SDK 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:

Issue Command

An extension app can request for commands to be issued using ADP. IssueCommandRequest contains the request object that will contain detail on the command to be issued and specific parameters.

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.

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 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 register a callback to receive updates for status changes of long running commands following these steps:

  1. Command status changes are notified to CommandListener, implement this interface in your app and provide implementation on how to handle the received status updates.
  2. Extend NotificationReceiverService and provide CommandListener instance.
  3. Specify class name of extended NotificationReceiverService in Android Management API policy (see Policy Configuration).

    import com.google.android.managementapi.commands.CommandListener;
    import com.google.android.managementapi.notification.NotificationReceiverService;
    
    ...
    
    public class SampleCommandService extends NotificationReceiverService {
    
      @Override
      protected void setupInjection() {
        // (Optional) If using DI and needs initialisation then use this method.
      }
    
      @Override
      public CommandListener getCommandListener() {
        // return the concrete implementation from previous step
        return ...;
      }
    }
    
  4. Add the service to your AndroidManifest.xml and ensure that it is exported.

    <service
     android:name = ".notification.SampleCommandService"
     android:exported = "true" />
    

Policy Configuration

To enable the extension app to communicate directly with ADP, the EMM has to provide an extensionConfig policy.

 "applications": [{
   "packageName": "com.amapi.extensibility.demo",
   ...
   "extensionConfig": {
     "signingKeyFingerprintsSha256": [
       // Include signing key of extension app
     ],
     // Optional if callback is implemented
     "notificationReceiver": "com.amapi.extensibility.demo.notification.SampleCommandService"
   }
 }]

Testing

Unit testing

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