管理多个用户

本开发者指南介绍了设备政策控制器 (DPC) 如何管理专用设备上的多个 Android 用户。

概览

设备政策控制器 (DPC) 可帮助多人共用一台专用设备。在全代管式设备上运行的 DPC 可以创建和管理两类用户:

  • 次要用户是指在会话之间保存独立应用和数据的 Android 用户。您可以使用管理组件管理用户。在轮班开始时拿起设备(例如送餐司机或安全工作人员)时,这些用户会非常有用。
  • 临时用户是系统在用户停止、切换应用或设备重新启动时删除的次要用户。对于可在会话结束后删除数据的情况(例如可公开访问的互联网自助服务终端),这类用户非常有用。

您可以使用现有的 DPC 管理专用设备和次要用户。您 DPC 中的管理员组件会在您创建新次要用户时将其自身设置为管理员。

主要用户和两个次要用户。
图 1. 由同一 DPC 中的管理员管理的主要和次要用户

次要用户的管理员必须与完全受管设备的管理员同属一个软件包。为了简化开发,我们建议在设备和次要用户之间共享管理员。

在专用设备上管理多个用户通常需要 Android 9.0,但是本开发者指南中使用的一些方法可用于早期版本的 Android。

创建用户

您的 DPC 可以在后台创建其他用户,然后可以将这些用户切换到前台。对于次要用户和临时用户,该过程几乎相同。在完全受管设备和次要用户的管理员中执行以下步骤:

  1. 调用 DevicePolicyManager.createAndManageUser()。如需创建临时用户,请在 flag 参数中添加 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
使所有系统应用保持启用新用户的状态。如果您不设置此标志,新用户将仅包含手机运行所需的最少量应用,通常是文件浏览器、电话拨号器、通讯录和短信。

遵循用户生命周期

您的 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()

系统会在临时用户退出账号、停止或退出账号时将其删除。

停用默认界面

如果您的 DPC 提供了用于管理用户的界面,您可以停用 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);

设备使用者无法使用 Android 的内置界面添加次要用户,因为完全受管设备的管理员会自动添加 DISALLOW_ADD_USER 用户限制。

会话消息

当使用设备的用户切换到新用户时,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);

传递 null 即可删除您的自定义消息并返回 Android 的默认消息。如果您需要检查当前消息文本,请调用 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() 时设置了任何管理员 extra,则可以从 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);

其他资源

如需详细了解专用设备,请参阅以下文档: