Externe Geräte steuern

In Android 11 und höher können Nutzer über die Funktion „Schnellzugriff auf Gerätesteuerung“ externe Geräte wie Lampen, Thermostate und Kameras innerhalb von drei Interaktionen über einen Standard-Launcher schnell aufrufen und steuern. Der OEM des Geräts wählt den Launcher aus. Geräteaggregatoren wie Google Home und Apps von Drittanbietern können Geräte für die Anzeige in diesem Bereich bereitstellen. Auf dieser Seite erfahren Sie, wie Sie Gerätesteuerungen in diesem Bereich anzeigen und mit Ihrer Steuer-App verknüpfen.

Abbildung 1. Bereich für die Gerätesteuerung in der Android-Benutzeroberfläche

Wenn Sie diese Unterstützung hinzufügen möchten, erstellen und deklarieren Sie eine ControlsProviderService. Erstellen Sie die Steuerelemente, die von Ihrer App unterstützt werden, anhand vordefinierter Steuerelementtypen und erstellen Sie dann Publisher für diese Steuerelemente.

Benutzeroberfläche

Geräte werden unter Gerätesteuerung als Vorlagen-Widgets angezeigt. Es sind fünf Widgets für die Gerätesteuerung verfügbar, wie in der folgenden Abbildung dargestellt:

Widget ein-/ausschalten
Aktivieren/Deaktivieren
Mit Schieberegler-Widget umschalten
Mit Schieberegler umschalten
Bereichs-Widget
Bereich (kann nicht aktiviert oder deaktiviert werden)
Zustandsloses Ein/Aus-Widget
Zustandsloser Schalter
Steuerfeld-Widget für Temperatur (geschlossen)
Temperaturbereich (geschlossen)
Abbildung 2 Sammlung von Vorlagen-Widgets.

Wenn Sie ein Widget gedrückt halten, werden Sie zur App weitergeleitet, in der Sie weitere Einstellungen vornehmen können. Sie können das Symbol und die Farbe für jedes Widget anpassen. Für eine optimale Nutzerfreundlichkeit sollten Sie jedoch das Standardsymbol und die Standardfarbe verwenden, wenn die Standardeinstellungen zum Gerät passen.

Ein Bild, das das Temperatur-Steuerfeld-Widget (geöffnet) zeigt
Abbildung 3. Das Steuerfeld für die Temperatur ist geöffnet.

Dienst erstellen

In diesem Abschnitt wird gezeigt, wie Sie die ControlsProviderService erstellen. Dieser Dienst teilt der Android-System-UI mit, dass Ihre App Gerätesteuerungen enthält, die im Bereich Gerätesteuerungen der Android-UI angezeigt werden müssen.

Für die ControlsProviderService API wird vorausgesetzt, dass Sie mit reaktiven Streams vertraut sind, wie sie im GitHub-Projekt „Reactive Streams“ definiert und in den Java 9-Flow-Schnittstellen implementiert sind. Die API basiert auf den folgenden Konzepten:

  • Publisher:Ihre Anwendung ist der Publisher.
  • Abonnent:Die System-UI ist der Abonnent und kann vom Verlag oder Webpublisher eine Reihe von Steuerelementen anfordern.
  • Abo:Der Zeitraum, in dem der Publisher Updates an die System-UI senden kann. Dieses Fenster kann entweder vom Publisher oder vom Abonnenten geschlossen werden.

Dienst deklarieren

Ihre App muss in ihrem App-Manifest einen Dienst wie MyCustomControlService deklarieren.

Der Dienst muss einen Intent-Filter für ControlsProviderService enthalten. Mit diesem Filter können Anwendungen Steuerelemente zur System-UI hinzufügen.

Außerdem benötigen Sie ein label, das in den Steuerelementen der System-UI angezeigt wird.

Das folgende Beispiel zeigt, wie ein Dienst deklariert wird:

<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>

Erstellen Sie als Nächstes eine neue Kotlin-Datei mit dem Namen MyCustomControlService.kt und erweitern Sie sie um ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Den richtigen Steuerelementtyp auswählen

Die API bietet Builder-Methoden zum Erstellen der Steuerelemente. Legen Sie zum Ausfüllen des Builders fest, welches Gerät Sie steuern möchten und wie der Nutzer damit interagiert. Führen Sie die folgenden Schritte aus:

  1. Wählen Sie den Gerätetyp aus, den das Steuerelement darstellt. Die Klasse DeviceTypes ist eine Aufzählung aller unterstützten Geräte. Anhand des Typs werden die Symbole und Farben für das Gerät in der Benutzeroberfläche bestimmt.
  2. Legen Sie den für Nutzer sichtbaren Namen, den Gerätestandort (z. B. Küche) und andere Textelemente der Benutzeroberfläche fest, die mit dem Steuerelement verknüpft sind.
  3. Wählen Sie die beste Vorlage für die Nutzerinteraktion aus. Steuerelementen wird von der Anwendung eine ControlTemplate zugewiesen. In dieser Vorlage wird dem Nutzer direkt der Steuerelementstatus sowie die verfügbaren Eingabemethoden angezeigt, also die ControlAction. In der folgenden Tabelle sind einige der verfügbaren Vorlagen und die von ihnen unterstützten Aktionen aufgeführt:
Vorlage Aktion Beschreibung
ControlTemplate.getNoTemplateObject() None Die Anwendung kann damit Informationen zum Steuerelement übermitteln, der Nutzer kann damit jedoch nicht interagieren.
ToggleTemplate BooleanAction Stellt ein Steuerelement dar, das zwischen aktiviert und deaktiviert umgeschaltet werden kann. Das BooleanAction-Objekt enthält ein Feld, das sich ändert, um den angeforderten neuen Status darzustellen, wenn der Nutzer auf das Steuerelement tippt.
RangeTemplate FloatAction Stellt ein Schieberegler-Widget mit angegebenen Mindest-, Höchst- und Schrittwerten dar. Wenn der Nutzer mit dem Schieberegler interagiert, senden Sie ein neues FloatAction-Objekt mit dem aktualisierten Wert an die Anwendung zurück.
ToggleRangeTemplate BooleanAction, FloatAction Diese Vorlage ist eine Kombination aus ToggleTemplate und RangeTemplate. Es unterstützt Touch-Ereignisse sowie einen Schieberegler, z. B. zum Dimmen von Lampen.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Neben den oben genannten Aktionen können Nutzer mit dieser Vorlage auch einen Modus festlegen, z. B. „Heizen“, „Kühlen“, „Heizen/Kühlen“, „Eco“ oder „Aus“.
StatelessTemplate CommandAction Wird verwendet, um ein Steuerelement anzugeben, das eine Touchbedienung bietet, dessen Status jedoch nicht ermittelt werden kann, z. B. eine Infrarotfernbedienung für Fernseher. Mit dieser Vorlage können Sie einen Ablauf oder ein Makro definieren, also eine Kombination aus Steuer- und Statusänderungen.

Anhand dieser Informationen können Sie das Steuerelement erstellen:

Wenn Sie beispielsweise eine intelligente Glühbirne und einen Thermostat steuern möchten, fügen Sie MyCustomControlService die folgenden Konstanten hinzu:

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

Publisher für die Steuerelemente erstellen

Nachdem Sie das Steuerelement erstellt haben, ist ein Publisher erforderlich. Der Verlag oder Webpublisher informiert die System-UI über die Existenz des Steuerelements. Die ControlsProviderService-Klasse hat zwei Publisher-Methoden, die Sie in Ihrem Anwendungscode überschreiben müssen:

  • createPublisherForAllAvailable(): Erstellt eine Publisher für alle Steuerelemente, die in Ihrer App verfügbar sind. Mit Control.StatelessBuilder() können Sie Control-Objekte für diesen Publisher erstellen.
  • createPublisherFor(): Erstellt eine Publisher für eine Liste von Steuerelementen, die anhand ihrer String-IDs identifiziert werden. Verwende Control.StatefulBuilder, um diese Control-Objekte zu erstellen, da der Publisher jedem Steuerelement einen Status zuweisen muss.

Publisher erstellen

Wenn Ihre App zum ersten Mal Steuerelemente in der System-UI veröffentlicht, kennt die App den Status der einzelnen Steuerelemente nicht. Das Abrufen des Status kann zeitaufwendig sein und viele Hops im Netzwerk des Geräteanbieters erfordern. Verwenden Sie die Methode createPublisherForAllAvailable(), um dem System die verfügbaren Steuerelemente anzuzeigen. Bei dieser Methode wird die Builder-Klasse Control.StatelessBuilder verwendet, da der Status der einzelnen Steuerelemente unbekannt ist.

Sobald die Steuerelemente in der Android-Benutzeroberfläche angezeigt werden , kann der Nutzer seine bevorzugten Steuerelemente auswählen.

Wenn Sie eine ControlsProviderService mit Kotlin-Coroutinen erstellen möchten, fügen Sie Ihrer build.gradle eine neue Abhängigkeit hinzu:

Groovy

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
}

Nachdem Sie Ihre Gradle-Dateien synchronisiert haben, fügen Sie Service das folgende Snippet hinzu, um createPublisherForAllAvailable() zu implementieren:

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

Wischen Sie im Systemmenü nach unten und suchen Sie die Schaltfläche Gerätesteuerung (siehe Abbildung 4):

Ein Bild, das die System-UI für die Gerätesteuerung zeigt
Abbildung 4: Gerätesteuerung im Systemmenü

Wenn Sie auf Gerätesteuerung tippen, gelangen Sie zu einem zweiten Bildschirm, auf dem Sie Ihre App auswählen können. Nachdem Sie Ihre App ausgewählt haben, sehen Sie, wie mit dem vorherigen Snippet ein benutzerdefiniertes Systemmenü mit Ihren neuen Steuerelementen erstellt wird, wie in Abbildung 5 dargestellt:

Ein Bild, das das Systemmenü mit einer Licht- und Thermostatsteuerung zeigt
Abbildung 5: Zu hinzufügende Licht- und Thermostatsteuerungen

Implementieren Sie jetzt die Methode createPublisherFor() und fügen Sie Ihrem Service Folgendes hinzu:

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

In diesem Beispiel enthält die createPublisherFor()-Methode eine gefälschte Implementierung dessen, was Ihre App tun muss: mit Ihrem Gerät kommunizieren, um den Status abzurufen und diesen Status an das System auszugeben.

Die createPublisherFor()-Methode verwendet Kotlin-Coroutinen und ‑Abläufe, um die Anforderungen der erforderlichen Reactive Streams API zu erfüllen. Gehen Sie dazu so vor:

  1. Erstellt einen Flow.
  2. Wartet eine Sekunde.
  3. Erstellt und sendet den Status der intelligenten Glühbirne.
  4. Wartet eine weitere Sekunde.
  5. Erstellt und sendet den Status des Thermostats.

Aktionen verarbeiten

Die performControlAction()-Methode signalisiert, wenn der Nutzer mit einem veröffentlichten Steuerelement interagiert. Die Aktion wird durch den Typ der gesendeten ControlAction bestimmt. Führen Sie die entsprechende Aktion für das jeweilige Steuerelement aus und aktualisieren Sie dann den Status des Geräts in der Android-Benutzeroberfläche.

Fügen Sie zum Abschluss des Beispiels Folgendes zu Service hinzu:

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

Öffnen Sie die App, rufen Sie das Menü Gerätesteuerung auf und sehen Sie sich die Steuerelemente für Lampen und Thermostate an.

Ein Bild, das eine Lampe und eine Thermostatsteuerung zeigt
Abbildung 6: Licht- und Thermostatsteuerung