外部デバイスを操作する

Android 11 以降のクイック アクセス デバイス コントロール機能 ユーザーは、照明、カメラ、音声、動画などの外部デバイスをすばやく表示して操作できます。 3 つのインタラクションの範囲内で、ユーザー アフォーダンスから あります。デバイスの OEM は、使用するランチャーを選択します。デバイス アグリゲータ(Google Home など)とサードパーティ ベンダーのアプリは、 このスペースに表示するデバイスを提供します。このページでは、Google Meet を使用して コントロール アプリにリンクすることもできます。

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">
</ph> 図 1.Android UI のデバイス コントロール スペース。

このサポートを追加するには、ControlsProviderService を作成して宣言します。作成 あらかじめ定義されたコントロール タイプに基づいて、アプリがサポートしているコントロールを作成し、 できます。

ユーザー インターフェース

デバイスは、[デバイス コントロール] の下にテンプレート化されたウィジェットとして表示されます。5 本 さまざまなデバイス コントロール ウィジェットを利用できます。

ウィジェットの切り替え
切り替え
スライダー ウィジェットで切り替える
スライダーで切り替え
<ph type="x-smartling-placeholder">
</ph> 範囲ウィジェット
<ph type="x-smartling-placeholder">
</ph> 範囲(オンとオフの切り替えは不可)
ステートレス切り替えウィジェット
ステートレス切り替え
温度パネル ウィジェット(閉じた状態)
温度パネル(閉じた状態)
図 2. テンプレート化されたウィジェットのコレクションです。

接触とウィジェットを長押しするとアプリに移動し、より細かい操作が可能です。Google Chat では 各ウィジェットでアイコンや色をカスタマイズできますが デフォルト セットがデバイスと一致する場合は、デフォルトのアイコンと色を使用します。

温度パネルのウィジェットを示す画像(開いている)
図 3. 温度パネル ウィジェットを開きます。

サービスを作成する

このセクションでは、Terraform を使用して ControlsProviderService。 このサービスは、アプリにデバイス コントロールが含まれていることを Android システム UI に通知します。 Android UI の [デバイス コントロール] 領域に表示する必要があります。

ControlsProviderService API は、次のようなリアクティブ ストリームに精通していることを前提としています。 Reactive Streams GitHub で定義される プロジェクト Java 9 Flow に実装 インターフェースをご覧ください。 この API は、次のコンセプトに基づいて構築されています。

  • パブリッシャー: アプリケーションがパブリッシャーです。
  • サブスクライバー: システム UI はサブスクライバーであり、番号をリクエストできます。 さまざまな設定を行えます
  • 購読: ニュース メディアが更新情報を送信できる期間 システム UI に表示されます。パブリッシャーまたはサブスクライバーは、この クリックします。

サービスを宣言する

アプリは、MyCustomControlService などのサービスを宣言する必要があります。 追加します。

サービスには ControlsProviderService のインテント フィルタが含まれている必要があります。この フィルタを使用すると、アプリがシステム UI にコントロールを提供できるようになります。

また、システム UI のコントロールに表示される label も必要です。

次の例は、サービスを宣言する方法を示しています。

<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 クラスは すべてのサポート対象デバイスの列挙です。種類は、変数の種類と 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 タップ機能を提供しているが、その状態を持つコントロールを示すために使用します。 テレビのリモコンなどで識別できないこともあります。このテンプレートを使用して、コントロールと状態の変化を集約したルーティンやマクロを定義できます。

この情報を使用して、コントロールを作成できます。

  • コントロールの状態が不明な場合は Control.StatelessBuilder ビルダークラスを使用します。
  • コントロールの状態がわかっている場合は Control.StatefulBuilder ビルダークラスを使用します。

たとえば、スマート電球とサーモスタットを操作するには、次のように追加します 定数を 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;
 
    ...
    }
    

コントロール用のパブリッシャーを作成する

コントロールを作成したら、次はパブリッシャーが必要です。パブリッシャーは、システム UI にコントロールの存在を通知します。ControlsProviderService クラスには、アプリケーション コードでオーバーライドする必要がある 2 つのパブリッシャー メソッドがあります。

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

システム メニューを下にスワイプして、[デバイス コントロール] ボタン( 図 4:

デバイス コントロールのシステム UI を示す画像
図 4. システム メニューのデバイス コントロール。

[デバイス コントロール] をタップすると、2 つ目の画面に移動し、 説明します。アプリを選択すると、前のスニペットによって 新しいコントロールを表示するカスタム システム メニュー(図 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 のコルーチンと Flow を使用して、 必要な Reactive Streams API を確認します。手順は次のとおりです。

  1. Flow を作成します。
  2. 1 秒待ちます。
  3. スマートライトの状態を作成して出力します。
  4. さらに 1 秒待ちます。
  5. サーモスタットの状態を作成して出力します。

アクションを処理する

performControlAction() メソッドは、ユーザーが あります。送信される ControlAction のタイプによってアクションが決まります。 指定されたコントロールに対して適切なアクションを実行し、状態を更新する デバイスの Android UI で表示できます。

この例を完了するには、次のコードを 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());
        }
    }
    

アプリを実行し、[デバイス コントロール] メニューにアクセスして サーモスタットの制御。

照明とサーモスタットの制御を示す画像
図 6. 照明とサーモスタットの制御。