Android 11 より、クイック アクセス デバイス コントロール機能を利用して、ユーザーは、照明、サーモスタット、カメラなどの外部デバイスを Android の電源ボタン メニューからすばやく表示し、操作することができます。デバイス アグリゲータ(Google Home など)やサードパーティ ベンダーのアプリは、このスペースに表示されるデバイスを指定できます。このガイドでは、このスペースにデバイス コントロールを表示し、コントロール アプリにリンクする方法について説明します。
このサポートを追加するには、ControlsProviderService
を作成して宣言し、事前定義されたコントロール タイプに基づいてアプリがサポートするコントロールを作成し、さらにそれらのコントロール用にパブリッシャーを作成します。
ユーザー インターフェース
デバイスは、[デバイス コントロール] の下にテンプレート化されたウィジェットとして表示されます。5 種類のデバイス コントロール ウィジェットを使用できます。
![]() |
![]() |
![]() |
![]() |
![]() |

ウィジェットを長押しすると、アプリでさらに詳細な制御が可能になります。各ウィジェットのアイコンと色はカスタマイズできますが、デフォルトの設定がデバイスに適合する限り、デフォルトのアイコンと色を使用してください。
サービスを作成する
このセクションでは、ControlsProviderService
の作成方法を示します。
このサービスは、Android UI のデバイス コントロール エリアに表示されるデバイス コントロールがアプリに含まれていることを、Android のシステム UI に伝えます。
ControlsProviderService
API では、Reactive Streams GitHub プロジェクトで定義されて Java 9 Flow インターフェースに実装されている、リアクティブ ストリームに習熟していることが前提になっています。
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
|
上記のアクションの 1 つをカプセル化するのに加えて、このテンプレートではユーザーが暖房、冷房、暖房・冷房、エコ、オフなどのモードを設定できます。 |
StatelessTemplate
|
CommandAction
|
リモートの IR テレビのように、タップ機能があるが状態を判定できないコントロールを示すために使用されます。このテンプレートを使用して、コントロールと状態の変化を集約したルーティンやマクロを定義できます。 |
この情報に基づいてコントロールを作成できます。
- コントロールの状態が不明な場合は
Control.StatelessBuilder
ビルダークラスを使用します。 - コントロールの状態がわかっている場合は
Control.StatefulBuilder
ビルダークラスを使用します。
コントロール用のパブリッシャーを作成する
コントロールを作成したら、次にパブリッシャーが必要になります。パブリッシャーは、システム UI にコントロールの存在を通知します。ControlsProviderService
クラスには、アプリケーションコードでオーバーライドする必要がある 2 つのパブリッシャーメソッドがあります。
createPublisherForAllAvailable()
: アプリで使用可能なすべてのコントロール用にPublisher
を作成します。このパブリッシャーについてはControl.StatelessBuilder()
を使用してControls
をビルドします。createPublisherFor()
: 文字列識別子によって識別される特定のコントロールのリストについて、Publisher
を作成します。パブリッシャーが各コントロールに状態を割り当てる必要があるため、これらのControls
をビルドするにはControl.StatefulBuilder
を使用します。
パブリッシャーを作成する
アプリがコントロールを最初にシステム 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)); } }
ユーザーが一連のコントロールを選択したら、それらのコントロール専用のパブリッシャーを作成します。Control.StatefulBuilder
ビルダークラスによって各コントロールの現在の状態(オン、オフなど)を割り当てる、createPublisherFor()
メソッドを使用します。
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); } }