專用裝置教戰手冊

本教戰手冊可協助開發人員和系統整合商強化專屬裝置解決方案。按照操作方法尋找專用裝置行為的解決方案。本教戰手冊最適合已擁有裝置專用應用程式的開發人員。如果您才剛開始使用,請參閱「專用裝置總覽」一文。

自訂 Google Home 應用程式

如果您開發的應用程式會取代 Android 主畫面和啟動器,這些方案都非常實用。

當 Google Home 應用程式

您可以將應用程式設為裝置的主畫面應用程式,使其在裝置啟動時自動啟動。您也可以啟用「主畫面」按鈕,在鎖定任務模式下將許可清單中的應用程式移至前景。

所有主畫面應用程式都會處理 CATEGORY_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>

一般來說,不會希望啟動器應用程式顯示在「總覽」畫面中。不過,您不需要將 excludeFromRecents 加入活動宣告中,因為系統在鎖定任務模式下執行時,Android 啟動器會隱藏最初啟動的活動。

顯示個別工作

FLAG_ACTIVITY_NEW_TASK 對啟動器類型應用程式而言相當實用,因為每個新工作都會在「總覽」畫面中顯示為個別項目。如要進一步瞭解「總覽」畫面中的工作,請參閱「最近使用」畫面

公用資訊站

這些食譜很適合在公共空間中的無人負責裝置,但也有助於許多專用裝置使用者專注在工作。

鎖定裝置

為確保裝置適用於其用途,您可以新增表 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 會分為以下兩個階段:

  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(),並在範圍引數中加入 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 的一或多個複本發布到專用裝置的方式取決於裝置的遠端方式,以及裝置之間的距離。您的解決方案必須遵循安全性最佳做法,才能將 APK 安裝到專用裝置上。

您可以使用 PackageInstaller.Session 建立工作階段,並將一或多個 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。請注意,管理員不會收到 STATUS_PENDING_USER_ACTION 狀態更新,因為裝置使用者不需要核准安裝作業。

如要解除安裝應用程式,可以呼叫 PackageInstaller.uninstall。 全代管裝置、使用者和工作資料夾的管理員都可以解除安裝套件,不必與執行支援的 Android 版本互動的使用者互動 (請參閱表 2)。

凍結系統更新

Android 裝置會接收系統和應用程式軟體的無線更新 (OTA)。如要在關鍵期間 (例如假日或其他忙碌時間) 凍結 OS 版本,專用裝置可能會暫停 OTA 系統更新,最長可達 90 天。詳情請參閱管理系統更新

遠端設定

Android 的受管理設定可讓 IT 管理員從遠端設定應用程式。您可能需要公開許可清單、網路主機或內容網址等設定,讓應用程式更適合 IT 管理員使用。

如果您的應用程式公開了設定,請記得在說明文件中納入相關設定。如要進一步瞭解如何公開應用程式的設定,以及如何回應設定的變更,請參閱「調整受管理的設定」。

開發設定

當您為專用裝置開發解決方案時,有時將應用程式設為全代管裝置管理員但不恢復原廠設定。如要設定全代管裝置的管理員,請按照下列步驟操作:

  1. 在裝置上安裝並安裝裝置政策控制器 (DPC) 應用程式。
  2. 確認裝置上沒有帳戶。
  3. Android Debug Bridge (adb) 殼層中執行下列指令。您需要將範例中的 com.example.dpc/.MyDeviceAdminReceiver 替換成應用程式的管理員元件名稱:

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

為協助客戶部署您的解決方案,請參閱其他註冊方法。建議針對特定裝置進行 QR code 註冊

其他資源

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