専用デバイス クックブック

このクックブックは、デベロッパーやシステム インテグレータが専用デバイス ソリューションを強化するのに役立ちます。ハウツーレシピに沿って、専用デバイスの動作に関するソリューションを見つけてください。このクックブックは、すでに専用デバイスアプリをインストールしているデベロッパーに最適です。初めて使用する場合は、専用デバイスの概要をご覧ください。

カスタムの Google Home アプリ

以下のレシピは、Android のホーム画面とランチャーを置き換えるアプリを開発する場合に役立ちます。

Google Home アプリになる

アプリをデバイスの Google Home アプリとして設定すると、デバイスの起動時にアプリが自動的に起動されます。ホームボタンを有効にすると、許可リストに登録されたアプリをロックタスク モードでフォアグラウンドにできます。

すべての Google Home アプリは CATEGORY_HOME インテント カテゴリを処理します。これはシステムが Google Home アプリを認識する方法です。デフォルトの Google Home アプリになるには、次の例に示すように DevicePolicyManager.addPersistentPreferredActivity() を呼び出して、アプリのアクティビティのいずれかを優先スマートホーム インテント ハンドラとして設定します。

Kotlin

// Create an intent filter to specify the Home category.
val filter = IntentFilter(Intent.ACTION_MAIN)
filter.addCategory(Intent.CATEGORY_HOME)
filter.addCategory(Intent.CATEGORY_DEFAULT)

// Set the activity as the preferred option for the device.
val activity = ComponentName(context, KioskModeActivity::class.java)
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE)
        as DevicePolicyManager
dpm.addPersistentPreferredActivity(adminName, filter, activity)

Java

// Create an intent filter to specify the Home category.
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
filter.addCategory(Intent.CATEGORY_HOME);
filter.addCategory(Intent.CATEGORY_DEFAULT);

// Set the activity as the preferred option for the device.
ComponentName activity = new ComponentName(context, KioskModeActivity.class);
DevicePolicyManager dpm =
    (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
dpm.addPersistentPreferredActivity(adminName, filter, activity);

この場合も、次の XML スニペットに示すように、アプリ マニフェスト ファイルでインテント フィルタを宣言する必要があります。

<activity
        android:name=".KioskModeActivity"
        android:label="@string/kiosk_mode"
        android:launchMode="singleInstance"
        android:excludeFromRecents="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.HOME"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

通常は、ランチャー アプリを [概要] 画面に表示されないようにします。ただし、システムがロックタスク モードで実行されている場合、Android のランチャーは最初に起動されたアクティビティを非表示にするため、アクティビティの宣言に excludeFromRecents を追加する必要はありません。

個別のタスクを表示する

新しいタスクはそれぞれの概要画面に個別のアイテムとして表示されるため、FLAG_ACTIVITY_NEW_TASK はランチャー タイプのアプリにとって便利なフラグです。[Overview] 画面のタスクの詳細については、履歴画面をご覧ください。

公開キオスク

これらのレシピは、公共の場で無人デバイスで使用する場合に適していますが、多くの専用デバイス ユーザーが自分のタスクに集中することもできます。

デバイスをロックダウンする

デバイスが意図した目的で使用されるように、表 1 に示すユーザー制限を追加できます。

表 1. キオスク デバイスのユーザー制限
ユーザーの制限 説明
DISALLOW_FACTORY_RESET デバイス ユーザーがデバイスを出荷時の設定にリセットできないようにします。完全管理対象デバイスの管理者とプライマリ ユーザーは、この制限を設定できます。
DISALLOW_SAFE_BOOT デバイス ユーザーがアプリを自動的に起動しないセーフモードでデバイスを起動できないようにします。完全管理対象デバイスの管理者とプライマリ ユーザーは、この制限を設定できます。
DISALLOW_MOUNT_PHYSICAL_MEDIA デバイスのユーザーが、デバイスに接続する可能性のあるストレージ ボリュームをマウントできないようにします。完全管理対象デバイスの管理者とプライマリ ユーザーは、この制限を設定できます。
DISALLOW_ADJUST_VOLUME デバイスをミュートし、デバイスのユーザーが音量とバイブレーションの設定を変更できないようにします。キオスクでメディア再生やユーザー補助機能に音声が不要なことを確認します。完全管理対象デバイス、プライマリ ユーザー、セカンダリ ユーザー、仕事用プロファイルの管理者は、この制限を設定できます。
DISALLOW_ADD_USER デバイスのユーザーが新規ユーザー(セカンダリ ユーザーや制限付きユーザーなど)を追加できないようにします。このユーザー制限は、完全管理対象デバイスに自動的に追加されますが、解除されている可能性があります。完全管理対象デバイスの管理者とプライマリ ユーザーは、この制限を設定できます。

次のスニペットは、制限を設定する方法を示しています。

Kotlin

// If the system is running in lock task mode, set the user restrictions
// for a kiosk after launching the activity.
arrayOf(
        UserManager.DISALLOW_FACTORY_RESET,
        UserManager.DISALLOW_SAFE_BOOT,
        UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
        UserManager.DISALLOW_ADJUST_VOLUME,
        UserManager.DISALLOW_ADD_USER).forEach { dpm.addUserRestriction(adminName, it) }

Java

// If the system is running in lock task mode, set the user restrictions
// for a kiosk after launching the activity.
String[] restrictions = {
    UserManager.DISALLOW_FACTORY_RESET,
    UserManager.DISALLOW_SAFE_BOOT,
    UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
    UserManager.DISALLOW_ADJUST_VOLUME,
    UserManager.DISALLOW_ADD_USER};

for (String restriction: restrictions) dpm.addUserRestriction(adminName, restriction);

アプリが管理者モードのときは、これらの制限を削除して、IT 管理者がデバイスのメンテナンスで引き続きこれらの機能を使用できるようにすることをおすすめします。制限をクリアするには、DevicePolicyManager.clearUserRestriction() を呼び出します。

エラー ダイアログを表示しない

販売店デモや公開情報の表示など、環境によっては、エラー ダイアログをユーザーに表示したくない場合があります。Android 9.0(API レベル 28)以降では、DISALLOW_SYSTEM_ERROR_DIALOGS ユーザー制限を追加することで、クラッシュしたアプリや応答しないアプリのシステムエラー ダイアログを抑制できます。応答しないアプリは、デバイスのユーザーがダイアログからアプリを閉じた場合と同様に再起動します。次の例はその方法を示しています。

Kotlin

override fun onEnabled(context: Context, intent: Intent) {
    val dpm = getManager(context)
    val adminName = getWho(context)

    dpm.addUserRestriction(adminName, UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS)
}

Java

public void onEnabled(Context context, Intent intent) {
  DevicePolicyManager dpm = getManager(context);
  ComponentName adminName = getWho(context);

  dpm.addUserRestriction(adminName, UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS);
}

プライマリ ユーザーまたはセカンダリ ユーザーの管理者がこの制限を設定すると、そのユーザーに対してのみエラー ダイアログが表示されなくなります。完全管理対象デバイスの管理者がこの制限を設定すると、すべてのユーザーのダイアログが非表示になります。

画面をオンのままにする

キオスクを構築している場合は、アプリのアクティビティを実行しているときにスリープ状態になるデバイスを停止できます。次の例に示すように、FLAG_KEEP_SCREEN_ON レイアウト フラグをアプリのウィンドウに追加します。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Keep the screen on and bright while this kiosk activity is running.
    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // Keep the screen on and bright while this kiosk activity is running.
  getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

デバイスが AC、USB、ワイヤレス充電器に接続されていることを確認してください。バッテリー変更のブロードキャストに登録し、BatteryManager 値を使用して充電状態を検出します。デバイスの電源が切れた場合に、IT 管理者にリモート アラートを送信することもできます。詳しい手順については、バッテリーの残量と充電状態をモニタリングするをご覧ください。

また、STAY_ON_WHILE_PLUGGED_IN グローバル設定を設定して、電源に接続している間はデバイスを起動させておくこともできます。Android 6.0(API レベル 23)以降の完全管理対象デバイスの管理者は、次の例に示すように DevicePolicyManager.setGlobalSetting() を呼び出すことができます。

Kotlin

val pluggedInto = BatteryManager.BATTERY_PLUGGED_AC or
        BatteryManager.BATTERY_PLUGGED_USB or
        BatteryManager.BATTERY_PLUGGED_WIRELESS
dpm.setGlobalSetting(adminName,
        Settings.Global.STAY_ON_WHILE_PLUGGED_IN, pluggedInto.toString())

Java

int pluggedInto = BatteryManager.BATTERY_PLUGGED_AC |
    BatteryManager.BATTERY_PLUGGED_USB |
    BatteryManager.BATTERY_PLUGGED_WIRELESS;
dpm.setGlobalSetting( adminName,
    Settings.Global.STAY_ON_WHILE_PLUGGED_IN, String.valueOf(pluggedInto));

アプリ パッケージ

このセクションでは、アプリを専用デバイスに効率的にインストールするためのレシピを紹介します。

アプリ パッケージをキャッシュに保存する

共有デバイスのユーザーがすべて共通のアプリセットを共有している場合は、可能な限りアプリをダウンロードしないようにする必要があります。Android 9.0(API レベル 28)以降では、シフト ワーカー用のデバイスなど、固定のユーザーセットがいる共有デバイスでのユーザー プロビジョニングを効率化するために、マルチユーザー セッションに必要なアプリ パッケージ(APK)をキャッシュに保存できます。

キャッシュされた APK(デバイスにすでにインストールされている APK)のインストールは、次の 2 つのステージで行われます。

  1. 完全管理対象デバイス(または委任 - 以下を参照)の管理コンポーネントが、デバイスに保持する APK のリストを設定します。
  2. 関連付けられたセカンダリ ユーザー(またはその代理人)の管理コンポーネントは、ユーザーに代わってキャッシュされた APK をインストールできます。完全管理対象デバイス、プライマリ ユーザー、関連する仕事用プロファイル(またはその代理人)の管理者も、必要に応じてキャッシュされたアプリをインストールできます。

デバイスに保持する APK のリストを設定するために、管理者は DevicePolicyManager.setKeepUninstalledPackages() を呼び出します。この方法では、APK がデバイスにインストールされていることを確認しません。ユーザーに必要になる直前にアプリをインストールしたい場合に便利です。以前に設定したパッケージのリストを取得するには、DevicePolicyManager.getKeepUninstalledPackages() を呼び出します。変更を指定して setKeepUninstalledPackages() を呼び出した後、またはセカンダリ ユーザーが削除されると、不要になったキャッシュされた APK はシステムによって削除されます。

キャッシュされた APK をインストールするには、DevicePolicyManager.installExistingPackage() を呼び出します。このメソッドでインストールできるのは、システムがすでにキャッシュに保存しているアプリのみです。このメソッドを呼び出す前に、まず専用のデバイス ソリューション(またはデバイスのユーザー)がデバイスにアプリをインストールする必要があります。

次のサンプルは、完全管理対象デバイスとセカンダリ ユーザーの管理者として、これらの API 呼び出しを使用する方法を示しています。

Kotlin

// Set the package to keep. This method assumes that the package is already
// installed on the device by managed Google Play.
val cachedAppPackageName = "com.example.android.myapp"
dpm.setKeepUninstalledPackages(adminName, listOf(cachedAppPackageName))

// ...

// The admin of a secondary user installs the app.
val success = dpm.installExistingPackage(adminName, cachedAppPackageName)

Java

// Set the package to keep. This method assumes that the package is already
// installed on the device by managed Google Play.
String cachedAppPackageName = "com.example.android.myapp";
List<String> packages = new ArrayList<String>();
packages.add(cachedAppPackageName);
dpm.setKeepUninstalledPackages(adminName, packages);

// ...

// The admin of a secondary user installs the app.
boolean success = dpm.installExistingPackage(adminName, cachedAppPackageName);

アプリを委任する

アプリのキャッシュの管理を別のアプリに委任できます。これは、ソリューションの機能を分離する場合や、IT 管理者が独自のアプリを使用できるようにする場合に行います。委任アプリは、管理コンポーネントと同じ権限を取得します。たとえば、セカンダリ ユーザーの管理者のアプリ デリゲートは、installExistingPackage() を呼び出すことはできますが、setKeepUninstalledPackages() を呼び出すことはできません。

デリゲートで DevicePolicyManager.setDelegatedScopes() を呼び出し、scopes 引数に DELEGATION_KEEP_UNINSTALLED_PACKAGES を含めます。次の例は、別のアプリをデリゲートにする方法を示しています。

Kotlin

var delegatePackageName = "com.example.tools.kept_app_assist"

// Check that the package is installed before delegating.
try {
    context.packageManager.getPackageInfo(delegatePackageName, 0)
    dpm.setDelegatedScopes(
            adminName,
            delegatePackageName,
            listOf(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES))
} catch (e: PackageManager.NameNotFoundException) {
    // The delegate app isn't installed. Send a report to the IT admin ...
}

Java

String delegatePackageName = "com.example.tools.kept_app_assist";

// Check that the package is installed before delegating.
try {
  context.getPackageManager().getPackageInfo(delegatePackageName, 0);
  dpm.setDelegatedScopes(
      adminName,
      delegatePackageName,
      Arrays.asList(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES));
} catch (PackageManager.NameNotFoundException e) {
  // The delegate app isn't installed. Send a report to the IT admin ...
}

問題がなければ、委譲アプリは ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED ブロードキャストを受信し、委譲になります。アプリは、デバイス所有者やプロファイル所有者の場合と同様に、このガイドのメソッドを呼び出すことができます。DevicePolicyManager メソッドを呼び出すと、デリゲートは管理コンポーネントの引数に null を渡します。

アプリ パッケージをインストールする

ローカル キャッシュに保存されたカスタムアプリを専用デバイスにインストールすると便利な場合があります。たとえば、専用デバイスは、帯域幅が限られている環境やインターネットに接続できないエリアに導入されることがよくあります。専用のデバイス ソリューションでは、顧客の帯域幅に注意する必要があります。アプリは PackageInstaller クラスを使用して別のアプリ パッケージ(APK)のインストールを開始できます。

APK はどのアプリでもインストールできますが、完全管理対象デバイスの管理者は、ユーザーの操作なしでパッケージをインストール(またはアンインストール)できます。管理者は、デバイス、関連するセカンダリ ユーザー、関連する仕事用プロファイルを管理できます。インストールが完了すると、すべてのデバイス ユーザーに表示される通知が送信されます。この通知は、管理者によってアプリがインストール(または更新)されたことをデバイス ユーザーに通知します。

表 2. ユーザーによる操作なしでのパッケージのインストールをサポートしている Android バージョン
Android バージョン インストールとアンインストール用の管理コンポーネント
Android 9.0(API レベル 28)以降 関連付けられているセカンダリ ユーザーと仕事用プロファイル(どちらも完全管理対象デバイス上)
Android 6.0(API レベル 23)以降 完全管理対象デバイス

APK の 1 つ以上のコピーを専用デバイスに配布する方法は、デバイスがどの程度リモートか、場合によってはデバイス間の距離に応じて異なります。専用デバイスに APK をインストールする前に、ソリューションがセキュリティのベスト プラクティスに従う必要があります。

PackageInstaller.Session を使用すると、1 つ以上の APK のインストールをキューに登録するセッションを作成できます。次の例では、アクティビティ(SingleTop モード)でステータス フィードバックを受け取りますが、サービスまたはブロードキャスト レシーバを使用することもできます。

Kotlin

// First, create a package installer session.
val packageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
        PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)

// Add the APK binary to the session. The APK is included in our app binary
// and is read from res/raw but file storage is a more typical location.
// The I/O streams can't be open when installation begins.
session.openWrite("apk", 0, -1).use { output ->
    getContext().resources.openRawResource(R.raw.app).use { input ->
        input.copyTo(output, 2048)
    }
}

// Create a status receiver to report progress of the installation.
// We'll use the current activity.
// Here we're requesting status feedback to our Activity but this can be a
// service or broadcast receiver.
val intent = Intent(context, activity.javaClass)
intent.action = "com.android.example.APK_INSTALLATION_ACTION"
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val statusReceiver = pendingIntent.intentSender

// Start the installation. Because we're an admin of a fully managed device,
// there isn't any user interaction.
session.commit(statusReceiver)

Java

// First, create a package installer session.
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = packageInstaller.createSession(params);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);

// Add the APK binary to the session. The APK is included in our app binary
// and is read from res/raw but file storage is a more typical location.
try (
    // These I/O streams can't be open when installation begins.
    OutputStream output = session.openWrite("apk", 0, -1);
    InputStream input = getContext().getResources().openRawResource(R.raw.app);
) {
  byte[] buffer = new byte[2048];
  int n;
  while ((n = input.read(buffer)) >= 0) {
    output.write(buffer, 0, n);
  }
}

// Create a status receiver to report progress of the installation.
// We'll use the current activity.
// Here we're requesting status feedback to our Activity but this can be a
// service or broadcast receiver.
Intent intent = new Intent(context, getActivity().getClass());
intent.setAction("com.android.example.APK_INSTALLATION_ACTION");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
IntentSender statusReceiver = pendingIntent.getIntentSender();

// Start the installation. Because we're an admin of a fully managed device,
// there isn't any user interaction.
session.commit(statusReceiver);

セッションは、インテントを使用してインストールに関するステータス フィードバックを送信します。各インテントの EXTRA_STATUS フィールドを確認してステータスを取得します。デバイスのユーザーがインストールを承認する必要がないため、管理者が STATUS_PENDING_USER_ACTION のステータス アップデートを受け取ることはありません。

アプリをアンインストールするには、PackageInstaller.uninstall を呼び出します。 完全管理対象デバイス、ユーザー、仕事用プロファイルの管理者は、サポート対象の Android バージョンを実行しているときに、ユーザーの操作なしでパッケージをアンインストールできます(表 2 を参照)。

システム アップデートを凍結する

Android デバイスは、システムとアプリ ソフトウェアの無線(OTA)アップデートを受信します。休日や繁忙期などの重要な期間に OS バージョンを凍結するために、専用デバイスで OTA システム アップデートを最大 90 日間一時停止できます。詳しくは、システム アップデートを管理するをご覧ください。

Remote Config

Android の管理対象設定を使用すると、IT 管理者はアプリをリモートで設定できます。IT 管理者にとってアプリの利便性を高めるために、許可リスト、ネットワーク ホスト、コンテンツ URL などの設定を公開することをおすすめします。

アプリがその構成を公開している場合は、必ずドキュメントに設定を含めてください。アプリの構成を公開し、設定の変更に対応する方法について詳しくは、管理対象構成をセットアップするをご覧ください。

開発のセットアップ

専用デバイス向けのソリューションを開発する際は、初期状態にリセットせずに完全管理対象デバイスの管理者としてアプリを設定することをおすすめします。完全管理対象デバイスの管理者を設定する手順は次のとおりです。

  1. Device Policy Controller(DPC)アプリをビルドしてデバイスにインストールします。
  2. デバイスにアカウントがないことを確認します。
  3. Android Debug Bridge(adb)シェルで次のコマンドを実行します。この例の com.example.dpc/.MyDeviceAdminReceiver は、アプリの管理コンポーネント名に置き換える必要があります。

    adb shell dpm set-device-owner com.example.dpc/.MyDeviceAdminReceiver

お客様によるソリューションのデプロイを支援するには、他の登録方法を検討する必要があります。専用デバイスでは QR コードの登録をおすすめします。

参考情報

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