Android Management API 기반 EMM은 기기에서 맞춤 애플리케이션을 원격으로 관리할 수 있습니다. 여기에는 이러한 앱의 설치와 제거가 모두 포함됩니다. 이 기능은 AMAPI SDK를 사용하여 로컬에서 확장 앱을 개발하여 구현됩니다.
기본 요건
1. 기능 사용을 위한 앱 준비
1.1. 확장 프로그램 앱에서 AMAPI SDK와 통합
맞춤 앱 관리 프로세스에서는 확장 프로그램 앱에 AMAPI SDK를 통합해야 합니다. 이 라이브러리와 앱에 추가하는 방법에 관한 자세한 내용은 AMAPI SDK 통합 가이드를 참고하세요.
1.2. FileProvider
을 지원하도록 앱의 매니페스트 업데이트
- AMAPI SDK 통합 가이드에 표시된 대로 Android 기기 정책 (ADP) 애플리케이션의
<queries>
요소를AndroidManifest.xml
에 추가합니다. - 다음
<provider>
스니펫을<application>
태그 내 앱의AndroidManifest.xml
에 구현합니다. 이 스니펫은 맞춤 앱 APK를 공유할 때 파일을 저장하는 데 사용되며 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>
- 맞춤 APK의 저장소 경로가 포함된 새 XML 파일을 앱의
res/xml/
디렉터리에 만듭니다.
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. AMAPI SDK의 맞춤 앱 기능과 통합
2.1. 설치를 위해 맞춤 APK 파일 준비
배포하기 전에 애플리케이션의 APK 파일을 설치할 수 있도록 준비해야 합니다. 다음 코드 스니펫은 이 프로세스를 보여줍니다.
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 }
자바
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. 맞춤 앱 설치 요청 발행
다음 스니펫은 맞춤 앱 설치 요청을 발행하는 방법을 보여줍니다.
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() } }
자바
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. 설치된 앱을 가져오기 위한 요청을 발행합니다.
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. 맞춤 앱 관리 정책으로 기기 프로비저닝
관리하려는 맞춤 앱으로
policy
를 설정합니다.{ "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" } } ] } ```
allowPersonalUsage
이PERSONAL_USAGE_DISALLOWED
로 설정된 enterprises.enrollmentTokens.create를 호출하여 기기의 등록 토큰을 만듭니다.관리 Google Play에서 확장성 앱을 설치합니다.
확장성 앱:
- 맞춤 앱의 APK 파일을 다운로드할 수 있습니다.
- 맞춤 앱 설치 요청을 발행할 수 있습니다 (앞에 나온 코드 스니펫 참고).
- 응답을 받아야 합니다.
API
서버-클라이언트 API
나열된 새 필드와 enum을 참고하세요.