在 Android 11 以上版本中,使用者可透過「快速存取裝置控制」功能,在預設啟動器的三次互動中,以及功能可見的情況下,快速檢視及控制外部裝置,例如指示燈、溫度控制器及相機。(裝置的 OEM 會選擇要使用的啟動器)。裝置集結網站(例如 Google Home)和第三方供應商應用程式可以在此提供要顯示的裝置。本指南說明如何在這個區域中顯示裝置控制,以及將其連結至控制應用程式。
如要新增這項支援,請建立並宣告 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 提供建構工具方法以建立控制選項。如要填入建構工具,必須決定要控制的裝置,以及使用者與其互動的方式。其中特別需要採取以下操作:
- 選擇控制項所代表的裝置類型。
DeviceTypes
類別是所有目前支援裝置的列舉。該類型用於決定 UI 中裝置的圖示和顏色。 - 決定向使用者顯示的名稱、裝置位置(例如廚房)以及其他與控制項相關的 UI 文字元素。
- 選擇最適合用來支援使用者互動的範本。應用程式會指派一個
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
建構工具類別。
為控制項建立發布商
建立控制項之後,需要有發布商。發布商會告知系統 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 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 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, 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); } }