複数のユーザーの管理

このデベロッパー ガイドでは、Device Policy Controller(DPC)が専用デバイス上の複数の Android ユーザーを管理する方法について説明します。

概要

DPC を使用すると、複数のユーザーが 1 つの専用デバイスを共有できます。完全管理対象デバイスで実行される DPC では、次の 2 種類のユーザーを作成して管理できます。

  • セカンダリ ユーザーとは、セッション間で個別のアプリとデータが保存されている Android ユーザーです。ユーザーは管理コンポーネントを使用して管理します。このようなユーザーは、シフトの開始時にデバイスを集荷する場合に便利です(配達員やセキュリティ ワーカーなど)。
  • エフェメラル ユーザーとは、ユーザーの停止、切り替え、デバイスの再起動時にシステムによって削除されるセカンダリ ユーザーです。これらのユーザーは、公共アクセスのインターネット キオスクなど、セッション終了後にデータを削除できる場合に便利です。

既存の DPC を使用して、専用デバイスとセカンダリ ユーザーを管理します。DPC の管理コンポーネントは、新しいセカンダリ ユーザーを作成するときに、自身を管理者として設定します。

プライマリ ユーザーと 2 人のセカンダリ ユーザー。
図 1. 同じ DPC の管理者が管理しているプライマリ ユーザーとセカンダリ ユーザー

セカンダリ ユーザーの管理者は、フルマネージド デバイスの管理者と同じパッケージに属している必要があります。開発を簡素化するために、デバイスとセカンダリ ユーザーの間で管理者を共有することをおすすめします。

専用デバイスで複数のユーザーを管理するには、通常は Android 9.0 が必要ですが、このデベロッパー ガイドで使用している方法の一部は、以前のバージョンの Android でも使用できます。

ユーザーを作成する

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() を呼び出すことで、失敗の理由を確認できます。一般的なエラーの原因としては、ユーザーの上限を超えると、次のようなエラーが挙げられます。

ユーザーの作成には時間がかかることがあります。ユーザーを頻繁に作成する場合は、すぐに使用できるユーザーをバックグラウンドで準備することで、ユーザー エクスペリエンスを改善できます。すぐに使用できるユーザーのメリットと、1 つのデバイスで許可される最大ユーザー数のバランスを取る必要が生じることがあります。

共感

新しいユーザーを作成した後は、永続的なシリアル番号を持つユーザーを参照する必要があります。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
新規ユーザーですべてのシステムアプリを有効のままにします。このフラグを設定しない場合、新しいユーザーには、スマートフォンの動作に必要な最小限のアプリセット(通常はファイル ブラウザ、電話アプリ、連絡先、SMS メッセージ)のみが含まれます。

ユーザー ライフサイクルに従う

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

完全管理対象デバイスの管理者は DISALLOW_ADD_USER ユーザー制限を自動的に追加するため、デバイスを使用しているユーザーは、Android の組み込み UI でセカンダリ ユーザーを追加できません。

セッション メッセージ

デバイスを使用しているユーザーが新しいユーザーに切り替えると、Android でスイッチをハイライト表示するパネルが表示されます。Android には次のメッセージが表示されます。

  • デバイスがプライマリ ユーザーからセカンダリ ユーザーに切り替えられるときに表示されるユーザー セッション開始メッセージ
  • デバイスがセカンダリ ユーザーからプライマリ ユーザーに戻るときに表示されるエンドユーザー セッション メッセージ

2 人のセカンダリ ユーザーを切り替えても、メッセージは表示されません。

メッセージがすべての状況に適しているとは限らないため、これらのメッセージのテキストは変更できます。たとえば、ソリューションで一時的なユーザー セッションを使用している場合は、「ブラウザ セッションの停止と個人データの削除...」のようなメッセージに反映させることができます。

メッセージは数秒だけ表示されるため、各メッセージは短く明確なフレーズにする必要があります。メッセージをカスタマイズするには、次の例に示すように、管理者は 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 のインスタンスが 2 つ必要です。1 つは完全管理対象デバイスを所有し、もう 1 つはセカンダリ ユーザーが所有します。新しいユーザーを作成する際、完全管理対象デバイスの管理者は、そのデバイスの別のインスタンスを新しいユーザーの管理者として設定します。

関連付けられたユーザー

このデベロッパー ガイドの 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

2 つの DPC インスタンスが別々のユーザーで実行されていても、デバイスを所有する DPC とセカンダリ ユーザーは相互に通信できます。別の DPC のサービスの呼び出しはユーザーの境界を越えるため、DPC は Android での通常のように bindService() を呼び出すことができません。別のユーザーで実行されているサービスにバインドするには、DevicePolicyManager.bindDeviceAdminServiceAsUser() を呼び出します。

RPC を呼び出すプライマリ ユーザーと、関連する 2 つのセカンダリ ユーザー。
図 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);

参考情報

専用デバイスの詳細については、以下のドキュメントをご覧ください。