Integrating with AMAPI SDK

The AMAPI SDK enables an EMM-specified extension app to communicate directly with Android Device Policy. Currently it includes support for local execution of Commands, and only the ClearAppData command. The following steps must be taken to integrate with the SDK:

  1. Add library to extension app.
  2. Make use of the provided APIs to issue commands as required.
  3. Add queries element, if target SDK >= 30.
  4. Optionally, you can provide implementation of service to listen to command status change callbacks.
  5. Provision device with extensibility policy.

Prerequisites

  • Ensure that extension app’s minSdkVersion is set to at least API level 21.

Adding library to the extension application

In your top-level build.gradle file add the Google Maven repository which contains the SDK library to the relevant modules and add the dependency to the library:

repositories {
  ...
  google()
}

Then add the library to your module's dependencies block:

dependencies {
  implementation 'com.google.android.libraries.enterprise.amapi:amapi:1.0.0'
}

Send requests to Android Device Policy

It should now be possible to send requests to ADP. The following requests are supported.

Issue Command

The 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. More information on it can be found in the Javadoc.

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 above 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

The extension app can also 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 sending a GetCommand request 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());
  }
  ...

Adding queries element

If your app targets SDK 30 and above, then queries element is needed in the manifest to specify that it will interact with ADP.

<queries>
    <package android:name="com.google.android.apps.work.clouddpc" />
</queries>

See Package visibility filtering on Android for more information.

Listen to Command status change callbacks

  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

 "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 testing allows to easily provide a testable implementation.

Integration testing

The following information will be needed to test with Android Device Policy:

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