管理多个用户

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

概述

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

  • 辅助用户是 Android 用户,他们在不同会话之间保存有独立的应用和数据。您使用管理员组件来管理用户。这些用户适用于设备在轮班开始时就被取走的场景,例如送货员或安保人员。
  • 临时用户是当用户停止、切换或设备重启时,系统会删除的辅助用户。这些用户适用于数据可以在会话结束后删除的场景,例如公共互联网自助服务亭。

您可以使用现有的 DPC 来管理专用设备和辅助用户。DPC 中的管理员组件在您创建新的辅助用户时将自身设置为这些用户的管理员。

Primary user and two secondary users.
图 1. 由同一 DPC 中的管理员管理的主用户和辅助用户

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

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

辅助用户

辅助用户可以连接到 Wifi 并配置新网络。但是,他们不能编辑或删除网络,即使是他们自己创建的网络也不能。

创建用户

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

当临时用户注销、停止或从其切换时,系统会删除它们。

禁用默认 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);

使用设备的人无法使用 Android 的内置 UI 添加辅助用户,因为完全管理的设备的管理员会自动添加 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 传递给 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() 时设置了任何管理员额外信息,则可以从 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()

Primary user and two affiliated secondary users calling RPCs.
图 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);

其他资源

要了解有关专用设备的更多信息,请阅读以下文档