此开发者指南说明了您的设备策略控制器 (DPC) 如何在专用设备上管理多个 Android 用户。
概览
您的 DPC 可以帮助多个人共享一台专用设备。在完全托管设备上运行的 DPC 可以创建和管理两种类型的用户:
- 次要用户是指拥有独立应用和数据,并在不同会话之间保存的 Android 用户。您可以使用管理员组件管理用户。这些用户适用于在班次开始时领取设备的场景,例如送货司机或安保人员。
- 临时用户是当用户停止、切换或设备重启时系统会删除的次要用户。这些用户适用于会话结束后数据可以删除的场景,例如公共访问互联网信息亭。
您可以使用现有的 DPC 来管理专用设备和次要用户。您创建新的次要用户时,DPC 中的管理员组件会将其自身设置为新用户的管理员。
次要用户的管理员必须与完全托管设备的管理员属于同一软件包。为了简化开发,我们建议在设备和次要用户之间共享一个管理员。
在专用设备上管理多个用户通常需要 Android 9.0,但本开发者指南中使用的某些方法在早期 Android 版本中也可用。
次要用户
次要用户可以连接到 Wi-Fi 并配置新网络。但是,他们不能编辑或删除网络,即使是他们自己创建的网络也不行。
创建用户
您的 DPC 可以在后台创建其他用户,然后将其切换到前台。次要用户和临时用户的过程几乎相同。在完全托管设备和次要用户的管理员中实施以下步骤:
- 调用
DevicePolicyManager.createAndManageUser()
。要创建临时用户,请在 flags 参数中包含MAKE_USER_EPHEMERAL
。 - 调用
DevicePolicyManager.startUserInBackground()
以在后台启动用户。用户开始运行,但您需要在将用户带到前台并将其显示给设备使用者之前完成设置。 - 在次要用户的管理员中,调用
DevicePolicyManager.setAffiliationIds()
以将新用户与主要用户关联。请参阅下面的DPC 协调。 - 回到完全托管设备的管理员中,调用
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_FOREGROUND
和 ACTION_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()
。
您的 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);
其他资源
要了解有关专用设备的更多信息,请阅读以下文档: