专用设备菜谱

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

自定义主屏幕应用

如果您正在开发一个应用来替换 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

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

其他资源

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