管理多位使用者

本開發人員指南說明您的裝置政策控制器 (DPC) 如何在指定裝置上管理多位 Android 使用者。

總覽

DPC 可以協助多人共用一部專用裝置。在全代管裝置上執行的 DPC,可以建立及管理以下兩種使用者:

  • 「次要使用者」是指 Android 使用者,且各自擁有獨立的應用程式和在工作階段之間儲存的資料。您可以用管理員元件管理使用者。如果裝置在車輛剛啟動 (例如配送驅動程式或保全人員) 啟動時就被帶到裝置,這些使用者就能派上用場。
  • 臨時使用者是指系統會在使用者停止、切換或重新啟動裝置時刪除的次要使用者。對於工作階段完成後 (例如公開存取網際網路資訊站) 可以刪除資料的情況,這些使用者就非常實用。

您可以使用現有的 DPC 管理專用裝置和次要使用者。建立新次要使用者時,DPC 中的管理員元件會將自己設為管理員。

主要使用者和兩位次要使用者。
圖 1. 由管理員透過同一個 DPC 管理的主要和次要使用者

次要使用者的管理員必須與全代管裝置的管理員屬於同一套件。為了簡化開發流程,建議您在裝置和次要使用者之間共用管理員。

在專用裝置上管理多位使用者通常需要 Android 9.0 版,但這份開發人員指南中的部分方法可在舊版 Android 中提供。

建立使用者

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 提供管理使用者的 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 會顯示下列訊息:

  • 裝置從主要使用者切換至次要使用者時,系統會顯示啟動使用者工作階段訊息
  • 當裝置從次要使用者返回主要使用者時,系統會顯示使用者工作階段訊息

系統在切換兩位次要使用者時不會顯示相關訊息。

訊息可能不適合所有情況,因此您可以變更這些訊息的文字。舉例來說,如果您的解決方案使用暫時的使用者工作階段,您可以在訊息中反映這一點,例如:Stopping 瀏覽器工作階段及刪除個人資料...

系統會顯示訊息幾秒鐘,因此每則訊息都應為簡短明確的語句。如要自訂訊息,管理員可以呼叫 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 之間的遠端程序呼叫

即使這兩個 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);

其他資源

想進一步瞭解專用裝置,請參閱下列文件: