# Manage custom apps with AMAPI

As an Android Management API based EMM, you can remotely manage custom
applications on devices. This includes both installing and uninstalling these
apps. This functionality is achieved by developing an extension app locally
using the AMAPI SDK.

> [!IMPORTANT]
> **Important:** The functionality is only supported in fully managed devices.

## Prerequisites

- Your extension app is integrated with the [AMAPI SDK](https://developers.google.com/android/management/extensibility-sdk-integration).
- The device is [fully managed](https://developers.google.com/android/work/overview#fully-managed-devices).
- AMAPI SDK v1.6.0-rc01 or higher is required.

## 1. Prepare your app for using the feature

### 1.1. Integrate with the AMAPI SDK in your extension app

The custom app management process requires you to integrate the AMAPI SDK in
your extension app. You can find more information about this library and how to
add it to your app in the [AMAPI SDK integration guide](https://developers.google.com/android/management/extensibility-sdk-integration).

### 1.2. Update your app's manifest to support [`FileProvider`](https://developers.google.com/reference/androidx/core/content/FileProvider)

- Add to your `AndroidManifest.xml` the `<queries>` element for the Android Device Policy (ADP) application as shown in the [AMAPI SDK integration guide](https://developers.google.com/android/management/extensibility-sdk-integration) .
- Implement the following `<provider>` snippet into your app's `AndroidManifest.xml` inside the `<application>` tag. This snippet is used to store files when sharing the custom app APK, enabling the installation of custom apps using AMAPI.

`AndroidManifest.xml`:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.customapp">
      <queries>
        <package android:name="com.google.android.apps.work.clouddpc" />
      </queries>

      <application>

        <!--This is used to store files when sharing the custom app apk.-->
        <provider
            android:name="com.google.android.managementapi.customapp.provider.CustomAppProvider"
            android:authorities="${applicationId}.AmapiCustomAppProvider"
            android:exported="false"
            android:grantUriPermissions="true">
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS"
              android:resource="@xml/file_provider_paths" />
        </provider>
      </application>
    </manifest>

- Create a new XML file in your app's `res/xml/` directory containing the storage path for custom apks.

`file_provider_paths.xml`:

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
      <cache-path
          name="android_managementapi_custom_apks"
          path="com.google.android.managementapi/customapp/apks/" />
    </paths>

## 2. Integrate with the custom app feature of the AMAPI SDK

### 2.1. Prepare the custom APK file for installation

Before deploying, the application's APK file must be prepared for installation.
The following code snippet demonstrates the process:

### Kotlin

```kotlin
import android.net.Uri
import androidx.core.net.Uri
import java.io.File
...
import com.google.android.managementapi.commands.LocalCommandClient
import com.google.android.managementapi.commands.LocalCommandClient.InstallCustomAppCommandHelper
import com.google.android.managementapi.commands.LocalCommandClientFactory

...

fun prepareApkFile(): Uri? {
    // Get the storage location of custom APK files from AM API
    val client: LocalCommandClient = LocalCommandClientFactory.create(context)
    val installCustomAppCommandHelper = client.installCustomAppCommandHelper

    val customApksStorageDir: File = installCustomAppCommandHelper.customApksStorageDirectory ?: return null

    // Once you get hold of the custom APKs storage directory, you must store your custom APK
    // in that location before issuing the install command.
    val customApkFile: File = fetchMyAppToDir(customApksStorageDir) ?: return null
    val customApkFileUri: Uri = customApkFile.toUri()

    return customApkFileUri
}
```

### Java

```java
import android.net.Uri;
import androidx.core.net.Uri;
import java.io.File;
...
import com.google.android.managementapi.commands.LocalCommandClient;
import com.google.android.managementapi.commands.LocalCommandClient.InstallCustomAppCommandHelper;
import com.google.android.managementapi.commands.LocalCommandClientFactory;

...

Uri prepareApkFile() {
  // Get the storage location of custom APK files from AM API
  LocalCommandClient client = LocalCommandClientFactory.create();
  InstallCustomAppCommandHelper installCustomAppCommandHelper = client.getInstallCustomAppCommandHelper();
  File customApksStorageDir = installCustomAppCommandHelper.getCustomApksStorageDirectory();

  // Once you get hold of the custom APKs storage directory, you must store your custom APK
  // in that location before issuing the install command.
  File customApkFile = fetchMyAppToDir(customApksStorageDir);
  Uri customApkFileUri = Uri.fromFile(customApkFile);
  ...
}
```

### 2.2. Issue a request to install a custom app

The following snippet shows how to issue a request to install a custom app:

### Kotlin

```kotlin
import android.content.Context
import android.net.Uri
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.InstallCustomApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.withContext
import java.lang.Exception

private const val TAG = "MyClass"

...

    // Requires a file URI of the APK file.
    fun issueInstallCustomAppCommand(packageName: String, fileUri: Uri) {
        coroutineScope.launch {
            try {
                withContext(coroutineScope.coroutineContext) {
                    val result: Command = LocalCommandClientFactory.create(context)
                        .issueCommand(createInstallCustomAppRequest(packageName, fileUri)).await()
                    // Process the returned command result here.
                    Log.i(TAG, "Successfully issued command: $result")
                }
            } catch (t: Exception) {
                Log.e(TAG, "Failed to issue command", t)
                // Handle the exception (e.g., show an error message)
            } finally {
                // Make sure to clean up the apk file after the command is executed.
                cleanUpApkFile(fileUri)
            }
        }
    }

    private fun createInstallCustomAppRequest(packageName: String, fileUri: Uri): IssueCommandRequest {
        return IssueCommandRequest.builder()
            .setInstallCustomApp(
                InstallCustomApp.builder()
                    .setPackageName(packageName)
                    .setPackageUri(fileUri.toString())
                    .build()
            )
            .build()
    }
}
```

### Java

```java
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;

...

  // Requires a file URI of the APK file.
  void issueInstallCustomAppCommand(String packageName, Uri fileUri) {
    Futures.addCallback(
        LocalCommandClientFactory.create(getContext())
            .issueCommand(createInstallCustomAppRequest(packageName, fileUri)),
        new FutureCallback() {
          @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 createInstallCustomAppRequest(String packageName, Uri fileUri) {
    return IssueCommandRequest.builder()
        .setInstallCustomApp(
            InstallCustomApp.builder()
                .setPackageName(packageName)
                .setPackageUri(fileUri.toString())
                .build()
        )
        .build();
  }
```

### 2.3. Issue a request to get installed apps

### Kotlin

```kotlin
import android.content.Context
import com.google.android.managementapi.device.DeviceClientFactory
import com.google.android.managementapi.device.model.GetDeviceRequest
import kotlinx.coroutines.guava.await

  suspend fun getInstalledApps(context: Context) =
    DeviceClientFactory.create(context)
      .getDevice(GetDeviceRequest.getDefaultInstance())
      .await()
      .getApplicationReports()
```

### Java

```java
import android.content.Context;
import com.google.android.managementapi.device.DeviceClientFactory;
import com.google.android.managementapi.device.model.GetDeviceRequest;
import com.google.android.managementapi.device.model.Device;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
import java.util.concurrent.Executor;

public ListenableFuture<List> getInstalledApps() {
        ListenableFuture deviceFuture =
            DeviceClientFactory.create(context)
                .getDevice(GetDeviceRequest.getDefaultInstance());

        return Futures.transform(
            deviceFuture,
            Device::getApplicationReports,
            executor // Use the provided executor
        );
    }
```

## 3. Provision the device with custom apps management policies

1. Set up a `policy` with the custom apps you intend to manage.

         {
           "statusReportingSettings": {
             "applicationReportsEnabled": true
           },
           "applications": [
           {
             "signingKeyCerts": [
               {
               "signingKeyCertFingerprintSha256": <sha256 signing key certificate hash value>
               }
             ],
             "packageName": "<emm_extensibility_app>",
             "installType": "AVAILABLE",
             "defaultPermissionPolicy": "GRANT",
             "extensionConfig": {
                 "notificationReceiver": "com.example.customapp.NotificationReceiverService"
             }
           },
           {
             "signingKeyCerts": [
               {
               "signingKeyCertFingerprintSha256": <sha256 signing key certificate hash value>
               },
             ],
             "packageName": "<custom_app>",
             "installType": "CUSTOM",
             "defaultPermissionPolicy": "GRANT",
             "customAppConfig": {
                 "userUninstallSettings": "DISALLOW_UNINSTALL_BY_USER"
             }
           }
           ]
         }
         ```

2. Create an enrollment token for the device by calling
   [enterprises.enrollmentTokens.create](https://developers.google.com/reference/rest/v1/enterprises.enrollmentTokens/create), with [`allowPersonalUsage`](https://developers.google.com/android/management/reference/rest/v1/AllowPersonalUsage)
   set to `PERSONAL_USAGE_DISALLOWED`.

3. [Provision](https://developers.google.com/android/management/provision-device) the device in [fully managed](https://developers.google.com/android/management/policies/fully-managed-devices) mode with the enrollment token.

4. Install your extensibility app from the Managed Play.

5. Your extensibility app:

   - can download the APK file of the custom app
   - can issue a request to install the custom app (refer to [code snippet](https://developers.google.com/android/management/manage-custom-apps#2_integrate_with_the_custom_app_feature_of_the_amapi_sdk) shown earlier)
   - should receive a response
6. If the custom app is removed from the policy, the installed sideloaded
   application won't get uninstalled if it conforms to [`playStoreMode`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#PlayStoreMode).

7. For more details, check [`CUSTOM`](https://developers.google.com/android/management/reference/rest/v1/enterprises.devices#ApplicationSource.ENUM_VALUES.CUSTOM) install type.

## API

### Server-client API

Refer to the new fields and enums listed:

- [`ApplicationPolicy.InstallType.CUSTOM`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#InstallType.ENUM_VALUES.CUSTOM)
- [`ApplicationPolicy.signingKeyCerts`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#ApplicationPolicy.FIELDS.signingKeyCerts)
- [`ApplicationPolicy.customAppConfig`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#ApplicationPolicy.FIELDS.customAppConfig)
- [`ApplicationPolicy.customAppConfig.UserUninstallSettings`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#CustomAppConfig.ENUM_VALUES.USER_UNINSTALL_SETTINGS)
- [`NonComplianceReason.APP_SIGNING_CERT_MISMATCH`](https://developers.google.com/android/management/reference/rest/v1/enterprises.policies#ApplicationPolicy.FIELDS.customAppConfig)
- [`Device.ApplicationReport.ApplicationSource.CUSTOM`](https://developers.google.com/android/management/reference/rest/v1/enterprises.devices#ApplicationSource.ENUM_VALUES.CUSTOM)