여러 사용자 관리하기

이 개발자 가이드에서는 기기 정책 컨트롤러 (DPC)가 전용 기기에서 여러 Android 사용자를 관리하는 방법을 설명합니다.

개요

DPC를 사용하면 여러 사용자가 하나의 전용 기기를 공유할 수 있습니다. 완전 관리형 기기에서 실행되는 DPC는 두 가지 유형의 사용자를 만들고 관리할 수 있습니다.

  • 보조 사용자는 세션 간에 별도의 앱과 데이터가 저장된 Android 사용자입니다. 관리자는 관리자 구성요소를 사용하여 사용자를 관리합니다. 이러한 사용자는 배달 기사나 보안 직원과 같이 교대 근무 시작 시 기기를 픽업하는 경우에 유용합니다.
  • 임시 사용자는 사용자가 중지 또는 전환하거나 기기를 재부팅할 때 시스템에서 삭제하는 보조 사용자입니다. 이러한 사용자는 공개 액세스 인터넷 키오스크와 같이 세션이 완료된 후 데이터를 삭제할 수 있는 경우에 유용합니다.

기존 DPC를 사용하여 전용 기기 및 보조 사용자를 관리합니다. DPC의 관리자 구성요소는 새로운 보조 사용자를 생성할 때 스스로를 이 사용자의 관리자로 설정합니다.

기본 사용자 및 보조 사용자 두 명.
그림 1. 관리자가 동일한 DPC에서 관리하는 기본 및 보조 사용자

보조 사용자의 관리자는 완전 관리형 기기의 관리자와 동일한 패키지에 속해야 합니다. 개발을 간소화하려면 기기와 보조 사용자 간에 관리자를 공유하는 것이 좋습니다.

전용 기기에서 여러 사용자를 관리하려면 일반적으로 Android 9.0이 필요하지만 이 개발자 가이드에서 사용된 일부 메서드는 이전 버전의 Android에서 사용할 수 있습니다.

사용자 만들기

DPC는 백그라운드에서 추가 사용자를 만든 다음 포그라운드로 전환할 수 있습니다. 이 프로세스는 보조 사용자와 임시 사용자 모두 거의 동일합니다. 완전 관리형 기기 및 보조 사용자의 관리자에서 다음 단계를 구현합니다.

  1. DevicePolicyManager.createAndManageUser()을 호출합니다. 임시 사용자를 만들려면 플래그 인수에 MAKE_USER_EPHEMERAL를 포함합니다.
  2. DevicePolicyManager.startUserInBackground()를 호출하여 백그라운드에서 사용자를 시작합니다. 사용자가 달리기를 시작하지만, 사용자를 포그라운드로 가져와 기기 사용자에게 표시하기 전에 설정을 완료하는 것이 좋습니다.
  3. 보조 사용자의 관리자에서 DevicePolicyManager.setAffiliationIds()를 호출하여 신규 사용자를 기본 사용자와 연결합니다. 아래의 DPC 조정을 참고하세요.
  4. 완전 관리형 기기의 관리자로 돌아가서 DevicePolicyManager.switchUser()를 호출하여 사용자를 포그라운드로 전환합니다.

다음 샘플은 DPC에 1단계를 추가하는 방법을 보여줍니다.

Kotlin

val dpm = getContext().getSystemService(Context.DEVICE_POLICY_SERVICE)
        as DevicePolicyManager

// If possible, reuse an existing affiliation ID across the
// primary user and (later) the ephemeral user.
val identifiers = dpm.getAffiliationIds(adminName)
if (identifiers.isEmpty()) {
    identifiers.add(UUID.randomUUID().toString())
    dpm.setAffiliationIds(adminName, identifiers)
}

// Pass an affiliation ID to the ephemeral user in the admin extras.
val adminExtras = PersistableBundle()
adminExtras.putString(AFFILIATION_ID_KEY, identifiers.first())
// Include any other config for the new user here ...

// Create the ephemeral user, using this component as the admin.
try {
    val ephemeralUser = dpm.createAndManageUser(
            adminName,
            "tmp_user",
            adminName,
            adminExtras,
            DevicePolicyManager.MAKE_USER_EPHEMERAL or
                    DevicePolicyManager.SKIP_SETUP_WIZARD)

} catch (e: UserManager.UserOperationException) {
    if (e.userOperationResult ==
            UserManager.USER_OPERATION_ERROR_MAX_USERS) {
        // Find a way to free up users...
    }
}

Java

DevicePolicyManager dpm = (DevicePolicyManager)
    getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);

// If possible, reuse an existing affiliation ID across the
// primary user and (later) the ephemeral user.
Set<String> identifiers = dpm.getAffiliationIds(adminName);
if (identifiers.isEmpty()) {
  identifiers.add(UUID.randomUUID().toString());
  dpm.setAffiliationIds(adminName, identifiers);
}

// Pass an affiliation ID to the ephemeral user in the admin extras.
PersistableBundle adminExtras = new PersistableBundle();
adminExtras.putString(AFFILIATION_ID_KEY, identifiers.iterator().next());
// Include any other config for the new user here ...

// Create the ephemeral user, using this component as the admin.
try {
  UserHandle ephemeralUser = dpm.createAndManageUser(
      adminName,
      "tmp_user",
      adminName,
      adminExtras,
      DevicePolicyManager.MAKE_USER_EPHEMERAL |
          DevicePolicyManager.SKIP_SETUP_WIZARD);

} catch (UserManager.UserOperationException e) {
  if (e.getUserOperationResult() ==
      UserManager.USER_OPERATION_ERROR_MAX_USERS) {
    // Find a way to free up users...
  }
}

새 사용자를 만들거나 시작할 때 UserOperationException 예외를 포착하고 getUserOperationResult()를 호출하여 실패 이유를 확인할 수 있습니다. 사용자 한도를 초과하는 일반적인 실패 이유는 다음과 같습니다.

사용자를 만드는 데 다소 시간이 걸릴 수 있습니다. 사용자를 자주 만드는 경우 바로 백그라운드에서 실행 가능한 사용자를 준비시켜 사용자 환경을 개선할 수 있습니다. 바로 사용할 수 있는 사용자의 장점과 기기에 허용되는 최대 사용자 수의 균형을 맞춰야 할 수도 있습니다.

정체성

신규 사용자를 만든 후에는 영구 일련번호로 사용자를 참조해야 합니다. UserHandle는 사용자를 만들고 삭제할 때 시스템에서 재활용하므로 유지하지 마세요. UserManager.getSerialNumberForUser()를 호출하여 일련번호를 가져옵니다.

Kotlin

// After calling createAndManageUser() use a device-unique serial number
// (that isn’t recycled) to identify the new user.
secondaryUser?.let {
    val userManager = getContext().getSystemService(UserManager::class.java)
    val ephemeralUserId = userManager!!.getSerialNumberForUser(it)
    // Save the serial number to storage  ...
}

Java

// After calling createAndManageUser() use a device-unique serial number
// (that isn’t recycled) to identify the new user.
if (secondaryUser != null) {
  UserManager userManager = getContext().getSystemService(UserManager.class);
  long ephemeralUserId = userManager.getSerialNumberForUser(secondaryUser);
  // Save the serial number to storage  ...
}

사용자 구성

사용자의 필요에 따라 보조 사용자의 설정을 맞춤설정할 수 있습니다. createAndManageUser()를 호출할 때 다음 플래그를 포함할 수 있습니다.

SKIP_SETUP_WIZARD
업데이트를 확인하고 설치하는 신규 사용자 설정 마법사 실행을 건너뛰고, 사용자에게 Google 서비스와 함께 Google 계정을 추가하라는 메시지를 표시하며, 화면 잠금을 설정합니다. 이 작업은 다소 시간이 걸릴 수 있으며 모든 사용자(예: 공개 인터넷 키오스크)에 적용되지 않을 수 있습니다.
LEAVE_ALL_SYSTEM_APPS_ENABLED
신규 사용자에게 모든 시스템 앱을 사용 설정된 상태로 둡니다. 이 플래그를 설정하지 않으면 신규 사용자에게는 휴대전화가 작동하는 데 필요한 최소한의 앱(일반적으로 파일 브라우저, 전화 다이얼러, 연락처, SMS 메시지)만 포함됩니다.

사용자 수명 주기 준수

DPC (완전 관리형 기기의 관리자인 경우)는 보조 사용자가 변경되는 시점을 알면 유용합니다. 변경 후에 후속 작업을 실행하려면 DPC의 DeviceAdminReceiver 서브클래스에서 다음과 같은 콜백 메서드를 재정의합니다.

onUserStarted()
시스템에서 사용자를 시작한 후에 호출됩니다. 이 사용자는 아직 설정 중이거나 백그라운드에서 실행 중일 수 있습니다. startedUser 인수에서 사용자를 가져올 수 있습니다.
onUserSwitched()
시스템이 다른 사용자로 전환한 후 호출됩니다. 현재 포그라운드에서 실행 중인 신규 사용자를 switchedUser 인수에서 가져올 수 있습니다.
onUserStopped()
사용자가 로그아웃했거나 새 사용자로 전환되었거나 (사용자가 임시 사용자인 경우) DPC가 사용자를 중지했기 때문에 시스템에서 사용자를 중지한 후에 호출됩니다. stoppedUser 인수에서 사용자를 가져올 수 있습니다.
onUserAdded()
시스템에서 신규 사용자를 추가할 때 호출됩니다. 일반적으로 DPC가 콜백을 가져올 때 보조 사용자는 완전히 설정되지 않습니다. newUser 인수에서 사용자를 가져올 수 있습니다.
onUserRemoved()
시스템이 사용자를 삭제한 후에 호출됩니다. 사용자가 이미 삭제되었으므로 removedUser 인수로 표시되는 사용자에게는 액세스할 수 없습니다.

시스템에서 사용자를 포그라운드로 가져오거나 사용자를 백그라운드로 보내는 시점을 알기 위해 앱은 ACTION_USER_FOREGROUNDACTION_USER_BACKGROUND 브로드캐스트에 수신기를 등록하면 됩니다.

사용자 탐색

모든 보조 사용자를 가져오려면 완전 관리형 기기의 관리자가 DevicePolicyManager.getSecondaryUsers()를 호출하면 됩니다. 결과에는 관리자가 만든 보조 또는 임시 사용자가 포함됩니다. 결과에는 기기 사용자가 만들었을 수 있는 보조 사용자 (또는 게스트 사용자)도 포함됩니다. 직장 프로필은 보조 사용자가 아니므로 결과에 포함되지 않습니다. 다음 샘플은 이 메서드의 사용 방법을 보여줍니다.

Kotlin

// The device is stored for the night. Stop all running secondary users.
dpm.getSecondaryUsers(adminName).forEach {
    dpm.stopUser(adminName, it)
}

Java

// The device is stored for the night. Stop all running secondary users.
for (UserHandle user : dpm.getSecondaryUsers(adminName)) {
  dpm.stopUser(adminName, user);
}

보조 사용자의 상태를 확인하기 위해 호출할 수 있는 기타 메서드는 다음과 같습니다.

DevicePolicyManager.isEphemeralUser()
보조 사용자의 관리자로부터 이 메서드를 호출하여 임시 사용자인지 확인합니다.
DevicePolicyManager.isAffiliatedUser()
이 사용자가 기본 사용자와 연결되어 있는지 확인하려면 보조 사용자의 관리자에서 이 메서드를 호출합니다. 제휴에 관한 자세한 내용은 아래의 DPC 조정을 참고하세요.

사용자 관리

사용자 수명 주기를 완전히 관리하려면 API를 호출하여 기기가 사용자를 변경하는 시점과 방법을 세부적으로 제어하면 됩니다. 예를 들어 한동안 기기를 사용하지 않은 사용자를 삭제하거나 교대 근무가 끝나기 전에 전송되지 않은 주문을 서버로 보낼 수 있습니다.

로그아웃

Android 9.0에서는 기기 사용자가 세션을 종료할 수 있도록 잠금 화면에 로그아웃 버튼을 추가했습니다. 버튼을 탭하면 시스템은 보조 사용자를 중지하고 임시 사용자인 경우 사용자를 삭제하며 기본 사용자가 포그라운드로 돌아갑니다. 기본 사용자가 포그라운드에 있으면 Android에서는 버튼이 숨겨집니다. 기본 사용자는 로그아웃할 수 없기 때문입니다.

Android에서는 기본적으로 세션 종료 버튼이 표시되지 않지만, 완전 관리형 기기의 관리자가 DevicePolicyManager.setLogoutEnabled()를 호출하여 이를 사용 설정할 수 있습니다. 버튼의 현재 상태를 확인해야 하는 경우 DevicePolicyManager.isLogoutEnabled()를 호출합니다.

보조 사용자의 관리자는 프로그래매틱 방식으로 사용자를 로그아웃하고 기본 사용자로 돌아갈 수 있습니다. 먼저 보조 사용자와 기본 사용자가 연결되어 있는지 확인한 다음 DevicePolicyManager.logoutUser()를 호출합니다. 로그아웃한 사용자가 임시 사용자인 경우 시스템은 사용자를 중지한 후 삭제합니다.

사용자 전환

완전 관리형 기기의 관리자는 DevicePolicyManager.switchUser()를 호출하여 다른 보조 사용자로 전환할 수 있습니다. 편의상 null를 전달하여 기본 사용자로 전환할 수 있습니다.

사용자 중지

보조 사용자를 중지하려면 완전 관리형 기기를 소유한 DPC에서 DevicePolicyManager.stopUser()를 호출하면 됩니다. 중지된 사용자가 임시 사용자인 경우 해당 사용자는 중지된 후 삭제됩니다.

기기의 최대 실행 사용자 수를 초과하지 않도록 가능한 한 사용자를 중지하는 것이 좋습니다.

사용자 삭제

보조 사용자를 영구적으로 삭제하기 위해 DPC는 다음 DevicePolicyManager 메서드 중 하나를 호출할 수 있습니다.

  • 완전 관리형 기기의 관리자는 removeUser()를 호출할 수 있습니다.
  • 보조 사용자의 관리자는 wipeData()를 호출할 수 있습니다.

임시 사용자가 로그아웃되거나 중지되거나 다른 곳으로 전환되면 시스템은 임시 사용자를 삭제합니다.

기본 UI 사용 중지

DPC가 사용자 관리를 위한 UI를 제공하는 경우 Android의 내장 멀티 사용자 인터페이스를 사용 중지할 수 있습니다. 다음 예와 같이 DevicePolicyManager.setLogoutEnabled()를 호출하고 DISALLOW_USER_SWITCH 제한을 추가하면 됩니다.

Kotlin

// Explicitly disallow logging out using Android UI (disabled by default).
dpm.setLogoutEnabled(adminName, false)

// Disallow switching users in Android's UI. This DPC can still
// call switchUser() to manage users.
dpm.addUserRestriction(adminName, UserManager.DISALLOW_USER_SWITCH)

Java

// Explicitly disallow logging out using Android UI (disabled by default).
dpm.setLogoutEnabled(adminName, false);

// Disallow switching users in Android's UI. This DPC can still
// call switchUser() to manage users.
dpm.addUserRestriction(adminName, UserManager.DISALLOW_USER_SWITCH);

완전 관리형 기기의 관리자는 DISALLOW_ADD_USER 사용자 제한을 자동으로 추가하므로 기기 사용자는 Android 내장 UI를 사용하여 보조 사용자를 추가할 수 없습니다.

세션 메시지

기기 사용자가 새 사용자로 전환하면 Android가 스위치를 강조 표시하는 패널을 표시합니다. Android에서 다음 메시지를 표시합니다.

  • 기기가 기본 사용자로부터 보조 사용자로 전환할 때 표시되는 사용자 세션 시작 메시지입니다.
  • 기기가 보조 사용자로부터 기본 사용자로 돌아올 때 표시되는 최종 사용자 세션 메시지

두 보조 사용자 간에 전환할 때는 시스템에서 메시지가 표시되지 않습니다.

메시지가 모든 상황에 적합하지 않을 수 있으므로 이러한 메시지의 텍스트를 변경할 수 있습니다. 예를 들어 솔루션에서 임시 사용자 세션을 사용하는 경우 브라우저 세션 중지 및 개인 정보 삭제...와 같은 메시지에 이를 반영할 수 있습니다.

시스템에서 메시지를 몇 초 동안 표시하므로 각 메시지는 짧고 명확한 문구여야 합니다. 메시지를 맞춤설정하려면 관리자가 다음 예와 같이 DevicePolicyManager 메서드 setStartUserSessionMessage()setEndUserSessionMessage()를 호출하면 됩니다.

Kotlin

// Short, easy-to-read messages shown at the start and end of a session.
// In your app, store these strings in a localizable resource.
internal val START_USER_SESSION_MESSAGE = "Starting guest session…"
internal val END_USER_SESSION_MESSAGE = "Stopping & clearing data…"

// ...
dpm.setStartUserSessionMessage(adminName, START_USER_SESSION_MESSAGE)
dpm.setEndUserSessionMessage(adminName, END_USER_SESSION_MESSAGE)

Java

// Short, easy-to-read messages shown at the start and end of a session.
// In your app, store these strings in a localizable resource.
private static final String START_USER_SESSION_MESSAGE = "Starting guest session…";
private static final String END_USER_SESSION_MESSAGE = "Stopping & clearing data…";

// ...
dpm.setStartUserSessionMessage(adminName, START_USER_SESSION_MESSAGE);
dpm.setEndUserSessionMessage(adminName, END_USER_SESSION_MESSAGE);

맞춤 메시지를 삭제하고 Android의 기본 메시지로 돌아가려면 null를 전달합니다. 현재 메시지 텍스트를 확인해야 하는 경우 getStartUserSessionMessage() 또는 getEndUserSessionMessage()를 호출합니다.

DPC는 사용자의 현재 언어에 맞게 현지화된 메시지를 설정해야 합니다. 사용자의 언어가 변경될 때 메시지도 업데이트해야 합니다.

Kotlin

override fun onReceive(context: Context?, intent: Intent?) {
    // Added the <action android:name="android.intent.action.LOCALE_CHANGED" />
    // intent filter for our DeviceAdminReceiver subclass in the app manifest file.
    if (intent?.action === ACTION_LOCALE_CHANGED) {

        // Android's resources return a string suitable for the new locale.
        getManager(context).setStartUserSessionMessage(
                getWho(context),
                context?.getString(R.string.start_user_session_message))

        getManager(context).setEndUserSessionMessage(
                getWho(context),
                context?.getString(R.string.end_user_session_message))
    }
    super.onReceive(context, intent)
}

Java

public void onReceive(Context context, Intent intent) {
  // Added the <action android:name="android.intent.action.LOCALE_CHANGED" />
  // intent filter for our DeviceAdminReceiver subclass in the app manifest file.
  if (intent.getAction().equals(ACTION_LOCALE_CHANGED)) {

    // Android's resources return a string suitable for the new locale.
    getManager(context).setStartUserSessionMessage(
        getWho(context),
        context.getString(R.string.start_user_session_message));

    getManager(context).setEndUserSessionMessage(
        getWho(context),
        context.getString(R.string.end_user_session_message));
  }
  super.onReceive(context, intent);
}

DPC 조정

보조 사용자를 관리하려면 일반적으로 두 개의 DPC 인스턴스가 필요합니다. 하나는 완전 관리형 기기를 소유하고 다른 하나는 보조 사용자를 소유합니다. 신규 사용자를 만들 때 완전 관리형 기기의 관리자는 자신의 다른 인스턴스를 신규 사용자의 관리자로 설정합니다.

연결된 사용자

이 개발자 가이드의 일부 API는 보조 사용자가 연결된 경우에만 작동합니다. 연결되지 않은 새로운 보조 사용자를 기기에 추가하면 Android는 일부 기능(예: 네트워크 로깅)을 사용 중지하므로 가능한 한 빨리 사용자를 연결해야 합니다. 아래 설정에서 예를 참고하세요.

설정

사용자가 사용할 수 있도록 하기 전에 (보조 사용자를 소유한 DPC에서) 새로운 보조 사용자를 설정합니다. DeviceAdminReceiver.onEnabled() 콜백에서 이 설정을 실행할 수 있습니다. 이전에 createAndManageUser() 호출에서 관리자 추가 항목을 설정한 경우 intent 인수에서 값을 가져올 수 있습니다. 다음 예는 콜백에서 새로운 보조 사용자를 연결하는 DPC를 보여줍니다.

Kotlin

override fun onEnabled(context: Context?, intent: Intent?) {
    super.onEnabled(context, intent)

    // Get the affiliation ID (our DPC previously put in the extras) and
    // set the ID for this new secondary user.
    intent?.getStringExtra(AFFILIATION_ID_KEY)?.let {
        val dpm = getManager(context)
        dpm.setAffiliationIds(getWho(context), setOf(it))
    }
    // Continue setup of the new secondary user ...
}

Java

public void onEnabled(Context context, Intent intent) {
  // Get the affiliation ID (our DPC previously put in the extras) and
  // set the ID for this new secondary user.
  String affiliationId = intent.getStringExtra(AFFILIATION_ID_KEY);
  if (affiliationId != null) {
    DevicePolicyManager dpm = getManager(context);
    dpm.setAffiliationIds(getWho(context),
        new HashSet<String>(Arrays.asList(affiliationId)));
  }
  // Continue setup of the new secondary user ...
}

DPC 간 RPC

두 DPC 인스턴스가 서로 다른 사용자 하에서 실행되더라도 기기를 소유한 DPC와 보조 사용자가 서로 통신할 수 있습니다. 다른 DPC 서비스를 호출하면 사용자 경계를 초과하므로 DPC는 일반적으로 Android에서 하는 것처럼 bindService()를 호출할 수 없습니다. 다른 사용자에서 실행 중인 서비스에 바인딩하려면 DevicePolicyManager.bindDeviceAdminServiceAsUser()를 호출합니다.

RPC를 호출하는 기본 사용자 및 연결된 보조 사용자 두 명
그림 2. 서비스 메서드를 호출하는 연결된 기본 및 보조 사용자의 관리자

DPC는 DevicePolicyManager.getBindDeviceAdminTargetUsers()에서 반환된 사용자로부터 실행되는 서비스에만 바인딩할 수 있습니다. 다음 예에서는 완전 관리형 기기의 관리자와 보조 사용자 결합의 관리자를 보여줍니다.

Kotlin

// From a secondary user, the list contains just the primary user.
dpm.getBindDeviceAdminTargetUsers(adminName).forEach {

    // Set up the callbacks for the service connection.
    val intent = Intent(mContext, FullyManagedDeviceService::class.java)
    val serviceconnection = object : ServiceConnection {
        override fun onServiceConnected(componentName: ComponentName,
                                        iBinder: IBinder) {
            // Call methods on service ...
        }
        override fun onServiceDisconnected(componentName: ComponentName) {
            // Clean up or reconnect if needed ...
        }
    }

    // Bind to the service as the primary user [it].
    val bindSuccessful = dpm.bindDeviceAdminServiceAsUser(adminName,
            intent,
            serviceconnection,
            Context.BIND_AUTO_CREATE,
            it)
}

Java

// From a secondary user, the list contains just the primary user.
List<UserHandle> targetUsers = dpm.getBindDeviceAdminTargetUsers(adminName);
if (targetUsers.isEmpty()) {
  // If the users aren't affiliated, the list doesn't contain any users.
  return;
}

// Set up the callbacks for the service connection.
Intent intent = new Intent(mContext, FullyManagedDeviceService.class);
ServiceConnection serviceconnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(
      ComponentName componentName, IBinder iBinder) {
    // Call methods on service ...
  }

  @Override
  public void onServiceDisconnected(ComponentName componentName) {
    // Clean up or reconnect if needed ...
  }
};

// Bind to the service as the primary user.
UserHandle primaryUser = targetUsers.get(0);
boolean bindSuccessful = dpm.bindDeviceAdminServiceAsUser(
    adminName,
    intent,
    serviceconnection,
    Context.BIND_AUTO_CREATE,
    primaryUser);

추가 리소스

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

  • 전용 기기 개요는 전용 기기의 개요입니다.
  • 작업 잠금 모드에서는 전용 기기를 단일 앱 또는 앱 모음에 잠그는 방법을 설명합니다.
  • 전용 기기 설명서에는 전용 기기를 제한하고 사용자 환경을 개선하기 위한 추가 예가 포함되어 있습니다.