控制外部裝置

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

Android UI 中的裝置控制區

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

使用者介面

裝置會以樣板化的小工具顯示在「Device controls」中。目前有五個不同的裝置控制小工具可以使用:

切換小工具
切換
使用滑桿小工具切換
使用滑桿切換
範圍小工具
範圍(無法開啟或關閉)
無狀態的切換小工具
無狀態切換
溫度面板小工具(關閉)
溫度面板(關閉)
溫度面板小工具(開啟)
溫度面板(開啟)

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

建立服務

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

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

  • 發布商:應用程式是發布商
  • 訂閱者:系統 UI 是訂閱者,可要求發布者提出一些控制選項
  • 訂閱:發布商可傳送更新至系統 UI 的時間區間;發布商或訂閱者可將這段期間結束

宣告服務

應用程式必須在應用程式資訊清單中宣告服務。請務必加入 BIND_CONTROLS 權限。

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

<!-- 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 類別是所有目前支援裝置的列舉。該類型用於決定 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 用來表示提供觸控功能但無法確定狀態的控制項,例如紅外線電視遙控器。您可以使用此範本定義一個例行工作或巨集,其為控制項和狀態變更的集合。

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

為控制項建立發布商

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

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

建立發布商

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

當 Android UI 顯示這些控制項後,使用者就能選取感興趣的控制項(亦即挑選喜愛的項目)。

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

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);
  }
}