Externe Geräte steuern

Mit der Funktion „Schnellzugriff auf Gerätesteuerung“ in Android 11 und höher können Nutzer externe Geräte wie Lampen, Thermostate und Kameras schnell ansehen und steuern. Dazu sind nur drei Interaktionen über einen Standard-Launcher erforderlich. Der Geräte-OEM wählt aus, welcher Launcher verwendet wird. Geräteaggregatoren wie Google Home und Drittanbieter-Apps können Geräte zur Anzeige in diesem Bereich bereitstellen. Auf dieser Seite erfahren Sie, wie Sie Gerätesteuerungen in diesem Bereich anzeigen und mit Ihrer Steuerungs-App verknüpfen.

Abbildung 1. Bereich zur Gerätesteuerung in der Android-Benutzeroberfläche.

Um diese Unterstützung hinzuzufügen, erstellen und deklarieren Sie eine ControlsProviderService. Erstellen Sie die von Ihrer App unterstützten Steuerelemente basierend auf vordefinierten 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 Gerätesteuerungs-Widgets verfügbar, wie in der folgenden Abbildung dargestellt:

Widget für die Gerätesteuerung ein- und ausblenden
Ein/Aus
Umschalten mit dem Schieberegler-Widget
Mit Schieberegler umschalten
Bereichsschieberegler-Widget für die Gerätesteuerung
Bereich (kann nicht aktiviert oder deaktiviert werden)
Statusloses Ein/Aus-Schalt-Widget
Stateless toggle
Temperatur-Widget im geschlossenen Zustand
Temperaturbereich (geschlossen)
Abbildung 2. Sammlung von Vorlagen-Widgets.

Wenn Sie ein Widget gedrückt halten, werden Sie zur App weitergeleitet, um weitere Einstellungen vorzunehmen. 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 das Standardset zum Gerät passt.

Widget für das Temperatursteuerfeld im geöffneten Zustand
Abbildung 3. Öffne das Widget für den Temperaturbereich.

Dienst erstellen

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

Für die ControlsProviderService API sind Kenntnisse über reaktive Streams erforderlich, 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 eine Reihe von Steuerelementen vom Verlag oder Webpublisher anfordern.
  • Abo:Der Zeitraum, in dem der Verlag oder Webpublisher Updates an die System-UI senden kann. Sowohl der Publisher als auch der Abonnent können 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 beitragen.

Außerdem benötigen Sie ein label, das in den Steuerelementen in 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 lassen Sie sie von ControlsProviderService erben:

class MyCustomControlService : ControlsProviderService() {
    // ...
}

Den richtigen Steuerungstyp auswählen

Die API bietet Builder-Methoden zum Erstellen der Steuerelemente. Um den Builder zu füllen, müssen Sie das Gerät bestimmen, das Sie steuern möchten, und festlegen, wie der Nutzer damit interagiert. Führen Sie diese 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. Der Typ wird verwendet, um die Symbole und Farben für das Gerät in der Benutzeroberfläche zu bestimmen.
  2. Bestimmen Sie den für Nutzer sichtbaren Namen, den Gerätestandort (z. B. 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 aus der Anwendung eine ControlTemplate zugewiesen. In dieser Vorlage wird dem Nutzer direkt der Steuerungsstatus sowie die verfügbaren Eingabemethoden angezeigt, d. h. 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 vermitteln, der Nutzer kann jedoch nicht damit interagieren.
ToggleTemplate BooleanAction Stellt ein Steuerelement dar, das zwischen den Status „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 zurück an die Anwendung.
ToggleRangeTemplate BooleanAction, FloatAction Diese Vorlage ist eine Kombination aus ToggleTemplate und RangeTemplate. Es unterstützt Touch-Ereignisse sowie einen Schieberegler, z. B. zum Steuern dimmbarer Lampen.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Mit dieser Vorlage werden nicht nur die vorherigen Aktionen gekapselt, sondern der Nutzer kann auch einen Modus festlegen, z. B. „Heizen“, „Kühlen“, „Heizen/Kühlen“, „Eco“ oder „Aus“.
StatelessTemplate CommandAction Wird verwendet, um ein Steuerelement anzugeben, das Touch-Funktionen bietet, dessen Status jedoch nicht bestimmt werden kann, z. B. eine IR-TV-Fernbedienung. Mit dieser Vorlage können Sie eine Routine oder ein Makro definieren, das eine Aggregation von Steuerungs- und Statusänderungen ist.

Anhand dieser Informationen können Sie die Kontrollgruppe erstellen:

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

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() {
    // ...
}

Publisher für die Steuerelemente erstellen

Nachdem Sie die Kontrollgruppe erstellt haben, benötigen Sie einen Publisher. Der Verlag oder Webpublisher informiert die System-UI über das Vorhandensein des Steuerelements. Die ControlsProviderService-Klasse hat zwei Publisher-Methoden, die Sie in Ihrem Anwendungscode überschreiben müssen:

  • createPublisherForAllAvailable: Erstellt ein Publisher für alle in Ihrer App verfügbaren Steuerelemente. 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-Kennungen identifiziert werden. Verwenden Sie 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 sie den Status der einzelnen Steuerelemente nicht. Das Abrufen des Status kann ein zeitaufwendiger Vorgang sein, der viele Hops im Netzwerk des Geräteanbieters umfasst. Verwenden Sie die Methode createPublisherForAllAvailable, um dem System die verfügbaren Steuerelemente mitzuteilen. 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, können Nutzer Favoriten auswählen.

Wenn Sie Kotlin-Coroutinen verwenden möchten, um ein ControlsProviderService zu erstellen, fügen Sie Ihrem 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 der Datei Service das folgende Snippet hinzu, um createPublisherForAllAvailable zu implementieren:

class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
        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<String>): Flow.Publisher<Control> {
        TODO()
    }

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer<Int>,
    ) {
        TODO()
    }
}

Wische im Systemmenü nach unten und suche nach der Schaltfläche Gerätesteuerung (siehe Abbildung 4):

Systembenutzeroberfläche für Gerätesteuerung
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 durch den vorherigen Snippet ein benutzerdefiniertes Systemmenü mit Ihren neuen Steuerelementen erstellt wird, wie in Abbildung 5 dargestellt:

Systemmenü mit Steuerelementen für Beleuchtung und Thermostat
Abbildung 5: Steuerelemente für Beleuchtung und Thermostat, die hinzugefügt werden sollen.

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

private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val controlFlows = mutableMapOf<String, MutableSharedFlow<Control>>()

private var toggleState = false
private var rangeState = 18f

override fun createPublisherFor(controlIds: List<String>): Flow.Publisher<Control> {
    val flow = MutableSharedFlow<Control>(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 <T> 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()
}

In diesem Beispiel enthält die Methode createPublisherFor eine gefälschte Implementierung der Aufgaben, die Ihre App ausführen muss: mit Ihrem Gerät kommunizieren, um seinen Status abzurufen, und diesen Status an das System senden.

Die createPublisherFor-Methode verwendet Kotlin-Coroutinen und -Flows, um die erforderliche Reactive Streams API zu erfüllen. Dazu wird Folgendes ausgeführt:

  1. Erstellt ein Flow.
  2. Wartet eine Sekunde.
  3. Erstellt und gibt den Status der Smart-Home-Lampe aus.
  4. Wartet eine weitere Sekunde.
  5. Erstellt und gibt den Status des Thermostats aus.

Aktionen verarbeiten

Die Methode performControlAction signalisiert, wenn der Nutzer mit einem veröffentlichten Steuerelement interagiert. Die Art der gesendeten ControlAction bestimmt die Aktion. Führe die entsprechende Aktion für das angegebene Steuerelement aus und aktualisiere dann den Status des Geräts in der Android-Benutzeroberfläche.

Fügen Sie Folgendes zu Ihrem Service hinzu, um das Beispiel zu vervollständigen:

override fun performControlAction(
    controlId: String,
    action: ControlAction,
    consumer: Consumer<Int>,
) {
    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)
}

Starte die App, rufe das Menü Gerätesteuerung auf und sieh dir die Steuerelemente für deine Lampe und dein Thermostat an.

Steuerelemente mit einer Lampe und einem Thermostat
Abbildung 6: Steuerung von Beleuchtung und Thermostat