Page Summary
-
The Android Management API SDK allows an extension app to communicate with Android Device Policy and execute commands, currently supporting only the
ClearAppDatacommand. -
After integration, an extension app can send command requests, query their status, and receive status change notifications.
-
To issue a command, an extension app uses an
IssueCommandRequestcontaining details about the command and its parameters. -
The status of a previously issued command request can be queried using the command ID returned after successfully issuing the command.
-
Extension apps can optionally register a
CommandListenerthrough an extendedNotificationReceiverServiceto receive updates for long-running command status changes. -
Enabling direct communication between the extension app and ADP requires configuring an
extensionConfigpolicy, including the app's signing key fingerprints and the optional notification receiver.
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.
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 command when the IT admin assigns
the COMPANION_APP role with the 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.
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).
WipeDataFlagvalues 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:
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();
}
...
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 optionally register a callback to receive updates for status changes of long running commands following these steps:
- Command status changes are notified to
CommandListener, implement this interface in your app and provide implementation on how to handle the received status updates. - Extend
NotificationReceiverServiceand provide aCommandListenerinstance through thegetCommandListenermethod. Configure the application policy to assign the
COMPANION_APProle to your app (see Policy Configuration).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 command, set
extraCapabilities.localDeviceWipeCommand 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.
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.
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:
- Package name of the extension app.
- The base64-encoded SHA-256 hash of the Signature associated with the app package.
- Optionally, if testing callback - fully qualified name of the service from
the newly introduced service to support callback. (Fully qualified name of
CommandServicein the example).