管理多个用户

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

概览

您的 DPC 可帮助多人共用一台专用设备。在全代管式设备上运行的 DPC 可以创建和管理两种类型的用户:

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

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

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

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

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

次要用户

次要用户可以连接到 Wi-Fi,还可以配置新网络。但是,他们不能修改或删除网络,即使是他们创建的网络也不行。

创建用户

您的 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
使新用户的所有系统应用保持启用状态。如果您不设置此标志,新用户只会包含手机运行所需的最小应用集(通常是文件浏览器、电话拨号器、通讯录和短信)。

遵循用户生命周期

您的 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 会显示以下消息:

  • 当设备从主要用户切换到次要用户时显示的启动用户会话消息
  • 当设备从次要用户返回主要用户时显示的最终用户会话消息

在两个次要用户之间切换时,系统不会显示相关消息。

由于这些消息可能不适合所有情况,因此您可以更改这些消息的文本。例如,如果您的解决方案使用临时用户会话,您可以在消息中体现这一点,例如:Stopping browser session & deleted personal data...

系统只会显示几秒钟的消息,因此每条消息都应是简短明了的短语。如需自定义消息,您的管理员可以调用 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);

其他资源

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