Controllare i dispositivi esterni

In Android 11 e versioni successive, la funzionalità Controlli dei dispositivi con accesso rapido consente agli utenti di visualizzare e controllare rapidamente dispositivi esterni come luci, termostati e videocamere da un affordance utente entro tre interazioni da un Avvio app predefinito. L'OEM del dispositivo sceglie quale Avvio app utilizzare. Gli aggregatori di dispositivi, ad esempio Google Home, e le app di fornitori di terze parti possono fornire dispositivi da visualizzare in questo spazio. Questa pagina mostra come visualizzare i controlli dei dispositivi in questo spazio e collegarli alla tua app di controllo.

Figura 1. Spazio di controllo dei dispositivi nell'UI di Android.

Per aggiungere questo supporto, crea e dichiara un ControlsProviderService. Crea i controlli supportati dalla tua app in base ai tipi di controlli predefiniti, quindi crea i publisher per questi controlli.

Interfaccia utente

I dispositivi vengono visualizzati in Controllo dei dispositivi come widget basati su modelli. Sono disponibili cinque widget di controllo dei dispositivi, come mostrato nella figura seguente:

Attiva/disattiva il widget per i controlli dei dispositivi
Attiva/disattiva
Attiva/disattiva con il widget di scorrimento
Attiva/disattiva con il cursore
Widget del cursore dell'intervallo per i controlli del dispositivo
Intervallo (non può essere attivato o disattivato)
Widget di controllo di attivazione/disattivazione stateless
Attivazione/disattivazione stateless
Widget del pannello della temperatura in stato chiuso
Pannello della temperatura (chiuso)
Figura 2. Raccolta di widget basati su modelli.

Se tocchi e tieni premuto un widget, accedi all'app per un controllo più approfondito. Puoi personalizzare l'icona e il colore di ogni widget, ma per un'esperienza utente ottimale, utilizza l'icona e il colore predefiniti se il set predefinito corrisponde al dispositivo.

Widget del pannello della temperatura in stato aperto
Figura 3. Apri il widget del pannello della temperatura.

Crea il servizio

Questa sezione mostra come creare ControlsProviderService. Questo servizio comunica alla UI di sistema Android che la tua app contiene controlli dei dispositivi che devono essere visualizzati nell'area Controllo dei dispositivi della UI di Android.

L'API ControlsProviderService presuppone la conoscenza degli stream reattivi, come definiti nel progetto GitHub Reactive Streams e implementati nelle interfacce Java 9 Flow. L'API si basa sui seguenti concetti:

  • Publisher:la tua applicazione è il publisher.
  • Sottoscrittore:la UI di sistema è il sottoscrittore e può richiedere un numero di controlli dal publisher.
  • Abbonamento: il periodo di tempo durante il quale il publisher può inviare aggiornamenti alla UI di sistema. Il publisher o il sottoscrittore possono chiudere questa finestra.

Dichiara il servizio

L'app deve dichiarare un servizio, ad esempio MyCustomControlService, nel manifest dell'app.

Il servizio deve includere un filtro per intent per ControlsProviderService. Questo filtro consente alle applicazioni di contribuire ai controlli della UI di sistema.

Hai anche bisogno di un label che viene visualizzato nei controlli della UI di sistema.

L'esempio seguente mostra come dichiarare un servizio:

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

Successivamente, crea un nuovo file Kotlin denominato MyCustomControlService.kt ed estendi ControlsProviderService:

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

Seleziona il tipo di controllo corretto

L'API fornisce metodi di creazione per creare i controlli. Per compilare il builder, determina il dispositivo che vuoi controllare e il modo in cui l'utente interagisce con questo. Segui questi passaggi:

  1. Scegli il tipo di dispositivo rappresentato dal controllo. La classe DeviceTypes è un'enumerazione di tutti i dispositivi supportati. Il tipo viene utilizzato per determinare le icone e i colori del dispositivo nell'interfaccia utente.
  2. Determina il nome rivolto agli utenti, la posizione del dispositivo, ad esempio cucina, e altri elementi di testo dell'interfaccia utente associati al controllo.
  3. Scegli il modello migliore per supportare l'interazione degli utenti. Ai controlli viene assegnato un ControlTemplate dall'applicazione. Questo modello mostra direttamente all'utente lo stato del controllo e i metodi di input disponibili, ovvero ControlAction. La tabella seguente illustra alcuni dei modelli disponibili e le azioni che supportano:
Modello Azione Descrizione
ControlTemplate.getNoTemplateObject() None L'applicazione potrebbe utilizzarlo per trasmettere informazioni sul controllo, ma l'utente non può interagire con questo elemento.
ToggleTemplate BooleanAction Rappresenta un controllo che può essere attivato e disattivato. L'oggetto BooleanAction contiene un campo che cambia per rappresentare il nuovo stato richiesto quando l'utente tocca il controllo.
RangeTemplate FloatAction Rappresenta un widget di scorrimento con valori minimo, massimo e di incremento specificati. Quando l'utente interagisce con il cursore, invia un nuovo oggetto FloatAction all'applicazione con il valore aggiornato.
ToggleRangeTemplate BooleanAction, FloatAction Questo modello è una combinazione di ToggleTemplate e RangeTemplate. Supporta gli eventi touch e un cursore, ad esempio per controllare le luci dimmerabili.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Oltre a incapsulare le azioni precedenti, questo modello consente all'utente di impostare una modalità, ad esempio caldo, freddo, caldo/freddo, eco o spento.
StatelessTemplate CommandAction Utilizzato per indicare un controllo che fornisce funzionalità touch, ma il cui stato non può essere determinato, ad esempio un telecomando TV a infrarossi. Puoi utilizzare questo modello per definire una routine o una macro, ovvero un'aggregazione di modifiche di controllo e stato.

Con queste informazioni, puoi creare il controllo:

Ad esempio, per controllare una lampadina smart e un termostato, aggiungi le seguenti costanti al tuo MyCustomControlService:

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

Crea publisher per i controlli

Dopo aver creato il controllo, devi assegnargli un publisher. Il publisher informa la UI di sistema dell'esistenza del controllo. La classe ControlsProviderService ha due metodi publisher di cui devi eseguire l'override nel codice dell'applicazione:

  • createPublisherForAllAvailable: crea unPublisher per tutti i controlli disponibili nella tua app. UsaControl.StatelessBuilder costruireControl oggetti per questo editore.
  • createPublisherFor: crea un Publisher per un elenco di controlli specifici, identificati dai relativi identificatori di stringa. Utilizza Control.StatefulBuilder per creare questi oggetti Control, perché il publisher deve assegnare uno stato a ogni controllo.

Crea il publisher

Quando l'app pubblica per la prima volta i controlli nella UI di sistema, non conosce lo stato di ogni controllo. Il recupero dello stato può essere un'operazione che richiede molto tempo e che comporta molti hop nella rete del fornitore del dispositivo. Utilizza il metodo createPublisherForAllAvailable per annunciare i controlli disponibili al sistema. Questo metodo utilizza la classe builder Control.StatelessBuilder, perché lo stato di ogni controllo è sconosciuto.

Una volta visualizzati i controlli nell'UI di Android, gli utenti possono selezionare i controlli preferiti.

Per utilizzare le coroutine Kotlin per creare un ControlsProviderService, aggiungi una nuova dipendenza al tuo build.gradle:

Alla moda

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

Kotlin

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

Dopo aver sincronizzato i file Gradle, aggiungi il seguente snippet al tuo Service per implementare createPublisherForAllAvailable:

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

Scorri verso il basso il menu di sistema e individua il pulsante Controlli dei dispositivi, mostrato nella figura 4:

Interfaccia utente di sistema per i controlli dei dispositivi
Figura 4. Controlli dei dispositivi nel menu di sistema.

Toccando Controlli dei dispositivi, si passa a una seconda schermata in cui puoi selezionare la tua app. Una volta selezionata l'app, vedrai come lo snippet precedente crea un menu di sistema personalizzato che mostra i nuovi controlli, come mostrato nella figura 5:

Menu di sistema che mostra un controllo della luce e del termostato
Figura 5. Controlli di luci e termostato da aggiungere.

Ora implementa il metodo createPublisherFor aggiungendo quanto segue a Service:

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 questo esempio, il metodo createPublisherFor contiene un'implementazione fittizia di ciò che l'app deve fare: comunicare con il dispositivo per recuperare il suo stato e trasmetterlo al sistema.

Il metodo createPublisherFor utilizza coroutine e flussi Kotlin per soddisfare l'API Reactive Streams richiesta nel seguente modo:

  1. Crea un Flow.
  2. Attende un secondo.
  3. Crea ed emette lo stato della luce smart.
  4. Attende un altro secondo.
  5. Crea e trasmette lo stato del termostato.

Gestisci le azioni

Il metodo performControlAction indica quando l'utente interagisce con un controllo pubblicato. Il tipo di ControlAction inviato determina l'azione. Esegui l'azione appropriata per il controllo specificato, quindi aggiorna lo stato del dispositivo nell'UI di Android.

Per completare l'esempio, aggiungi quanto segue al tuo Service:

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

Esegui l'app, accedi al menu Controlli dei dispositivi e visualizza i controlli delle luci e del termostato.

Controlli che mostrano una luce e un termostato
Figura 6. Controlli di luci e termostato.