專用裝置教戰手冊

此教戰手冊可協助開發人員和系統整合商改進專屬的裝置解決方案。按照我們的使用指南,尋找適用於專屬裝置行為的解決方案。這個教戰手冊最適合已有專屬裝置應用程式的開發人員。如果您剛開始使用,請參閱「專用裝置總覽」一文。

自訂主畫面應用程式

如果您正在開發可取代 Android 主畫面和啟動器的應用程式,這些食譜就非常實用。

成為 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_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 註冊

其他資源

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