专用设备菜谱

本菜谱可帮助开发者和系统集成商增强其专用设备解决方案。按照我们的操作指南食谱查找专用设备行为的解决方案。本菜谱最适合已经拥有专用设备应用的开发者 - 如果您刚开始,请阅读 专用设备概述

自定义主页应用

如果您正在开发应用以替换 Android 主屏幕和启动器,这些食谱将很有用。

成为主页应用

您可以将您的应用设置为设备的主页应用,以便在设备启动时自动启动。您也可以 启用主页按钮,该按钮将在锁定任务模式下将您的允许列表应用带到前台。

所有主页应用都处理 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);
}

您可能希望检查设备是否已连接到交流电源、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) 更新。为了在关键时期(例如假期或其他繁忙时间)冻结操作系统版本,专用设备可以暂停 OTA 系统更新长达 90 天。要了解更多信息,请阅读 管理系统更新.

远程配置

Android 的 托管配置 允许 IT 管理员远程配置您的应用程序。您可能希望公开设置,例如允许列表、网络主机或内容 URL,以使您的应用程序对 IT 管理员更有用。

如果您的应用程序公开了其配置,请务必在您的文档中包含这些设置。要了解有关公开应用程序配置和对设置更改做出反应的更多信息,请阅读 设置托管配置.

开发设置

在为专用设备开发解决方案时,将应用程序设置为完全托管设备的管理员(无需出厂重置)有时很有用。要设置完全托管设备的管理员,请执行以下步骤

  1. 在设备上构建并安装您的设备策略控制器 (DPC) 应用程序。
  2. 检查设备上是否没有帐户。
  3. Android 调试桥 (adb) shell 中运行以下命令。您需要用应用程序的管理员组件名称替换示例中的 com.example.dpc/.MyDeviceAdminReceiver

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

为了帮助客户部署您的解决方案,您需要查看 其他注册方法。我们建议使用 QR 码注册 用于专用设备。

其他资源

要了解有关专用设备的更多信息,请阅读以下文档