专用设备实战宝典

本实战宝典可帮助开发者和系统集成商增强其专用设备解决方案。请按照我们的方法指南查找专用设备行为的解决方案。本实战宝典最适合已拥有专用设备应用的开发者。如果您刚刚开始使用,请参阅专用设备概览

自定义 Home 应用

如果您正在开发用于替换 Android 主屏幕和启动器的应用,这些方案非常有用。

成为主屏幕应用

您可以将应用设为设备的主屏幕应用,使其在设备启动时自动启动。您还可以启用“主屏幕”按钮,在锁定任务模式下将列入许可名单的应用置于前台。

所有主屏幕应用都会处理 CATEGORY_HOME intent 类别,系统就是通过这种方式识别主屏幕应用。如需成为默认主屏幕应用,请通过调用 DevicePolicyManager.addPersistentPreferredActivity() 将应用的某个 activity 设置为首选主屏幕 intent 处理程序,如以下示例所示:

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);

您仍然需要在应用清单文件中声明 intent 过滤器,如以下 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>

通常,您不希望启动器应用显示在“Overview”屏幕中。不过,您无需将 excludeFromRecents 添加到 activity 声明中,因为当系统在锁定任务模式下运行时,Android 启动器会隐藏最初启动的 activity。

显示单独的任务

对于启动器类型应用,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 加入队列以进行安装。在以下示例中,我们在 activity(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);

会话使用 intent 发送有关安装的状态反馈。请检查每个意图的 EXTRA_STATUS 字段以获取状态。请注意,管理员不会收到 STATUS_PENDING_USER_ACTION 状态更新,因为设备用户不需要批准安装。

如需卸载应用,您可以调用 PackageInstaller.uninstall。完全受管设备、用户和工作资料的管理员可以卸载运行受支持的 Android 版本的软件包,而无需用户互动(请参阅表 2)。

冻结系统更新

Android 设备会收到系统和应用软件的无线下载 (OTA) 更新。如需在关键时段(例如节假日或其他繁忙时段)冻结操作系统版本,专用设备可以暂停 OTA 系统更新长达 90 天。如需了解详情,请参阅管理系统更新

远程配置

Android 的托管配置允许 IT 管理员远程配置您的应用。您可能需要公开许可名单、网络主机或内容网址等设置,以使您的应用对 IT 管理员更有用。

如果您的应用公开了其配置,请务必在文档中添加这些设置。如需详细了解如何公开应用的配置以及如何应对设置变更,请阅读设置托管配置

开发设置

在为专用设备开发解决方案时,有时在未恢复出厂设置的情况下将应用设置为全代管式设备的管理员会很有用。要设置完全受管设备的管理员,请按以下步骤操作:

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

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

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

其他资源

如需详细了解专用设备,请参阅以下文档: