控制外部设备

在 Android 11 及更高版本中,借助“快速访问设备控制器”功能,用户只需在默认启动器中执行三次互动,即可通过用户功能快速查看和控制外部设备(例如灯、恒温器和相机)。设备 OEM 可选择要使用的启动器。设备集合商家(例如 Google Home)和第三方供应商应用可以提供需在此工作区中显示的设备。本页面介绍了如何在此空间中显示设备控件,并将其关联到您的控件应用。

图 1. Android 界面中的设备控制空间。

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

界面

设备以模板化微件的形式显示在设备控制器下。有五个设备控件 widget 可用,如下图所示:

切换设备控制微件
切换
“带滑块的切换开关”微件
使用滑块切换
用于设备控件的范围滑块微件
范围(无法开启或关闭)
无状态切换开关控件 widget
无状态切换开关
处于关闭状态的温度面板微件
温度面板(已关闭)
图 2. 模板化 widget 的集合。

轻触并按住微件可进入应用,进行更深入的控制。您可以自定义每个微件上的图标和颜色,但为了获得最佳用户体验,如果默认设置与设备匹配,请使用默认图标和颜色。

处于打开状态的温度面板微件
图 3. 打开温度面板微件。

创建服务

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

ControlsProviderService API 假定您熟悉响应式流(如 Reactive Streams GitHub 项目中所定义,并在 Java 9 Flow 接口中实现)。该 API 基于以下概念:

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

声明服务

您的应用必须在其应用清单中声明服务(例如 MyCustomControlService)。

服务必须包含 ControlsProviderService 的 intent 过滤器。此过滤器可让应用向系统界面提供控件。

您还需要一个 label,它会显示在系统界面中的控件中。

以下示例展示了如何声明服务:

<service
    android:name="MyCustomControlService"
    android:label="My Custom Controls"
    android:permission="android.permission.BIND_CONTROLS"
    android:exported="true"
    >
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

接下来,创建一个名为 MyCustomControlService.kt 的新 Kotlin 文件,并使其扩展 ControlsProviderService:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

选择正确的控件类型

API 提供了用于创建控件的构建器方法。如需填充构建器,请确定您要控制的设备以及用户与该设备的互动方式。执行以下步骤:

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

有了这些信息,您就可以创建控件:

例如,如需控制智能灯泡和温控器,请将以下常量添加到 MyCustomControlService:

private const val LIGHT_ID = 1234
private const val LIGHT_TITLE = "My fancy light"
private const val LIGHT_TYPE = DeviceTypes.TYPE_LIGHT
private const val THERMOSTAT_ID = 5678
private const val THERMOSTAT_TITLE = "My fancy thermostat"
private const val THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT

class MyCustomControlService : ControlsProviderService() {
    // ...
}

为控件创建发布者

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

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

创建发布者

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

控件显示在 Android 界面中后,用户可以选择收藏的控件。

如需使用 Kotlin 协程创建 ControlsProviderService,请向 build.gradle 添加新的依赖项:

Groovy

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
}

同步 Gradle 文件后,将以下代码段添加到 Service 以实现 createPublisherForAllAvailable:

class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
        flowPublish {
            send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE))
            send(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE))
        }

    private fun createStatelessControl(id: Int, title: String, type: Int): Control {
        val intent = Intent(this, MainActivity::class.java)
            .putExtra(EXTRA_MESSAGE, title)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        val action = PendingIntent.getActivity(
            this,
            id,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )

        return Control.StatelessBuilder(id.toString(), action)
            .setTitle(title)
            .setDeviceType(type)
            .build()
    }

    override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
        TODO()
    }

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer<Int>,
    ) {
        TODO()
    }
}

向下滑动系统菜单,找到设备控制器按钮,如图 4 所示:

用于设备控制的系统界面
图 4. 系统菜单中的设备控制器。

点按设备控制器会转到第二个屏幕,您可以在其中选择应用。选择应用后,您会看到前面的代码段如何创建显示新控件的自定义系统菜单,如图 5 所示:

显示灯光和温控器控件的系统菜单
图 5. 要添加的灯具和温控器控件。

现在,实现 createPublisherFor 方法,将以下内容添加到 Service:

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val controlFlows = mutableMapOf<String, MutableSharedFlow<Control>>()

private var toggleState = false
private var rangeState = 18f

override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
    val flow = MutableSharedFlow<Control>(replay = 2, extraBufferCapacity = 2)

    controlIds.forEach { controlFlows[it] = flow }

    scope.launch {
        delay(1000) // Retrieving the toggle state.
        flow.tryEmit(createLight())

        delay(1000) // Retrieving the range state.
        flow.tryEmit(createThermostat())
    }
    return flow.asPublisher()
}

private fun createLight() = createStatefulControl(
    LIGHT_ID,
    LIGHT_TITLE,
    LIGHT_TYPE,
    toggleState,
    ToggleTemplate(
        LIGHT_ID.toString(),
        ControlButton(
            toggleState,
            toggleState.toString().uppercase(Locale.getDefault()),
        ),
    ),
)

private fun createThermostat() = createStatefulControl(
    THERMOSTAT_ID,
    THERMOSTAT_TITLE,
    THERMOSTAT_TYPE,
    rangeState,
    RangeTemplate(
        THERMOSTAT_ID.toString(),
        15f,
        25f,
        rangeState,
        0.1f,
        "%1.1f",
    ),
)

private fun <T> createStatefulControl(
    id: Int,
    title: String,
    type: Int,
    state: T,
    template: ControlTemplate,
): Control {
    val intent = Intent(this, MainActivity::class.java)
        .putExtra(EXTRA_MESSAGE, "$title $state")
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    val action = PendingIntent.getActivity(
        this,
        id,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

    return Control.StatefulBuilder(id.toString(), action)
        .setTitle(title)
        .setDeviceType(type)
        .setStatus(Control.STATUS_OK)
        .setControlTemplate(template)
        .build()
}

override fun onDestroy() {
    super.onDestroy()
    job.cancel()
}

在此示例中,createPublisherFor 方法包含应用必须执行的操作的虚假实现:与设备通信以检索其状态,并将该状态发送到系统。

createPublisherFor 方法使用 Kotlin 协程和流,通过执行以下操作来满足所需的 Reactive Streams API:

  1. 创建 Flow。
  2. 等待一秒。
  3. 创建并发出智能灯的状态。
  4. 再等待一秒。
  5. 创建并发出温控器的状态。

处理操作

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

如需完成该示例,请将以下内容添加到 Service:

override fun performControlAction(
    controlId: String,
    action: ControlAction,
    consumer: Consumer<Int>,
) {
    controlFlows[controlId]?.let { flow ->
        when (controlId) {
            LIGHT_ID.toString() -> {
                consumer.accept(ControlAction.RESPONSE_OK)
                if (action is BooleanAction) toggleState = action.newState
                flow.tryEmit(createLight())
            }
            THERMOSTAT_ID.toString() -> {
                consumer.accept(ControlAction.RESPONSE_OK)
                if (action is FloatAction) rangeState = action.newValue
                flow.tryEmit(createThermostat())
            }
            else -> consumer.accept(ControlAction.RESPONSE_FAIL)
        }
    } ?: consumer.accept(ControlAction.RESPONSE_FAIL)
}

运行应用,访问设备控件菜单,然后查看灯光和温控器控件。

显示灯具和温控器的控件
图 6. 灯光和温控器控制。