전용 기기 설명서

이 설명서는 개발자와 시스템 통합업체가 전용 기기 솔루션을 개선하는 데 도움이 됩니다. 방법 안내 레시피에 따라 전용 기기 동작을 위한 솔루션을 찾으세요. 이 설명서는 전용 기기 앱이 이미 있는 개발자에게 가장 적합합니다. 처음 시작하는 경우 전용 기기 개요를 참고하세요.

맞춤 Home 앱

이 레시피는 Android 홈 화면과 런처를 대체하는 앱을 개발하는 경우에 유용합니다.

홈 앱

앱을 기기의 홈 앱으로 설정하여 기기가 시작될 때 자동으로 실행되도록 할 수 있습니다. 잠금 작업 모드에서 허용 목록에 있는 앱을 포그라운드로 가져오는 홈 버튼을 사용 설정할 수도 있습니다.

모든 홈 앱은 CATEGORY_HOME 인텐트 카테고리를 처리합니다. 이는 시스템에서 홈 앱을 인식하는 방법입니다. 기본 홈 앱이 되려면 다음 예와 같이 DevicePolicyManager.addPersistentPreferredActivity()를 호출하여 앱의 활동 중 하나를 기본 홈 인텐트 핸들러로 설정합니다.

Kotlin

// Create an intent filter to specify the Home category.
val filter = IntentFilter(Intent.ACTION_MAIN)
filter.addCategory(Intent.CATEGORY_HOME)
filter.addCategory(Intent.CATEGORY_DEFAULT)

// Set the activity as the preferred option for the device.
val activity = ComponentName(context, KioskModeActivity::class.java)
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE)
        as DevicePolicyManager
dpm.addPersistentPreferredActivity(adminName, filter, activity)

Java

// Create an intent filter to specify the Home category.
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
filter.addCategory(Intent.CATEGORY_HOME);
filter.addCategory(Intent.CATEGORY_DEFAULT);

// Set the activity as the preferred option for the device.
ComponentName activity = new ComponentName(context, KioskModeActivity.class);
DevicePolicyManager dpm =
    (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
dpm.addPersistentPreferredActivity(adminName, filter, activity);

다음 XML 스니펫과 같이 앱 매니페스트 파일에서 여전히 인텐트 필터를 선언해야 합니다.

<activity
        android:name=".KioskModeActivity"
        android:label="@string/kiosk_mode"
        android:launchMode="singleInstance"
        android:excludeFromRecents="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.HOME"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

일반적으로 개요 화면에 런처 앱이 표시되는 것을 원하지 않습니다. 그러나 활동 선언에 excludeFromRecents를 추가할 필요는 없습니다. 시스템이 잠금 작업 모드로 실행 중일 때 Android의 런처는 처음 실행된 활동을 숨기기 때문입니다.

별도의 할 일 표시

각각의 새 작업이 개요 화면에서 별도의 항목으로 표시되므로 FLAG_ACTIVITY_NEW_TASK는 런처 유형 앱에 유용한 플래그일 수 있습니다. 개요 화면의 작업에 관한 자세한 내용은 최근 화면을 참고하세요.

공개 키오스크

이러한 레시피는 공용 공간에서 사용하지 않는 기기에도 유용하지만 많은 전용 기기 사용자가 작업에 집중하는 데 도움이 될 수도 있습니다.

기기 잠그기

기기가 의도된 용도로 사용되도록 하려면 표 1에 나열된 사용자 제한사항을 추가하면 됩니다.

표 1. 키오스크 기기의 사용자 제한사항
사용자 제한 설명
DISALLOW_FACTORY_RESET 기기 사용자가 기기를 초기화하지 못하게 합니다. 완전 관리형 기기의 관리자와 기본 사용자는 이 제한을 설정할 수 있습니다.
DISALLOW_SAFE_BOOT 기기 사용자가 시스템이 앱을 자동으로 실행하지 않는 안전 모드에서 기기를 시작하는 것을 방지합니다. 완전 관리형 기기 및 기본 사용자의 관리자는 이 제한을 설정할 수 있습니다.
DISALLOW_MOUNT_PHYSICAL_MEDIA 기기 사용자가 기기에 연결할 수 있는 저장소 볼륨을 마운트하지 못하게 합니다. 완전 관리형 기기의 관리자와 기본 사용자는 이 제한을 설정할 수 있습니다.
DISALLOW_ADJUST_VOLUME 기기를 음소거하고 기기 사용자가 사운드 볼륨 및 진동 설정을 변경하지 못하게 합니다. 키오스크에 미디어 재생 또는 접근성 기능을 위한 오디오가 필요하지 않은지 확인하세요. 완전 관리형 기기, 기본 사용자, 보조 사용자, 직장 프로필의 관리자는 이 제한을 설정할 수 있습니다.
DISALLOW_ADD_USER 기기 사용자가 보조 사용자 또는 제한된 사용자와 같은 신규 사용자를 추가하지 못하게 합니다. 시스템은 이 사용자 제한을 완전 관리형 기기에 자동으로 추가하지만 삭제되었을 수 있습니다. 완전 관리형 기기의 관리자와 기본 사용자는 이 제한을 설정할 수 있습니다.

다음 스니펫은 제한사항을 설정하는 방법을 보여줍니다.

Kotlin

// If the system is running in lock task mode, set the user restrictions
// for a kiosk after launching the activity.
arrayOf(
        UserManager.DISALLOW_FACTORY_RESET,
        UserManager.DISALLOW_SAFE_BOOT,
        UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
        UserManager.DISALLOW_ADJUST_VOLUME,
        UserManager.DISALLOW_ADD_USER).forEach { dpm.addUserRestriction(adminName, it) }

Java

// If the system is running in lock task mode, set the user restrictions
// for a kiosk after launching the activity.
String[] restrictions = {
    UserManager.DISALLOW_FACTORY_RESET,
    UserManager.DISALLOW_SAFE_BOOT,
    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
    UserManager.DISALLOW_ADJUST_VOLUME,
    UserManager.DISALLOW_ADD_USER};

for (String restriction: restrictions) dpm.addUserRestriction(adminName, restriction);

앱이 관리자 모드일 때는 IT 관리자가 기기 유지관리에 이러한 기능을 계속 사용할 수 있도록 이러한 제한을 삭제하는 것이 좋습니다. 제한을 해제하려면 DevicePolicyManager.clearUserRestriction()를 호출합니다.

오류 대화상자 숨기기

소매 데모 또는 공개 정보 디스플레이와 같은 일부 환경에서는 사용자에게 오류 대화상자를 표시하지 않는 것이 좋습니다. Android 9.0 (API 수준 28) 이상에서는 DISALLOW_SYSTEM_ERROR_DIALOGS 사용자 제한을 추가하여 비정상 종료되거나 응답하지 않는 앱의 시스템 오류 대화상자를 표시하지 않을 수 있습니다. 시스템은 기기 사용자가 대화상자에서 앱을 닫은 것처럼 응답하지 않는 앱을 다시 시작합니다. 다음의 예시는 그 방법을 나타냅니다.

Kotlin

override fun onEnabled(context: Context, intent: Intent) {
    val dpm = getManager(context)
    val adminName = getWho(context)

    dpm.addUserRestriction(adminName, UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS)
}

Java

public void onEnabled(Context context, Intent intent) {
  DevicePolicyManager dpm = getManager(context);
  ComponentName adminName = getWho(context);

  dpm.addUserRestriction(adminName, UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS);
}

기본 또는 보조 사용자의 관리자가 이 제한을 설정하면 시스템은 해당 사용자에 대해서만 오류 대화상자를 표시하지 않습니다. 완전 관리형 기기의 관리자가 이 제한을 설정하면 시스템은 모든 사용자의 대화상자를 숨깁니다.

화면을 켜진 상태로 유지

키오스크를 빌드하고 있다면 기기에서 앱 활동을 실행할 때 기기가 절전 모드로 전환되는 것을 중지할 수 있습니다. 다음 예와 같이 앱의 창에 FLAG_KEEP_SCREEN_ON 레이아웃 플래그를 추가합니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Keep the screen on and bright while this kiosk activity is running.
    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // Keep the screen on and bright while this kiosk activity is running.
  getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

기기가 AC나 USB, 무선 충전기에 연결되어 있는지 확인하는 것이 좋습니다. 배터리 변경 브로드캐스트를 등록하고 BatteryManager 값을 사용하여 충전 상태를 확인합니다. 기기의 연결이 끊긴 경우 IT 관리자에게 원격 알림을 보낼 수도 있습니다. 단계별 안내는 배터리 수준 및 충전 상태 모니터링을 참고하세요.

전원에 연결되어 있는 동안 기기가 켜진 상태를 유지하도록 STAY_ON_WHILE_PLUGGED_IN 전역 설정을 설정할 수도 있습니다. Android 6.0 (API 수준 23) 이상에서 완전 관리형 기기의 관리자는 다음 예와 같이 DevicePolicyManager.setGlobalSetting()를 호출할 수 있습니다.

Kotlin

val pluggedInto = BatteryManager.BATTERY_PLUGGED_AC or
        BatteryManager.BATTERY_PLUGGED_USB or
        BatteryManager.BATTERY_PLUGGED_WIRELESS
dpm.setGlobalSetting(adminName,
        Settings.Global.STAY_ON_WHILE_PLUGGED_IN, pluggedInto.toString())

Java

int pluggedInto = BatteryManager.BATTERY_PLUGGED_AC |
    BatteryManager.BATTERY_PLUGGED_USB |
    BatteryManager.BATTERY_PLUGGED_WIRELESS;
dpm.setGlobalSetting( adminName,
    Settings.Global.STAY_ON_WHILE_PLUGGED_IN, String.valueOf(pluggedInto));

앱 패키지

이 섹션에는 전용 기기에 앱을 효율적으로 설치하기 위한 레시피가 포함되어 있습니다.

앱 패키지 캐시

공유 기기의 사용자가 모두 공통 앱 집합을 공유하는 경우 가능한 한 앱을 다운로드하지 않는 것이 좋습니다. Android 9.0 (API 수준 28) 이상에서는 교대 근무자용 기기와 같이 고정된 사용자 집합이 있는 공유 기기에서 사용자 프로비저닝을 간소화하기 위해 멀티 사용자 세션에 필요한 앱 패키지 (APK)를 캐시할 수 있습니다.

캐시된 APK (기기에 이미 설치되어 있음)를 설치하는 작업은 두 단계로 이루어집니다.

  1. 완전 관리형 기기(또는 대리인(다음 참고))의 관리자 구성요소는 기기에 유지할 APK 목록을 설정합니다.
  2. 연결된 보조 사용자 (또는 대리인)의 관리자 구성요소는 사용자 대신 캐시된 APK를 설치할 수 있습니다. 완전 관리형 기기의 관리자, 기본 사용자, 연결된 직장 프로필 (또는 대리인)은 필요한 경우 캐시된 앱을 설치할 수도 있습니다.

기기에 유지할 APK 목록을 설정하기 위해 관리자는 DevicePolicyManager.setKeepUninstalledPackages()를 호출합니다. 이 메서드는 APK가 기기에 설치되어 있는지 확인하지 않으며, 사용자에게 앱이 필요하기 직전에 앱을 설치하려는 경우에 유용합니다. 이전에 설정한 패키지 목록을 가져오려면 DevicePolicyManager.getKeepUninstalledPackages()를 호출하면 됩니다. 변경사항이 있는 setKeepUninstalledPackages()를 호출한 후 또는 보조 사용자가 삭제되면 시스템은 더 이상 필요하지 않은 캐시된 APK를 모두 삭제합니다.

캐시된 APK를 설치하려면 DevicePolicyManager.installExistingPackage()를 호출합니다. 이 메서드는 시스템에서 이미 캐시한 앱만 설치할 수 있습니다. 전용 기기 솔루션 (또는 기기 사용자)이 먼저 기기에 앱을 설치해야 이 메서드를 호출할 수 있습니다.

다음 샘플은 완전 관리형 기기 및 보조 사용자의 관리자에서 이러한 API 호출을 사용하는 방법을 보여줍니다.

Kotlin

// Set the package to keep. This method assumes that the package is already
// installed on the device by managed Google Play.
val cachedAppPackageName = "com.example.android.myapp"
dpm.setKeepUninstalledPackages(adminName, listOf(cachedAppPackageName))

// ...

// The admin of a secondary user installs the app.
val success = dpm.installExistingPackage(adminName, cachedAppPackageName)

Java

// Set the package to keep. This method assumes that the package is already
// installed on the device by managed Google Play.
String cachedAppPackageName = "com.example.android.myapp";
List<String> packages = new ArrayList<String>();
packages.add(cachedAppPackageName);
dpm.setKeepUninstalledPackages(adminName, packages);

// ...

// The admin of a secondary user installs the app.
boolean success = dpm.installExistingPackage(adminName, cachedAppPackageName);

앱 위임

앱 캐싱을 관리하도록 다른 앱을 위임할 수 있습니다. 이렇게 하면 솔루션의 기능을 분리하거나 IT 관리자가 자체 앱을 사용할 수 있도록 할 수 있습니다. 위임 앱은 관리자 구성요소와 동일한 권한을 얻습니다. 예를 들어 보조 사용자의 관리자 권한을 위임받은 사용자는 installExistingPackage()는 호출할 수 있지만 setKeepUninstalledPackages()는 호출할 수 없습니다.

대리자를 만들려면 DevicePolicyManager.setDelegatedScopes()를 호출하고 범위 인수에 DELEGATION_KEEP_UNINSTALLED_PACKAGES를 포함합니다. 다음 예는 다른 앱을 대리자로 만드는 방법을 보여줍니다.

Kotlin

var delegatePackageName = "com.example.tools.kept_app_assist"

// Check that the package is installed before delegating.
try {
    context.packageManager.getPackageInfo(delegatePackageName, 0)
    dpm.setDelegatedScopes(
            adminName,
            delegatePackageName,
            listOf(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES))
} catch (e: PackageManager.NameNotFoundException) {
    // The delegate app isn't installed. Send a report to the IT admin ...
}

Java

String delegatePackageName = "com.example.tools.kept_app_assist";

// Check that the package is installed before delegating.
try {
  context.getPackageManager().getPackageInfo(delegatePackageName, 0);
  dpm.setDelegatedScopes(
      adminName,
      delegatePackageName,
      Arrays.asList(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES));
} catch (PackageManager.NameNotFoundException e) {
  // The delegate app isn't installed. Send a report to the IT admin ...
}

모든 것이 순조롭게 진행되면 위임 앱이 ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED 브로드캐스트를 수신하고 대리자가 됩니다. 앱은 마치 기기 소유자나 프로필 소유자인 것처럼 이 가이드의 메서드를 호출할 수 있습니다. DevicePolicyManager 메서드를 호출할 때 대리자는 관리자 구성요소 인수에 null를 전달합니다.

앱 패키지 설치

로컬에 캐시된 맞춤 앱을 전용 기기에 설치하는 것이 유용한 경우가 있습니다. 예를 들어 전용 기기는 대역폭이 제한된 환경이나 인터넷 연결이 없는 영역에 자주 배포됩니다. 전용 기기 솔루션은 고객의 대역폭을 고려해야 합니다. 앱에서 PackageInstaller 클래스를 사용하여 다른 앱 패키지 (APK) 설치를 시작할 수 있습니다.

모든 앱에서 APK를 설치할 수 있지만 완전 관리형 기기의 관리자는 사용자 상호작용 없이 패키지를 설치 (또는 제거)할 수 있습니다. 관리자는 기기, 연결된 보조 사용자 또는 연결된 직장 프로필을 관리할 수 있습니다. 설치가 완료되면 시스템은 모든 기기 사용자에게 표시되는 알림을 게시합니다. 이 알림은 관리자가 앱을 설치 (또는 업데이트)했음을 기기 사용자에게 알립니다.

표 2. 사용자 상호작용 없이 패키지 설치를 지원하는 Android 버전
Android 버전 설치 및 제거를 위한 관리 구성요소
Android 9.0 (API 수준 28) 이상 완전 관리형 기기에서 연결된 보조 사용자 및 직장 프로필
Android 6.0(API 수준 23) 이상 완전히 관리되는 기기

하나 이상의 APK 사본을 전용 기기에 배포하는 방법은 기기가 얼마나 멀리 떨어져 있는지, 기기 간의 거리가 얼마나 되는지에 따라 다릅니다. APK를 전용 기기에 설치하기 전에 솔루션이 보안 권장사항을 따라야 합니다.

PackageInstaller.Session를 사용하여 하나 이상의 APK를 설치를 위해 큐에 추가하는 세션을 만들 수 있습니다. 다음 예에서는 활동 (singleTop 모드)에서 상태 피드백을 수신하지만 서비스나 broadcast receiver를 사용할 수 있습니다.

Kotlin

// First, create a package installer session.
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
        PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)

// Add the APK binary to the session. The APK is included in our app binary
// and is read from res/raw but file storage is a more typical location.
// The I/O streams can't be open when installation begins.
session.openWrite("apk", 0, -1).use { output ->
    getContext().resources.openRawResource(R.raw.app).use { input ->
        input.copyTo(output, 2048)
    }
}

// Create a status receiver to report progress of the installation.
// We'll use the current activity.
// Here we're requesting status feedback to our Activity but this can be a
// service or broadcast receiver.
val intent = Intent(context, activity.javaClass)
intent.action = "com.android.example.APK_INSTALLATION_ACTION"
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val statusReceiver = pendingIntent.intentSender

// Start the installation. Because we're an admin of a fully managed device,
// there isn't any user interaction.
session.commit(statusReceiver)

Java

// First, create a package installer session.
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = packageInstaller.createSession(params);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);

// Add the APK binary to the session. The APK is included in our app binary
// and is read from res/raw but file storage is a more typical location.
try (
    // These I/O streams can't be open when installation begins.
    OutputStream output = session.openWrite("apk", 0, -1);
    InputStream input = getContext().getResources().openRawResource(R.raw.app);
) {
  byte[] buffer = new byte[2048];
  int n;
  while ((n = input.read(buffer)) >= 0) {
    output.write(buffer, 0, n);
  }
}

// Create a status receiver to report progress of the installation.
// We'll use the current activity.
// Here we're requesting status feedback to our Activity but this can be a
// service or broadcast receiver.
Intent intent = new Intent(context, getActivity().getClass());
intent.setAction("com.android.example.APK_INSTALLATION_ACTION");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
IntentSender statusReceiver = pendingIntent.getIntentSender();

// Start the installation. Because we're an admin of a fully managed device,
// there isn't any user interaction.
session.commit(statusReceiver);

세션은 인텐트를 사용하여 설치에 관한 상태 피드백을 전송합니다. 각 인텐트의 EXTRA_STATUS 필드를 확인하여 상태를 가져옵니다. 기기 사용자가 설치를 승인할 필요가 없으므로 관리자에게 STATUS_PENDING_USER_ACTION 상태 업데이트는 전송되지 않습니다.

앱을 제거하려면 PackageInstaller.uninstall를 호출합니다. 완전 관리형 기기, 사용자, 직장 프로필의 관리자는 지원되는 Android 버전을 실행하는 사용자 상호작용 없이 패키지를 제거할 수 있습니다 (표 2 참고).

시스템 업데이트 중지

Android 기기는 시스템 및 애플리케이션 소프트웨어의 무선 (OTA) 업데이트를 수신합니다. 휴일 또는 기타 사용량이 많은 시간과 같은 중요한 기간에 OS 버전을 고정하기 위해 전용 기기는 최대 90일 동안 OTA 시스템 업데이트를 정지할 수 있습니다. 자세한 내용은 시스템 업데이트 관리하기를 참고하세요.

원격 구성

Android의 관리 구성을 사용하면 IT 관리자가 앱을 원격으로 구성할 수 있습니다. IT 관리자에게 앱을 더 유용하게 만들기 위해 허용 목록이나 네트워크 호스트, 콘텐츠 URL과 같은 설정을 노출할 수 있습니다.

앱에서 구성을 노출하는 경우 문서에 설정을 포함해야 합니다. 앱 구성을 노출하고 설정 변경에 반응하는 방법을 자세히 알아보려면 관리 구성 설정을 참고하세요.

개발 설정

전용 기기용 솔루션을 개발하는 동안 초기화하지 않고 앱을 완전 관리형 기기의 관리자로 설정하는 것이 유용한 경우도 있습니다. 완전 관리형 기기의 관리자를 설정하려면 다음 단계를 따르세요.

  1. 기기에 기기 정책 컨트롤러 (DPC) 앱을 빌드하고 설치합니다.
  2. 기기에 계정이 없는지 확인합니다.
  3. Android 디버그 브리지 (adb) 셸에서 다음 명령어를 실행합니다. 예시의 com.example.dpc/.MyDeviceAdminReceiver을 앱의 관리자 구성요소 이름으로 바꿔야 합니다.

    adb shell dpm set-device-owner com.example.dpc/.MyDeviceAdminReceiver

고객이 솔루션을 배포할 수 있도록 지원하려면 기타 등록 방법을 살펴보아야 합니다. 전용 기기의 경우 QR 코드 등록을 권장합니다.

추가 리소스

전용 기기에 관한 자세한 내용은 다음 문서를 참고하세요.