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.

Prerequisites

  • Your extension app is integrated with the AMAPI SDK .
  • The device is fully managed .
  • 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 .

1.2. Update your app's manifest to support FileProvider

  • Add to your AndroidManifest.xml the <queries> element for the Android Device Policy (ADP) application as shown in the AMAPI SDK integration guide .
  • 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

 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

 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

 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

 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

 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

 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" 
     , 
      
     "lockTaskAllowed" 
     : 
      
     true 
     , 
      
     "defaultPermissionPolicy" 
     : 
      
     "GRANT" 
     , 
      
     "extensionConfig" 
     : 
      
     { 
      
     "notificationReceiver" 
     : 
      
     "com.example.customapp.NotificationReceiverService" 
      
     } 
      
     }, 
      
     { 
      
     "signingKeyCerts" 
     : 
      
     [ 
      
     { 
      
     "signingKeyCertFingerprintSha256" 
     : 
      
    < sha256 
      
     signing 
      
     key 
      
     certificate 
      
     hash 
      
     value 
    >  
     }, 
      
     ], 
      
     "packageName" 
     : 
      
     "<custom_app>" 
     , 
      
     "installType" 
     : 
      
     "CUSTOM" 
     , 
      
     "lockTaskAllowed" 
     : 
      
     true 
     , 
      
     "defaultPermissionPolicy" 
     : 
      
     "GRANT" 
     , 
      
     "customAppConfig" 
     : 
      
     { 
      
     "userUninstallSettings" 
     : 
      
     "DISALLOW_UNINSTALL_BY_USER" 
      
     } 
      
     } 
      
     ] 
      
     } 
      
     ``` 
     
    
  2. Create an enrollment token for the device by calling enterprises.enrollmentTokens.create , with allowPersonalUsage set to PERSONAL_USAGE_DISALLOWED .

  3. Provision the device in fully managed 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 shown earlier)
    • should receive a response

API

Server-client API

Refer to the new fields and enums listed:

Create a Mobile Website
View Site in Mobile | Classic
Share by: