控制外部设备

在 Android 11 及更高版本中,“快速访问设备控制器”功能可让用户通过 Android 电源菜单快速查看和控制外部设备(例如灯、恒温器和相机)。设备集合商家(例如 Google Home)和第三方供应商应用可以提供需在此工作区中显示的设备。本指南将向您介绍此工作区中设备控制器的布局,以及将其关联至控制应用的方式。

Android 界面中的设备控制器工作区

如需添加此支持,请创建并声明 ControlsProviderService,根据预定义的控件类型创建应用支持的控件,然后为这些控件创建发布者。

界面

设备以模板化微件的形式显示在设备控制器下。有五个不同的设备控制器微件可供使用:

切换开关微件
切换开关
“带滑块的切换开关”微件
带滑块的切换开关
范围微件
范围(无法切换为开启或关闭)
无状态切换开关微件
无状态切换开关
温度面板微件(已关闭)
温度面板(已关闭)
温度面板微件(已打开)
温度面板(已打开)

长按某个微件可以转到相应的应用,让您进行更多控制操作。您可以自定义每个微件的图标和颜色,但为了提供最佳用户体验,建议您使用默认的图标和颜色,除非默认设置与设备不匹配。

创建服务

本部分介绍如何创建 ControlsProviderService。此服务告知 Android 系统界面,您的应用包含的设备控制器应显示在 Android 界面的设备控制器区域中。

ControlsProviderService API 假定您熟悉响应式流 GitHub 项目中定义的响应式流以及在 Java 9 流界面中实现的响应式流。 该 API 围绕以下概念构建而成:

  • 发布者:您的应用是发布者
  • 订阅者:系统界面是订阅者,可以向发布者请求多个控件
  • 订阅:发布者可以向系统界面发送更新的时间范围;发布者或订阅者可以关闭这个时间范围

声明服务

您的应用必须在其应用清单中声明服务。确保包含 BIND_CONTROLS 权限。

服务必须包含 ControlsProviderService 的 intent 过滤器。此过滤器能够让应用控制系统界面。

<!-- New signature permission to ensure only systemui can bind to these services -->
<service android:name="YOUR-SERVICE-NAME" android:label="YOUR-SERVICE-LABEL"
    android:permission="android.permission.BIND_CONTROLS">
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

选择正确的控件类型

API 提供了用于创建控件的构建器方法。如需填充构建器,您需要确定要控制的设备以及用户与其互动的方式。具体而言,您需要执行以下操作:

  1. 选择控件代表的设备类型。DeviceTypes 类是对当前支持的所有设备的枚举。类型用于确定设备在界面中的图标和颜色。
  2. 确定面向用户显示的名称、设备所在位置(例如厨房)以及与控件关联的其他界面文本元素。
  3. 选择最佳模板,为用户互动提供支持。系统会为控件分配一个来自应用的 ControlTemplate。此模板会直接向用户显示控件状态以及可用的输入方法(即 ControlAction)。下表概述了一些可用的模板及其支持的操作:
模板 操作 说明
ControlTemplate.getNoTemplateObject() None 应用可以使用此模板来传递有关该控件的信息,但用户无法与其进行互动。
ToggleTemplate BooleanAction 表示可在启用和停用状态之间切换的控件。BooleanAction 对象包含一个字段,当用户轻触控件时,该字段会更改以表示请求的新状态。
RangeTemplate FloatAction 表示指定了最小值、最大值和步长值的滑块微件。当用户与滑块互动时,系统应将新的 FloatAction 对象发送回具有更新值的应用。
ToggleRangeTemplate BooleanAction, FloatAction 此模板是 ToggleTemplateRangeTemplate 的组合。它支持触摸事件和滑块,例如,在控制可调光灯的控件中。
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction 除了封装上述其中一项操作之外,此模板还允许用户设置模式,如制暖、制冷、适温、节能或关闭。
StatelessTemplate CommandAction 用于指示提供触摸功能但无法确定其状态的控件,例如红外线电视遥控器。您可以使用此模板定义常规操作或宏,其中聚合了控件和状态更改。

了解这些信息后,现在您可以创建控件:

为控件创建发布者

创建控件后,该控件需要一个发布者。发布者将告知系统界面该控件的存在。ControlsProviderService 类有两种您必须在应用代码中替换的发布者方法:

  • createPublisherForAllAvailable():为应用中提供的所有控件创建一个 Publisher。使用 Control.StatelessBuilder() 为该发布者构建 Controls
  • createPublisherFor():为一组给定的控件创建一个 Publisher,每个控件由其字符串标识符标识。使用 Control.StatefulBuilder 构建这些 Controls,因为发布者必须为每个控件分配一个状态。

创建发布者

当您的应用首次将控件发布到系统界面时,应用不知道每个控件的状态。获取状态可能是一项非常耗时的操作,涉及设备提供商网络中的许多跃点。使用 createPublisherForAllAvailable() 方法将可用的控件告知系统。请注意,此方法使用 Control.StatelessBuilder 构建器类,因为每个控件的状态都是未知的。

控件显示在 Android 界面中后,用户就可以选择感兴趣的控件(即选择收藏夹)了。

Kotlin

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher {
        val context: Context = baseContext
        val i = Intent()
        val pi =
            PendingIntent.getActivity(
                context, CONTROL_REQUEST_CODE, i,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        val controls = mutableListOf()
        val control =
            Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
                // Required: The name of the control
                .setTitle(MY-CONTROL-TITLE)
                // Required: Usually the room where the control is located
                .setSubtitle(MY-CONTROL-SUBTITLE)
                // Optional: Structure where the control is located, an example would be a house
                .setStructure(MY-CONTROL-STRUCTURE)
                // Required: Type of device, i.e., thermostat, light, switch
                .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                .build()
        controls.add(control)
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls))
    }
}

Java

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
public class MyCustomControlService extends ControlsProviderService {

    @Override
    public Publisher createPublisherForAllAvailable() {
        Context context = getBaseContext();
        Intent i = new Intent();
        PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);
        List controls = new ArrayList<>();
        Control control = new Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
          // Required: The name of the control
          .setTitle(MY-CONTROL-TITLE)
          // Required: Usually the room where the control is located
          .setSubtitle(MY-CONTROL-SUBTITLE)
          // Optional: Structure where the control is located, an example would be a house
          .setStructure(MY-CONTROL-STRUCTURE)
          // Required: Type of device, i.e., thermostat, light, switch
          .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
          .build();
        controls.add(control);
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
    }
}

用户选择一组控件后,请仅为这些控件创建发布者。使用 createPublisherFor() 方法,原因是此方法使用 Control.StatefulBuilder 构建器类,该类提供每个控件的当前状态(例如,启用或停用)。

Kotlin

class MyCustomControlService : ControlsProviderService() {
    private lateinit var updatePublisher: ReplayProcessor

    override fun createPublisherFor(controlIds: MutableList): Flow.Publisher {
        val context: Context = baseContext
        /* Fill in details for the activity related to this device. On long press,
         * this Intent will be launched in a bottomsheet. Please design the activity
         * accordingly to fit a more limited space (about 2/3 screen height).
         */
        val i = Intent(this, CustomSettingsActivity::class.java)
        val pi =
            PendingIntent.getActivity(context, CONTROL_REQUEST_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT)
        updatePublisher = ReplayProcessor.create()

        if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {
            val control =
                Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
                    // Required: The name of the control
                    .setTitle(MY-CONTROL-TITLE)
                    // Required: Usually the room where the control is located
                    .setSubtitle(MY -CONTROL-SUBTITLE)
                    // Optional: Structure where the control is located, an example would be a house
                    .setStructure(MY-CONTROL-STRUCTURE)
                    // Required: Type of device, i.e., thermostat, light, switch
                    .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                    // Required: Current status of the device
                    .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
                    .build()

            updatePublisher.onNext(control)
        }

        // If you have other controls, check that they have been selected here

        // Uses the Reactive Streams API
        updatePublisher.onNext(control)
    }
}

Java

private ReplayProcessor updatePublisher;

@Override
public Publisher createPublisherFor(List controlIds) {

    Context context = getBaseContext();
    /* Fill in details for the activity related to this device. On long press,
     * this Intent will be launched in a bottomsheet. Please design the activity
     * accordingly to fit a more limited space (about 2/3 screen height).
     */
    Intent i = new Intent();
    PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);

    updatePublisher = ReplayProcessor.create();

    // For each controlId in controlIds

    if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {

      Control control = new Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
        // Required: The name of the control
        .setTitle(MY-CONTROL-TITLE)
        // Required: Usually the room where the control is located
        .setSubtitle(MY-CONTROL-SUBTITLE)
        // Optional: Structure where the control is located, an example would be a house
        .setStructure(MY-CONTROL-STRUCTURE)
        // Required: Type of device, i.e., thermostat, light, switch
        .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
        // Required: Current status of the device
        .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
        .build();

      updatePublisher.onNext(control);
    }
    // Uses the Reactive Streams API
    return FlowAdapters.toFlowPublisher(updatePublisher);
}

处理操作

performControlAction() 方法会发出信号,表明用户已与已发布的控件进行了互动。操作由已发送的 ControlAction 类型指定。对给定的控件执行适当的操作,然后在 Android 界面中更新设备的状态。

Kotlin

class MyCustomControlService : ControlsProviderService() {

    override fun performControlAction(
        controlId: String, action: ControlAction, consumer: Consumer) {

        /* First, locate the control identified by the controlId. Once it is located, you can
         * interpret the action appropriately for that specific device. For instance, the following
         * assumes that the controlId is associated with a light, and the light can be turned on
         * or off.
         */
        if (action is BooleanAction) {

            // Inform SystemUI that the action has been received and is being processed
            consumer.accept(ControlAction.RESPONSE_OK)

            // In this example, action.getNewState() will have the requested action: true for “On”,
            // false for “Off”.

            /* This is where application logic/network requests would be invoked to update the state of
             * the device.
             * After updating, the application should use the publisher to update SystemUI with the new
             * state.
             */
            Control control = Control.StatefulBuilder (MY-UNIQUE-DEVICE-ID, pi)
            // Required: The name of the control
              .setTitle(MY-CONTROL-TITLE)
              // Required: Usually the room where the control is located
              .setSubtitle(MY-CONTROL-SUBTITLE)
              // Optional: Structure where the control is located, an example would be a house
              .setStructure(MY-CONTROL-STRUCTURE)
              // Required: Type of device, i.e., thermostat, light, switch
              .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
              // Required: Current status of the device
              .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
              .build()

            // This is the publisher the application created during the call to createPublisherFor()
            updatePublisher.onNext(control)
        }
    }
}

Java

@Override
public void performControlAction(String controlId, ControlAction action,
    Consumer consumer) {

  /* First, locate the control identified by the controlId. Once it is located, you can
   * interpret the action appropriately for that specific device. For instance, the following
   * assumes that the controlId is associated with a light, and the light can be turned on
   * or off.
   */
  if (action instanceof BooleanAction) {

    // Inform SystemUI that the action has been received and is being processed
    consumer.accept(ControlAction.RESPONSE_OK);

    BooleanAction action = (BooleanAction) action;
    // In this example, action.getNewState() will have the requested action: true for “On”,
    // false for “Off”.

    /* This is where application logic/network requests would be invoked to update the state of
     * the device.
     * After updating, the application should use the publisher to update SystemUI with the new
     * state.
     */
    Control control = new Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
      // Required: The name of the control
      .setTitle(MY-CONTROL-TITLE)
      // Required: Usually the room where the control is located
      .setSubtitle(MY-CONTROL-SUBTITLE)
      // Optional: Structure where the control is located, an example would be a house
      .setStructure(MY-CONTROL-STRUCTURE)
      // Required: Type of device, i.e., thermostat, light, switch
      .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
      // Required: Current status of the device
      .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
      .build();

    // This is the publisher the application created during the call to createPublisherFor()
    updatePublisher.onNext(control);
  }
}