管理多个用户

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

概述

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

  • 辅助用户是 Android 用户,其单独的应用和数据在会话之间保存。您可以使用管理员组件管理用户。这些用户适用于设备在轮班开始时被拾取的情况,例如送货司机或保安人员。
  • 临时用户是系统在用户停止、切换或设备重启时删除的辅助用户。这些用户适用于会话结束后可以删除数据的情况,例如公共互联网信息亭。

您使用现有的 DPC 来管理专用设备和辅助用户。当您创建辅助用户时,DPC 中的管理员组件会将其自身设置为新辅助用户的管理员。

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

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

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

辅助用户

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

创建用户

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

  1. 调用DevicePolicyManager.createAndManageUser()。要创建临时用户,请在 flags 参数中包含MAKE_USER_EPHEMERAL
  2. 调用DevicePolicyManager.startUserInBackground()以在后台启动用户。用户开始运行,但您需要在将用户带到前台并将其显示给使用设备的人员之前完成设置。
  3. 在辅助用户的管理员中,调用DevicePolicyManager.setAffiliationIds()以将新用户与主用户关联。请参阅下面的DPC 协调
  4. 返回到完全托管设备的管理员中,调用DevicePolicyManager.switchUser()以将用户切换到前台。

以下示例显示了如何将步骤 1 添加到您的 DPC 中

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 以删除自定义消息并返回到 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);

其他资源

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