外部デバイスを操作する

Android 11 より、クイック アクセス デバイス コントロール機能を利用して、ユーザーは、照明、サーモスタット、カメラなどの外部デバイスを Android の電源ボタン メニューからすばやく表示し、操作することができます。デバイス アグリゲータ(Google Home など)やサードパーティ ベンダーのアプリは、このスペースに表示されるデバイスを指定できます。このガイドでは、このスペースにデバイス コントロールを表示し、コントロール アプリにリンクする方法について説明します。

Android UI のデバイス コントロール スペース

このサポートを追加するには、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 には、コントロールを作成するためのビルダー メソッドが用意されています。ビルダーにデータを入力するには、制御するデバイスと、ユーザーがデバイスを操作する方法を決定する必要があります。特に次のことを行う必要があります。

  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 上記のアクションの 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 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));
    }
}

ユーザーが一連のコントロールを選択したら、それらのコントロール専用のパブリッシャーを作成します。Control.StatefulBuilder ビルダークラスによって各コントロールの現在の状態(オン、オフなど)を割り当てる、createPublisherFor() メソッドを使用します。

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