在 Android 11 及更高版本中,“快速访问设备控制器”功能可让用户通过 Android 电源菜单快速查看和控制外部设备(例如灯、恒温器和相机)。设备集合商家(例如 Google Home)和第三方供应商应用可以提供需在此工作区中显示的设备。本指南将向您介绍此工作区中设备控制器的布局,以及将其关联至控制应用的方式。
如需添加此支持,请创建并声明 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 提供了用于创建控件的构建器方法。如需填充构建器,您需要确定要控制的设备以及用户与其互动的方式。具体而言,您需要执行以下操作:
- 选择控件代表的设备类型。
DeviceTypes
类是对当前支持的所有设备的枚举。类型用于确定设备在界面中的图标和颜色。 - 确定面向用户显示的名称、设备所在位置(例如厨房)以及与控件关联的其他界面文本元素。
- 选择最佳模板,为用户互动提供支持。系统会为控件分配一个来自应用的
ControlTemplate
。此模板会直接向用户显示控件状态以及可用的输入方法(即ControlAction
)。下表概述了一些可用的模板及其支持的操作:
模板 | 操作 | 说明 |
ControlTemplate.getNoTemplateObject()
|
None
|
应用可以使用此模板来传递有关该控件的信息,但用户无法与其进行互动。 |
ToggleTemplate
|
BooleanAction
|
表示可在启用和停用状态之间切换的控件。BooleanAction 对象包含一个字段,当用户轻触控件时,该字段会更改以表示请求的新状态。
|
RangeTemplate
|
FloatAction
|
表示指定了最小值、最大值和步长值的滑块微件。当用户与滑块互动时,系统应将新的 FloatAction 对象发送回具有更新值的应用。
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
此模板是 ToggleTemplate 和 RangeTemplate 的组合。它支持触摸事件和滑块,例如,在控制可调光灯的控件中。
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
除了封装上述其中一项操作之外,此模板还允许用户设置模式,如制暖、制冷、适温、节能或关闭。 |
StatelessTemplate
|
CommandAction
|
用于指示提供触摸功能但无法确定其状态的控件,例如红外线电视遥控器。您可以使用此模板定义常规操作或宏,其中聚合了控件和状态更改。 |
了解这些信息后,现在您可以创建控件:
- 如果控件的状态未知,就使用
Control.StatelessBuilder
构建器类。 - 如果控件的状态已知,就使用
Control.StatefulBuilder
构建器类。
为控件创建发布者
创建控件后,该控件需要一个发布者。发布者将告知系统界面该控件的存在。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 PublishercreatePublisherForAllAvailable() { 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: ReplayProcessoroverride 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 ReplayProcessorupdatePublisher; @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, Consumerconsumer) { /* 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); } }