控制外部裝置

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

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

如要新增這項支援功能,請建立並宣告 ControlsProviderService。建立 以預先定義的控制項類型為依據 以便使用這些控制項

使用者介面

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

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

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

顯示溫度面板小工具 (開啟) 的圖片
圖 3. 開啟溫度面板小工具。

建立服務

本節將說明如何建立 ControlsProviderService。 這項服務會告知 Android 系統 UI,您的應用程式含有裝置控制項 都必須顯示在 Android UI 的「Device controls」區域。

ControlsProviderService API 會預期有類似的回應式訊息串,就如同在 Reactive Streams GitHub 專案所定義以及在 Java 9 流程介面中所實作的。此 API 是圍繞著以下概念建構而成:

  • 發布商:應用程式是由發布商負責。
  • 訂閱者:系統 UI 是訂閱者,可要求號碼 這些都是重要的控管選項
  • 訂閱:發布商可傳送更新的時間範圍 。發布商或訂閱者都可以關閉這個應用程式 視窗。

宣告服務

您的應用程式必須在以下位置宣告服務 (例如 MyCustomControlService) 含有應用程式資訊清單。

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

您還需要一個 label,顯示在系統 UI 中的控制項中。

以下範例說明如何宣告服務:

<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()

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

選取正確的控制項類型

API 提供建構工具方法以建立控制選項。為了填入 建構工具,決定要控制的裝置,以及使用者與互動的方式 。請執行下列步驟:

  1. 選擇控制項所代表的裝置類型。DeviceTypes 類別是所有支援裝置的列舉。類型可用來決定 使用者介面中裝置的圖示和顏色。
  2. 決定向使用者顯示的名稱、裝置位置 (例如廚房) 以及其他與控制項相關的 UI 文字元素。
  3. 選擇最適合用來支援使用者互動的範本。應用程式會指派一個 ControlTemplate 給控制項。這個範本會直接向 以及可用的輸入方式,也就是 ControlAction。 下表大致列出部分可用的範本和操作 他們支援的瀏覽器:
範本 操作 說明
ControlTemplate.getNoTemplateObject() None 應用程式可能會利用這個物件來傳送控制項的相關資訊、 但無法與其互動
ToggleTemplate BooleanAction 代表該控制項可在啟用及停用狀態之間切換。BooleanAction 物件包含會變更的欄位 在使用者輕觸控制項時,代表要求的新狀態。
RangeTemplate FloatAction 代表含有指定最小值、最大值和步驟值的滑桿小工具。當使用者與滑桿互動時,請將新的 FloatAction 物件傳回應用程式,並附上更新的值。
ToggleRangeTemplate BooleanAction, FloatAction 本範本是 ToggleTemplateRangeTemplate。支援觸控事件和滑桿 例如控制可調暗的燈光
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction 除了封裝上述操作外,此範本還可讓使用者設定模式,例如暖氣、冷氣、暖/冷氣、節能或關閉。
StatelessTemplate CommandAction 用於表示提供觸控功能的控制項,但其狀態 例如 IR 電視遙控器您可以使用 定義處理常式或巨集; 以及狀態變更

有了這些資訊,您現在可以建立控制項:

舉例來說,如要控制智慧型燈泡和溫度控制器,請新增以下內容 MyCustomControlService 的常數:

Kotlin

    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() {
      ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
    private final int LIGHT_ID = 1337;
    private final String LIGHT_TITLE = "My fancy light";
    private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
    private final int THERMOSTAT_ID = 1338;
    private final String THERMOSTAT_TITLE = "My fancy thermostat";
    private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
    ...
    }
    

為控制項建立發布商

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

  • 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()

Kotlin

    class MyCustomControlService : ControlsProviderService() {
 
      override fun createPublisherForAllAvailable(): Flow.Publisher =
          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): Flow.Publisher {
           TODO()
        }
 
        override fun performControlAction(
            controlId: String,
            action: ControlAction,
            consumer: Consumer
        ) {
            TODO()
        }
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
        private final int LIGHT_ID = 1337;
        private final String LIGHT_TITLE = "My fancy light";
        private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
        private final int THERMOSTAT_ID = 1338;
        private final String THERMOSTAT_TITLE = "My fancy thermostat";
        private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
        private boolean toggleState = false;
        private float rangeState = 18f;
        private final Map<String, ReplayProcessor> controlFlows = new HashMap<>();
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherForAllAvailable() {
            List controls = new ArrayList<>();
            controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE));
            controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE));
            return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
        }
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
            ReplayProcessor updatePublisher = ReplayProcessor.create();
 
            controlIds.forEach(control -> {
                controlFlows.put(control, updatePublisher);
                updatePublisher.onNext(createLight());
                updatePublisher.onNext(createThermostat());
            });
 
            return FlowAdapters.toFlowPublisher(updatePublisher);
        }
    }
    

將系統選單向下滑動,並找出「Device controls」按鈕,如下圖所示 圖 4:

顯示裝置控制項系統 UI 的圖片
圖 4. 系統選單中的裝置控制選項。

輕觸「裝置控制項」,即可前往第二個畫面選取 選取應用程式後,您會看到先前的程式碼片段 自訂系統選單顯示新的控制項,如圖 5 所示:

這張圖片顯示系統選單,內含燈具和溫度控制器控制選項
圖 5. 要新增的燈具和溫度控制器控制項。

現在,請實作 createPublisherFor() 方法,並將以下內容新增至 Service

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf<String, MutableSharedFlow>()
 
    private var toggleState = false
    private var rangeState = 18f
 
    override fun createPublisherFor(controlIds: List): Flow.Publisher {
        val flow = MutableSharedFlow(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  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()
    }
 
    

Java

    @NonNull
    @Override
    public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
        ReplayProcessor updatePublisher = ReplayProcessor.create();
 
        controlIds.forEach(control -> {
            controlFlows.put(control, updatePublisher);
            updatePublisher.onNext(createLight());
            updatePublisher.onNext(createThermostat());
        });
 
        return FlowAdapters.toFlowPublisher(updatePublisher);
    }
 
    private Control createStatelessControl(int id, String title, int type) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, title)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatelessBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .build();
    }
 
    private Control createLight() {
        return createStatefulControl(
                LIGHT_ID,
                LIGHT_TITLE,
                LIGHT_TYPE,
                toggleState,
                new ToggleTemplate(
                        LIGHT_ID + "",
                        new ControlButton(
                                toggleState,
                                String.valueOf(toggleState).toUpperCase(Locale.getDefault())
                        )
                )
        );
    }
 
    private Control createThermostat() {
        return createStatefulControl(
                THERMOSTAT_ID,
                THERMOSTAT_TITLE,
                THERMOSTAT_TYPE,
                rangeState,
                new RangeTemplate(
                        THERMOSTAT_ID + "",
                        15f,
                        25f,
                        rangeState,
                        0.1f,
                        "%1.1f"
                )
        );
    }
 
    private  Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, "$title $state")
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatefulBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .setStatus(Control.STATUS_OK)
                .setControlTemplate(template)
                .build();
    }
    

在這個範例中,createPublisherFor() 方法包含假 應用程式必須執行的動作:與裝置通訊 擷取狀態,然後發出該狀態給系統

createPublisherFor() 方法會使用 Kotlin 協同程式和資料流來滿足 必要的 Reactive Streams API 步驟如下:

  1. 建立 Flow
  2. 等候 1 秒鐘。
  3. 建立並發出智慧型燈具的狀態。
  4. 再等一下。
  5. 建立並發出溫度控制器的狀態。

處理動作

performControlAction() 方法會在使用者與 傳送的 ControlAction 類型會決定動作。對給定的控制項採取適當操作,然後更新狀態 。

如要完成範例,請在 Service 中新增以下內容:

Kotlin

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer
    ) {
        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)
    }
    

Java

    @Override
    public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer) {
        ReplayProcessor processor = controlFlows.get(controlId);
        if (processor == null) return;
 
        if (controlId.equals(LIGHT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState();
            processor.onNext(createLight());
        }
        if (controlId.equals(THERMOSTAT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue()
            processor.onNext(createThermostat());
        }
    }
    

執行應用程式,存取「Device controls」選單,並查看燈具和 溫度控制器控制選項

顯示燈具和溫度控制器控制的圖片
圖 6. 燈具和溫度控制器。