控制外部裝置

在 Android 11 以上版本中,使用者可透過「快速存取裝置控制」功能,在預設啟動器的三次互動中,以及功能可見的情況下,快速檢視及控制外部裝置,例如指示燈、溫度控制器及攝影機。裝置的 OEM 會選擇要使用的啟動器。裝置集結網站 (例如 Google Home) 和第三方供應商應用程式可以在此提供要顯示的裝置。本頁說明如何在這個區域中顯示裝置控制,以及將其連結至控制應用程式。

圖 1. Android UI 中的裝置控制區。

如要新增這項支援,請建立並宣告 ControlsProviderService。根據預先定義的控制類型建立應用程式支援的控制項,接著建立這些控制項的發布者。

使用者介面

裝置會以樣板化的小工具顯示在「裝置控制」中。目前有五個裝置控制小工具可以使用,如下圖所示:

切換裝置控制選項的小工具
切換
使用滑桿小工具切換
使用滑桿切換
裝置控制項的範圍滑桿小工具
範圍 (無法開啟或關閉)
無狀態的切換控制項小工具
無狀態切換
溫度面板小工具 (關閉狀態)
溫度面板 (關閉)
圖 2. 範本化的小工具集合。

按住小工具可前往應用程式進行更多控制。可以自訂每個小工具的圖示和顏色,但為了維護最佳的使用者體驗,如果預設設定與裝置相符,建議使用預設圖示和顏色。

開啟狀態的溫度面板小工具
圖 3. 開啟溫度面板小工具。

建立服務

本節說明如何建立 ControlsProviderService。這項服務會通知 Android 系統 UI,應用程式具有應列於 Android UI 中裝置控制區域的裝置控制。

就如同在 Reactive Streams GitHub 專案所定義以及在 Java 9 流程介面中所實作的,ControlsProviderService API 會預期有類似的回應式訊息串。這項 API 是以以下概念為基礎:

  • 發布者:您的應用程式是發布者。
  • 訂閱者:系統 UI 是訂閱者,可要求發布者提出一些控制選項。
  • 訂閱:發布商可傳送更新至系統 UI 的時間範圍。發布商或訂閱者都可以關閉這個視窗。

宣告服務

應用程式必須在應用程式資訊清單中宣告服務,例如 MyCustomControlService

服務必須為 ControlsProviderService 加上一個意圖篩選器。該篩選器可讓應用程式為系統 UI 提供控制功能。

您還需要顯示在系統 UI 控制項中的 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 類別是所有支援裝置的列舉。該類型用於決定 UI 中裝置的圖示和顏色。
  2. 決定向使用者顯示的名稱、裝置位置 (例如廚房) 以及其他與控制項相關的 UI 文字元素。
  3. 選擇最適合用來支援使用者互動的範本。應用程式會指派一個 ControlTemplate 給控制項。該範本可直接向使用者顯示控制狀態,以及可用的輸入方法 (即 ControlAction)。下表彙整了部分可用的範本以及支援的操作:
範本 操作 說明
ControlTemplate.getNoTemplateObject() None 應用程式可用來傳送控制資訊,但使用者無法與其互動。
ToggleTemplate BooleanAction 代表該控制項可在啟用及停用狀態之間切換。BooleanAction 物件包含一個欄位,當使用者輕觸控制項時,其內容會變更以顯示要求的新狀態。
RangeTemplate FloatAction 代表含有指定最小值、最大值和步驟值的滑桿小工具。當使用者與滑桿互動時,請將新的 FloatAction 物件連同更新的值傳回應用程式。
ToggleRangeTemplate BooleanAction, FloatAction 本範本是 ToggleTemplateRangeTemplate 的組合。可支援觸控事件及滑桿,例如可調暗燈光的控制項。
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() {
    // ...
}

為控制項建立發布商

建立控制項之後,需要有發布商。發布商會告知系統 UI 控制項的存在。ControlsProviderService 類別有兩個發布商方法,必須在應用程式的程式碼中將其覆寫:

  • createPublisherForAllAvailable:為應用程式中的所有控制項建立 Publisher。使用 Control.StatelessBuilder 為此發布商建構 Control 物件。
  • createPublisherFor:為給定的控制項清單建立 Publisher,控制項以其字串識別碼作為識別。使用 Control.StatefulBuilder 建立這些 Control 物件,因為發布商必須為每個控制項指定一種狀態。

建立發布商

應用程式首次將控制項發布至系統 UI 時,應用程式不知道各個控制項的狀態。取得狀態可能會相當耗時,這項作業涉及裝置與供應商網路中的許多躍點。使用 createPublisherForAllAvailable 方法向系統推送可用的控制項。這個方法使用 Control.StatelessBuilder 建構工具類別,因為每個控制項的狀態不明。

Android UI 顯示這些控制項後,使用者就能選取喜愛的控制項。

如要使用 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 UI 中更新裝置狀態。

如要完成範例,請在 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. 控制燈光和溫度控制器。