Externe Geräte steuern

Unter Android 11 und höher können Nutzer mit der Funktion „Gerätesteuerung über Schnellzugriff“ externe Geräte wie Lampen, Thermostate und Kameras schnell über eine Nutzerinteraktion innerhalb von drei Interaktionen von einem Standard-Launcher aus aufrufen und steuern. Der OEM des Geräts wählt aus, welcher Launcher verwendet werden soll. Geräteaggregatoren wie Google Home und Drittanbieter-Apps können Geräte für die Anzeige in diesem Bereich bereitstellen. Auf dieser Seite wird beschrieben, wie du Gerätesteuerungen in diesem Bereich anzeigen und mit deiner Steuerungs-App verknüpfen kannst.

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

Wenn du diese Unterstützung hinzufügen möchtest, erstelle und deklariere ein ControlsProviderService. Erstellen Sie die Steuerelemente, die Ihre App unterstützt, 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 Widgets für die Gerätesteuerung verfügbar, wie in der folgenden Abbildung dargestellt:

Widget für die Gerätesteuerung ein-/ausblenden
Umschalten
Mit Schieberegler-Widget umschalten
Mit Schieberegler umschalten
Schieberegler-Widget für Geräteeinstellungen
Bereich (kann nicht aktiviert oder deaktiviert werden)
Zustandsloses Widget für die Ein/Aus-Schaltfläche
Zustandsloser Schalter
Widget für das Temperaturfeld im geschlossenen Zustand
Temperaturbereich (geschlossen)
Abbildung 2. Sammlung von Vorlagen-Widgets.

Wenn du ein Widget gedrückt hältst, gelangst du zur App, 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 Temperaturfeld im geöffneten Zustand
Abbildung 3. Öffne das Widget für den Temperaturbereich.

Dienst erstellen

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

Die ControlsProviderService API setzt Vertrautheit mit reaktiven Streams voraus, wie sie im Reactive Streams GitHub-Projekt 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 Publisher anfordern.
  • Abo:Der Zeitraum, in dem der Publisher Aktualisierungen an die System-UI senden kann. Sowohl der Publisher als auch der Abonnent können dieses Fenster schließen.

Dienst deklarieren

Ihre App muss im App-Manifest einen Dienst deklarieren, z. B. MyCustomControlService.

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

Außerdem benötigst du 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:

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

Richtigen Steuerungstyp auswählen

Die API bietet Builder-Methoden zum Erstellen der Steuerelemente. Um den Builder zu füllen, musst du festlegen, welches Gerät du steuern möchtest und wie der Nutzer damit interagiert. Führen Sie diese Schritte aus:

  1. Wähle 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. Lege 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. Diese Vorlage zeigt dem Nutzer direkt den Steuerungsstatus sowie die verfügbaren Eingabemethoden an, d. h. die ControlAction. In der folgenden Tabelle sind einige der verfügbaren Vorlagen und die unterstützten Aktionen aufgeführt:
Vorlage Aktion Beschreibung
ControlTemplate.getNoTemplateObject() None Die Anwendung kann damit Informationen zum Steuerelement vermitteln, aber der Nutzer kann nicht damit interagieren.
ToggleTemplate BooleanAction Stellt ein Steuerelement dar, das zwischen aktiviertem und deaktiviertem Zustand 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 Steuern von dimmbaren Lampen.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Zusätzlich zu den vorherigen Aktionen kann der Nutzer mit dieser Vorlage 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 Infrarot-Fernbedienung für den Fernseher. Mit dieser Vorlage können Sie eine Routine oder ein Makro definieren, also eine Zusammenfassung von Steuerungs- und Statusänderungen.

Mit diesen Informationen können Sie die Steuerung erstellen:

Wenn du beispielsweise eine smarte Glühbirne und einen Thermostat steuern möchtest, füge deiner 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 Einstellung erstellt haben, muss sie einem Publisher zugewiesen werden. Der Publisher informiert die System-UI über das Vorhandensein des Steuerelements. Die Klasse ControlsProviderService 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 eine Publisher für eine Liste von angegebenen Steuerelementen, die durch ihre String-IDs identifiziert werden. Verwenden Sie Control.StatefulBuilder, um diese Control-Objekte zu erstellen, da der Publisher jedem Steuerelement einen Status zuweisen muss.

Publisher erstellen

Wenn deine 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 die verfügbaren Steuerelemente für das System anzugeben. 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 ihre bevorzugten Steuerelemente auswählen.

Wenn Sie Kotlin-Koroutinen zum Erstellen eines ControlsProviderService verwenden 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 das folgende Snippet in Ihre Service ein, 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 die Schaltfläche Gerätesteuerung, wie in Abbildung 4 dargestellt:

System-Benutzeroberfläche für Gerätesteuerung
Abbildung 4. Gerätesteuerung im Systemmenü

Wenn du auf Gerätesteuerung tippst, gelangst du zu einem zweiten Bildschirm, auf dem du deine App auswählen kannst. Nachdem du deine App ausgewählt hast, siehst du, wie das vorherige Snippet ein benutzerdefiniertes Systemmenü mit deinen neuen Steuerelementen erstellt, wie in Abbildung 5 dargestellt:

Systemmenü mit einer Licht- und Thermostatsteuerung
Abbildung 5: Licht- und Thermostatsteuerung, die hinzugefügt werden soll.

Implementieren Sie jetzt die Methode createPublisherFor und fügen Sie Folgendes zu Service 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 fiktive Implementierung dessen, was deine App tun muss: mit deinem Gerät kommunizieren, um seinen Status abzurufen, und diesen Status an das System senden.

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

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

Aktionen verarbeiten

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

Fügen Sie Ihrer Service Folgendes 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)
}

Öffne die App, rufe das Menü Gerätesteuerung auf und sieh dir die Steuerelemente für Licht und Thermostat an.

Bedienelemente für eine Lampe und einen Thermostat
Abbildung 6: Licht- und Thermostatsteuerung