AMAPI로 맞춤 앱 관리

Android Management API 기반 EMM은 기기에서 맞춤 애플리케이션을 원격으로 관리할 수 있습니다. 여기에는 이러한 앱의 설치와 제거가 모두 포함됩니다. 이 기능은 AMAPI SDK를 사용하여 로컬에서 확장 앱을 개발하여 구현됩니다.

기본 요건

  • 확장 프로그램 앱이 AMAPI SDK와 통합되어 있습니다.
  • 기기가 완전 관리형입니다.
  • AMAPI SDK v1.6.0-rc01 이상이 필요합니다.

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. 맞춤 앱 관리 정책으로 기기 프로비저닝

  1. 관리하려는 맞춤 앱으로 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"
          }
        }
        ]
      }
      ```
    
  2. allowPersonalUsagePERSONAL_USAGE_DISALLOWED로 설정된 enterprises.enrollmentTokens.create를 호출하여 기기의 등록 토큰을 만듭니다.

  3. 등록 토큰을 사용하여 기기를 완전 관리형 모드로 프로비저닝합니다.

  4. 관리 Google Play에서 확장성 앱을 설치합니다.

  5. 확장성 앱:

    • 맞춤 앱의 APK 파일을 다운로드할 수 있습니다.
    • 맞춤 앱 설치 요청을 발행할 수 있습니다 (앞에 나온 코드 스니펫 참고).
    • 응답을 받아야 합니다.

API

서버-클라이언트 API

나열된 새 필드와 enum을 참고하세요.