複数のユーザーの管理

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

概要

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

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

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

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

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

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

セカンダリ ユーザー

セカンダリ ユーザーは Wi-Fi に接続し、新しいネットワークを設定できます。ただし、作成したネットワークを含め、ネットワークの編集や削除はできません。

ユーザーを作成する

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() を呼び出すことで、失敗の理由を確認できます。一般的なエラーの原因は、ユーザーの上限を超えるとエラーになります。

ユーザーの作成には時間がかかることがあります。ユーザーを頻繁に作成する場合は、すぐに使用できるユーザーをバックグラウンドで準備することで、ユーザー エクスペリエンスを向上させることができます。場合によっては、すぐに使用できるユーザーの利点と、デバイスで許可される最大ユーザー数とのバランスを取る必要があります。

共感

新しいユーザーを作成したら、永続的なシリアル番号を持つユーザーを参照する必要があります。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 には次のメッセージが表示されます。

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

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

参考情報

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