专用设备开发手册

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

自定义主屏幕应用

如果您正在开发一款用于替换 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>

通常,您不希望您的启动器应用显示在概览屏幕中。但是,您无需将 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);
}

如果主用户或辅助用户的管理员设置此限制,系统将仅抑制该用户的错误对话框。如果完全托管设备的管理员设置此限制,系统将抑制所有用户的对话框。

保持屏幕常亮

如果您正在构建自助服务终端,则可以在设备运行您的应用 Activity 时阻止设备进入休眠状态。按照以下示例所示,将 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() 并在 scopes 参数中包含 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 发送有关安装的状态反馈。检查每个 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

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

其他资源

要详细了解专用设备,请阅读以下文档