Manage multiple users

This developer’s guide explains how your device policy controller (DPC) can manage multiple Android users on dedicated devices.

Overview

Your DPC can help multiple people share a single dedicated device. Your DPC running on a fully managed device can create and manage two types of users:

  • Secondary users are Android users with separate apps and data saved between sessions. You manage the user with an admin component. These users are useful for cases where a device is picked up at the start of a shift, such as delivery drivers or security workers.
  • Ephemeral users are secondary users that the system deletes when the user stops, switches away, or the device reboots. These users are useful for cases where data can be deleted after the session finishes, such as public-access internet kiosks.

You use your existing DPC to manage the dedicated device and the secondary users. An admin component in your DPC sets itself as the admin for new secondary users when you create them.

Primary user and two secondary users.
Figure 1. Primary and secondary users managed by admins from the same DPC

Admins of a secondary user must belong to the same package as the admin of the fully managed device. To simplify development, we recommend sharing an admin between the device and secondary users.

Managing multiple users on dedicated devices typically requires Android 9.0, however some of the methods used in this developer’s guide are available in earlier versions of Android.

Secondary users

Secondary users can connect to Wifi and can configure new networks. However, they can't edit or delete networks, not even the networks they created.

Create users

Your DPC can create additional users in the background and then can switch them to the foreground. The process is almost the same for both secondary and ephemeral users. Implement the following steps in the admins of the fully managed device and secondary user:

  1. Call DevicePolicyManager.createAndManageUser(). To create an ephemeral user, include MAKE_USER_EPHEMERAL in the flags argument.
  2. Call DevicePolicyManager.startUserInBackground() to start the user in the background. The user starts running but you will want to finish setup before bringing the user to the foreground and showing it to the person using the device.
  3. In the admin of the secondary user, call DevicePolicyManager.setAffiliationIds() to affiliate the new user with the primary user. See DPC coordination below.
  4. Back in the admin of the fully managed device, call DevicePolicyManager.switchUser() to switch the user to the foreground.

The following sample shows how you can add step 1 to your 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...
  }
}

When you create or start a new user, you can check the reason for any failures by catching the UserOperationException exception and calling getUserOperationResult(). Exceeding the user limits are common failure reasons:

Creating a user can take some time. If you’re frequently creating users, you can improve the user experience by preparing a ready-to-go user in the background. You might need to balance the advantages of a ready-to-go user with the maximum number of users allowed on a device.

Identification

After creating a new user, you should refer to the user with a persistent serial number. Don’t persist the UserHandle because the system recycles these as you create and delete users. Get the serial number by calling 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  ...
}

User config

Depending on the needs of your users, you can customize the setup of secondary users. You can include the following flags when calling createAndManageUser():

SKIP_SETUP_WIZARD
Skips running the new-user setup wizard that checks for and installs updates, prompts the user to add a Google Account along with Google services, and sets a screen lock. This can take some time and might not be applicable for all users—public internet kiosks, for example.
LEAVE_ALL_SYSTEM_APPS_ENABLED
Leaves all the system apps enabled in the new user. If you don’t set this flag, the new user contains just the minimal set of apps that the phone needs to operate—typically a file browser, telephone dialer, contacts, and SMS messages.

Follow the user lifecycle

Your DPC (if it's an admin of the fully managed device) might find it useful to know when secondary users change. To run follow-on tasks after changes, override these callback methods in your DPC's DeviceAdminReceiver subclass:

onUserStarted()
Called after the system starts a user. This user might still be setting up or be running in the background. You can get the user from the startedUser argument.
onUserSwitched()
Called after the system switches to a different user. You can get the new user that’s now running in the foreground from the switchedUser argument.
onUserStopped()
Called after the system stops a user because they logged out, switched to a new user (if the user is ephemeral), or your DPC stopped the user. You can get the user from the stoppedUser argument.
onUserAdded()
Called when the system adds a new user. Typically, secondary users aren’t fully set up when your DPC gets the callback. You can get the user from the newUser argument.
onUserRemoved()
Called after the system deletes a user. Because the user is already deleted, you can't access the user represented by the removedUser argument.

To know when the system brings a user to the foreground or sends a user to the background, apps can register a receiver for the ACTION_USER_FOREGROUND and ACTION_USER_BACKGROUND broadcasts.

Discover users

To get all the secondary users, an admin of a fully managed device can call DevicePolicyManager.getSecondaryUsers(). The results include any secondary or ephemeral users the admin created. The results also include any secondary users (or a guest user) a person using the device might have created. The results don’t include work profiles because they aren’t secondary users. The following sample shows how you can use this method:

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);
}

Here are other methods you can call to find out the status of secondary users:

DevicePolicyManager.isEphemeralUser()
Call this method from the admin of a secondary user to find out if this is an ephemeral user.
DevicePolicyManager.isAffiliatedUser()
Call this method from the admin of a secondary user to find out if this user is affiliated with the primary user. To learn more about affiliation, see DPC coordination below.

User management

If you want to completely manage the user lifecycle, you can call APIs for fine-grained control of when and how the device changes users. For example, you can delete a user when a device hasn’t been used for a period of time or you can send any unsent orders to a server before a person’s shift finishes.

Logout

Android 9.0 added a log-out button to the lock screen so that a person using the device can end their session. After tapping the button, the system stops the secondary user, deletes the user if it’s ephemeral, and the primary user returns to the foreground. Android hides the button when the primary user is in the foreground because the primary user can’t log out.

Android doesn’t show the end-session button by default but your admin (of a fully managed device) can enable it by calling DevicePolicyManager.setLogoutEnabled(). If you need to confirm the current state of the button, call DevicePolicyManager.isLogoutEnabled().

The admin of a secondary user can programmatically log out the user and return to the primary user. First, confirm the secondary and the primary users are affiliated, then call DevicePolicyManager.logoutUser(). If the logged-out user is an ephemeral user, the system stops and then deletes the user.

Switch users

To switch to a different secondary user, the admin of a fully managed device can call DevicePolicyManager.switchUser(). As a convenience, you can pass null to switch to the primary user.

Stop a user

To stop a secondary user, a DPC that owns a fully managed device can call DevicePolicyManager.stopUser(). If the stopped user is an ephemeral user, the user is stopped and then deleted.

We recommend stopping users whenever possible to help stay below the device’s maximum number of running users.

Delete a user

To permanently delete a secondary user a DPC can call one of the following DevicePolicyManager methods:

  • An admin of a fully managed device can call removeUser().
  • An admin of the secondary user can call wipeData().

The system deletes ephemeral users when they’re logged out, stopped, or switched away from.

Disable the default UI

If your DPC provides a UI to manage users, you can disable Android’s built-in multi-user interface. You can do this by calling DevicePolicyManager.setLogoutEnabled() and adding the DISALLOW_USER_SWITCH restriction as shown in the following example:

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);

The person using the device can’t add secondary users with Android’s built-in UI because admins of fully managed devices automatically add the DISALLOW_ADD_USER user restriction.

Session messages

When the person using a device switches to a new user, Android shows a panel to highlight the switch. Android shows the following messages:

  • Start-user-session message shown when the device switches to a secondary user from the primary user.
  • End-user-session message shown when the device returns to the primary user from a secondary user.

The system doesn't show the messages when switching between two secondary users.

Because the messages might not be suitable for all situations, you can change the text of these messages. For example, if your solution uses ephemeral user sessions, you can reflect this in the messages such as: Stopping browser session & deleting personal data…

The system shows the message for just a couple of seconds, so each message should be a short, clear phrase. To customize the messages, your admin can call the DevicePolicyManager methods setStartUserSessionMessage() and setEndUserSessionMessage() as shown in the following example:

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);

Pass null to delete your custom messages and return to Android’s default messages. If you need to check the current message text, call getStartUserSessionMessage() or getEndUserSessionMessage().

Your DPC should set localized messages for the user's current locale. You also need to update the messages when the user's locale changes:

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 coordination

Managing secondary users typically needs two instances of your DPC—one that owns the fully managed device while the other owns the secondary user. When creating a new user, the admin of the fully managed device sets another instance of itself as the admin of the new user.

Affiliated users

Some of the APIs in this developer's guide only work when the secondary users are affiliated. Because Android disables some features (network logging for example) when you add new unaffiliated secondary users to the device, you should affiliate users as soon as possible. See the example in Setup below.

Setup

Set up new secondary users (from the DPC that owns the secondary user) before letting people use them. You can do this setup from the DeviceAdminReceiver.onEnabled() callback. If you previously set any admin extras in the call to createAndManageUser(), you can get the values from the intent argument. The following example shows a DPC affiliating a new secondary user in the callback:

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 ...
}

RPCs between DPCs

Even though the two DPC instances are running under separate users, the DPCs that own the device and the secondary users can communicate with each other. Because calling another DPC's service crosses user boundaries, your DPC can't call bindService() as you normally would in Android. To bind to a service running in another user, call DevicePolicyManager.bindDeviceAdminServiceAsUser().

Primary user and two affiliated secondary users calling RPCs.
Figure 2. Admins of affiliated primary and secondary users calling service methods

Your DPC can only bind to services running in the users returned by DevicePolicyManager.getBindDeviceAdminTargetUsers(). The following example shows the admin of a secondary user binding to the admin of the fully managed device:

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);

Additional resources

To learn more about dedicated devices, read the following documents: