Externe Geräte steuern

Mit Android 11 und höher können Nutzer mit der Schnellzugriff-Gerätesteuerung schnell externe Geräte wie Lampen, Thermostate und Kameras über eine Nutzeraktion aufrufen und steuern. Dazu müssen sie nur drei Interaktionen über einen Standard-Launcher ausführen. Der OEM entscheidet selbst, welchen Launcher er verwendet. Geräteaggregatoren wie Google Home und Apps von Drittanbietern können Geräte zur Anzeige in diesem Bereich bereitstellen. Auf dieser Seite wird beschrieben, wie Sie Gerätesteuerelemente in diesem Bereich anzeigen und mit Ihrer Steuer-App verknüpfen können.

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

Erstellen und deklarieren Sie einen ControlsProviderService, um diese Unterstützung hinzuzufügen. Erstellen Sie die Steuerelemente, die Ihre App auf Basis vordefinierter Einstellungstypen unterstützt, 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 zur Gerätesteuerung verfügbar, wie in der folgenden Abbildung dargestellt:

Widget ein-/ausblenden
Ein/Aus
Mit Schieberegler-Widget umschalten
Ein/Aus mit Schieberegler
Bereichs-Widget
Bereich (kann nicht aktiviert oder deaktiviert werden)
Zustandsloses Ein/Aus-Widget
Zustandslose Ein/Aus-Schaltfläche
Widget für Temperatursteuerfeld (geschlossen)
Temperaturbereich (geschlossen)
Abbildung 2. Sammlung von Widget-Vorlagen.

Wenn du ein Widget berührst und gedrückt hältst, gelangst du zur App mit umfassenderen Einstellungen. Du kannst das Symbol und die Farbe für jedes Widget anpassen. Für eine optimale Nutzererfahrung solltest du jedoch das Standardsymbol und die Standardfarbe verwenden, wenn die Standardeinstellungen dem Gerät entsprechen.

Ein Bild, auf dem das Widget für den Temperaturbereich zu sehen ist (geöffnet)
Abbildung 3: Widget für Temperatursteuerfeld öffnen.

Dienst erstellen

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

Bei der ControlsProviderService API wird davon ausgegangen, dass Sie mit reaktiven Streams vertraut sind, wie im GitHub-Projekt zu reaktiven Streams definiert und in den Java 9-Flow-Oberflächen implementiert. Die API basiert auf den folgenden Konzepten:

  • Publisher:Ihre App ist der Publisher.
  • Abonnent: Die System-UI ist der Abonnent und kann eine Reihe von Steuerelementen vom Publisher anfordern.
  • Abo:Der Zeitraum, in dem der Verlag oder Webpublisher Updates an die System-UI senden kann. Der Verlag oder Webpublisher oder der Abonnent kann dieses Fenster schließen.

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

Richtigen Einstellungstyp auswählen

Die API bietet Builder-Methoden zum Erstellen der Steuerelemente. Um den Builder zu füllen, geben Sie das Gerät an, das Sie steuern möchten, und wie der Nutzer damit interagieren kann. Gehen Sie so vor:

  1. Wählen Sie den Gerätetyp aus, den das Steuerelement darstellt. Die Klasse DeviceTypes ist eine Aufzählung aller unterstützten Geräte. Der Typ wird verwendet, um die Symbole und Farben für das Gerät in der UI zu bestimmen.
  2. Bestimmen Sie den für den Nutzer sichtbaren Namen, den Gerätestandort (z. B. die Küche) und andere UI-Textelemente, die mit dem Steuerelement verknüpft sind.
  3. Wählen Sie die beste Vorlage für die Nutzerinteraktion aus. Steuerelementen wird ein ControlTemplate aus der Anwendung zugewiesen. Diese Vorlage zeigt dem Nutzer direkt den Steuerungsstatus sowie die verfügbaren Eingabemethoden an, also 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 jedoch nicht damit interagieren.
ToggleTemplate BooleanAction Stellt ein Steuerelement dar, das zwischen aktiviertem und deaktiviertem Status gewechselt 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 zurück an die Anwendung.
ToggleRangeTemplate BooleanAction, FloatAction Diese Vorlage ist eine Kombination aus ToggleTemplate und RangeTemplate. Unterstützt werden Touch-Ereignisse sowie ein Schieberegler, mit dem du dimmbare Lichter steuern kannst.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Mit dieser Vorlage können Nutzer nicht nur die vorhergehenden Aktionen, sondern auch einen Modus wie „Heizen“, „Kühlen“, „Heizen/Kühlen“, „Eco“ oder „Aus“ festlegen.
StatelessTemplate CommandAction Wird für ein Steuerelement verwendet, das Touchbedienung bietet, aber dessen Status nicht ermittelt werden kann, z. B. eine IR-Fernsehfernbedienung. Sie können diese Vorlage verwenden, um eine Routine oder ein Makro zu definieren, bei dem es sich um eine Aggregation von Steuer- und Statusänderungen handelt.

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 Ihrem 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 die Einstellung erstellt haben, benötigen Sie einen Publisher. Der Verlag oder Webpublisher teilt der System-UI mit, dass das Steuerelement vorhanden ist. Die Klasse ControlsProviderService hat zwei Publisher-Methoden, die Sie in Ihrem Anwendungscode überschreiben müssen:

  • createPublisherForAllAvailable(): Erstellt ein Publisher für alle Steuerelemente, die in Ihrer App verfügbar sind. Verwenden Sie Control.StatelessBuilder(), um Control-Objekte für diesen Publisher zu erstellen.
  • createPublisherFor(): Erstellt ein Publisher für eine Liste der angegebenen Steuerelemente, die durch ihre String-IDs identifiziert werden. Verwenden Sie Control.StatefulBuilder, um diese Control-Objekte zu erstellen, da der Verlag oder Webpublisher jedem Steuerelement einen Status zuweisen muss.

Publisher erstellen

Wenn Ihre App Steuerelemente zum ersten Mal auf der System-UI veröffentlicht, kennt sie den Status der einzelnen Steuerelemente nicht. Das Abrufen des Status kann ein zeitaufwendiger Vorgang sein, der viele Hops im Netzwerk des Geräteherstellers umfasst. Verwenden Sie die Methode createPublisherForAllAvailable(), um dem System die verfügbaren Steuerelemente anzubieten. Diese Methode verwendet die Builder-Klasse Control.StatelessBuilder, da der Status der einzelnen Steuerelemente unbekannt ist.

Sobald die Steuerelemente in der Android-Benutzeroberfläche angezeigt werden , können Nutzer Favoriten auswählen.

Wenn Sie Kotlin-Koroutinen zum Erstellen eines ControlsProviderService verwenden möchten, fügen Sie eine neue Abhängigkeit zu build.gradle hinzu:

Groovig

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

Kotlin

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

Nachdem du deine Gradle-Dateien synchronisiert hast, füge deinem 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> 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 das Systemmenü nach unten und suchen Sie die Schaltfläche Gerätesteuerung (siehe Abbildung 4):

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

Wenn Sie auf Gerätesteuerung tippen, wird ein zweiter Bildschirm angezeigt, 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, auf dem das Systemmenü mit einer Beleuchtung und einem Thermostat zu sehen ist
Abbildung 5: Licht- und Thermostatsteuerung, die hinzugefügt werden sollen.

Implementieren Sie nun die Methode createPublisherFor() und fügen Sie der Service Folgendes hinzu:

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf>()
 
    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 Methode createPublisherFor() eine fiktive Implementierung dessen, was Ihre App tun muss: Sie kommunizieren mit Ihrem Gerät, um seinen Status abzurufen, und geben diesen Status an das System aus.

Die Methode createPublisherFor() verwendet Kotlin-Koroutinen und -Abläufe, um die erforderliche Reactive Streams API zu erfüllen. Dazu wird Folgendes ausgeführt:

  1. Erstellt ein Flow.
  2. Wartet eine Sekunde.
  3. Erzeugt den Status der intelligenten Lampe und sendet sie aus.
  4. Wartet eine weitere Sekunde.
  5. Erzeugt den Status des Thermostats und gibt ihn aus.

Aktionen verarbeiten

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

Fügen Sie Service Folgendes hinzu, um das Beispiel abzuschließen:

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

Führen Sie die App aus, rufen Sie das Menü Gerätesteuerung auf und sehen Sie sich die Steuerelemente für Licht und Thermostat an.

Ein Bild, auf dem eine Lampe und eine Thermostatsteuerung zu sehen sind
Abbildung 6. Licht- und Thermostatsteuerung.