管理多位使用者

本開發人員指南將會說明裝置政策控制器 (DPC) 如何在專用裝置上管理多個 Android 使用者。

總覽

裝置政策控制器 (DPC) 可協助多人共用一部專用裝置。在全代管裝置上執行的 DPC 可以建立及管理兩種使用者:

  • 「次要使用者」是 Android 使用者,擁有各自獨立的應用程式和工作階段之間的資料。並透過管理員元件管理使用者。這類使用者相當適合在值班開始時拿起裝置,例如外送司機或保全人員。
  • 「臨時使用者」是指當使用者停止、切換或裝置重新啟動時,系統會刪除的次要使用者。如果可以在工作階段結束後刪除資料 (例如公開存取網際網路資訊站),這些使用者就能派上用場。

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

主要使用者和兩名次要使用者。
圖 1 由同一 DPC 的管理員控管的主要和次要使用者

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

一般來說,必須使用 Android 9.0 才能在專用裝置上管理多位使用者,不過這份開發人員指南中所用的部分方法都適用於舊版 Android。

次要使用者

次要使用者可以連上 Wi-Fi 及設定新網路。但無法編輯或刪除網路,即使是已建立的網路也不例外。

建立使用者

DPC 可以在背景建立其他使用者,然後就可以將其切換為前景。次要和暫時使用者的程序幾乎相同。在全代管裝置和次要使用者的管理員中實作下列步驟:

  1. 呼叫 DevicePolicyManager.createAndManageUser()。 如要建立臨時使用者,請在旗標引數中加入 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 可能會覺得非常實用。如要在變更後執行後續工作,請在 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 會顯示下列訊息:

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

當系統切換兩位次要使用者時,系統不會顯示這些訊息。

由於訊息可能不適合所有情況,您可以變更這些訊息的文字。例如,如果解決方案使用臨時使用者工作階段,您可以在訊息中反映這一點,例如:「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 ...
}

裝置政策控制器之間的遠端程序呼叫 (RPC)

雖然這兩個 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);

其他資源

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