Contrôler les appareils externes

Sous Android 11 et les versions ultérieures, la fonctionnalité de commandes d'accès rapide aux appareils permet aux utilisateurs d'afficher et de contrôler rapidement des appareils externes tels que des lumières, des thermostats et des caméras à partir d'une affordance utilisateur en trois interactions depuis un lanceur d'applications par défaut. L'OEM de l'appareil choisit le lanceur d'applications à utiliser. Les agrégateurs d'appareils (par exemple, Google Home) et les applications de fournisseurs tiers peuvent fournir des appareils à afficher dans cet espace. Cette page vous explique comment afficher les commandes de l'appareil dans cet espace et les associer à votre application de contrôle.

Figure 1. Espace de contrôle des appareils dans l'UI Android.

Pour ajouter cette compatibilité, créez et déclarez un ControlsProviderService. Créez les commandes compatibles avec votre application en fonction de types de commandes prédéfinis, puis créez des éditeurs pour ces commandes.

Interface utilisateur

Les appareils s'affichent sous Contrôle des appareils sous forme de widgets modèles. Cinq widgets de contrôle des appareils sont disponibles, comme illustré dans la figure suivante :

Activer/Désactiver le widget pour les commandes de l'appareil
Activer/Désactiver
Widget de curseur d'activation/désactivation
Activer/Désactiver avec le curseur
Widget de curseur de plage pour les commandes de l'appareil
Plage (ne peut pas être activée ni désactivée)
Widget de commande d'activation/désactivation sans état
Bouton d'activation/désactivation sans état
Widget du panneau de température à l'état fermé
Panneau de température (fermé)
Figure 2. Collection de widgets basés sur des modèles.

Appuyez de manière prolongée sur un widget pour accéder à l'application et à des commandes plus détaillées. Vous pouvez personnaliser l'icône et la couleur de chaque widget. Toutefois, pour une expérience utilisateur optimale, utilisez l'icône et la couleur par défaut si l'ensemble par défaut correspond à l'appareil.

Widget du panneau de température à l'état ouvert
Figure 3 : Ouvrez le widget du panneau de température.

Créer le service

Cette section explique comment créer le fichier ControlsProviderService. Ce service indique à l'UI du système Android que votre application contient des commandes d'appareil qui doivent être affichées dans la zone Commandes de l'appareil de l'UI Android.

L'API ControlsProviderService suppose que vous connaissez les flux réactifs, tels qu'ils sont définis dans le projet GitHub Reactive Streams et implémentés dans les interfaces Java 9 Flow. L'API repose sur les concepts suivants :

  • Éditeur : votre application est l'éditeur.
  • Abonné : l'UI du système est l'abonné et peut demander un certain nombre de commandes à l'éditeur.
  • Abonnement : période pendant laquelle l'éditeur peut envoyer des mises à jour à l'UI du système. L'éditeur ou l'abonné peut fermer cette fenêtre.

Déclarer le service

Votre application doit déclarer un service (tel que MyCustomControlService) dans son fichier manifeste.

Le service doit inclure un filtre d'intent pour ControlsProviderService. Ce filtre permet aux applications de contribuer aux commandes de l'interface utilisateur du système.

Vous avez également besoin d'un label qui s'affiche dans les commandes de l'UI du système.

L'exemple suivant montre comment déclarer un service :

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

Ensuite, créez un fichier Kotlin nommé MyCustomControlService.kt et faites-le étendre ControlsProviderService :

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

Sélectionner le type de commande approprié

L'API fournit des méthodes de compilation pour créer les commandes. Pour renseigner le compilateur, déterminez l'appareil que vous souhaitez contrôler et la manière dont l'utilisateur interagit avec lui. Procédez comme suit :

  1. Sélectionnez le type d'appareil que la commande représente. La classe DeviceTypes est une énumération de tous les appareils compatibles. Le type permet de déterminer les icônes et les couleurs de l'appareil dans l'interface utilisateur.
  2. Déterminez le nom visible par l'utilisateur, l'emplacement de l'appareil (par exemple, la cuisine) et les autres éléments de texte de l'interface utilisateur associés à la commande.
  3. Choisissez le modèle le plus adapté à l'interaction de l'utilisateur. Les commandes se voient attribuer un ControlTemplate à partir de l'application. Ce modèle affiche directement l'état de la commande pour l'utilisateur, ainsi que les méthodes de saisie disponibles, c'est-à-dire ControlAction. Le tableau suivant présente certains des modèles disponibles et les actions qu'ils permettent d'effectuer :
Modèle Action Description
ControlTemplate.getNoTemplateObject() None L'application peut l'utiliser pour transmettre des informations sur la commande, mais l'utilisateur ne peut pas interagir avec elle.
ToggleTemplate BooleanAction Représente une commande qui peut être activée ou désactivée. L'objet BooleanAction contient un champ qui change pour représenter le nouvel état demandé lorsque l'utilisateur appuie sur la commande.
RangeTemplate FloatAction Représente un widget de curseur avec des valeurs minimale, maximale et d'incrément spécifiées. Lorsque l'utilisateur interagit avec le curseur, renvoyez un nouvel objet FloatAction à l'application avec la valeur mise à jour.
ToggleRangeTemplate BooleanAction, FloatAction Ce modèle est une combinaison de ToggleTemplate et RangeTemplate. Il est compatible avec les événements tactiles ainsi qu'avec un curseur, par exemple pour contrôler l'intensité des lumières.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction En plus d'encapsuler les actions précédentes, ce modèle permet à l'utilisateur de définir un mode, tel que chauffage, climatisation, chauffage/climatisation, éco ou désactivé.
StatelessTemplate CommandAction Utilisé pour indiquer une commande qui fournit une fonctionnalité tactile, mais dont l'état ne peut pas être déterminé, comme une télécommande infrarouge de téléviseur. Vous pouvez utiliser ce modèle pour définir une routine ou une macro, qui est une agrégation de modifications de contrôle et d'état.

Avec ces informations, vous pouvez créer le contrôle :

Par exemple, pour contrôler une ampoule connectée et un thermostat, ajoutez les constantes suivantes à votre 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() {
    // ...
}

Créer des éditeurs pour les contrôles

Une fois que vous avez créé le contrôle, il a besoin d'un éditeur. L'éditeur informe l'UI du système de l'existence de la commande. La classe ControlsProviderService comporte deux méthodes d'éditeur que vous devez remplacer dans le code de votre application :

  • createPublisherForAllAvailable : crée un Publisher pour toutes les commandes disponibles dans votre application. Utilisez Control.StatelessBuilder pour créer des objets Control pour cet éditeur.
  • createPublisherFor : crée un Publisher pour une liste de contrôles donnés, identifiés par leurs identifiants de chaîne. Utilisez Control.StatefulBuilder pour créer ces objets Control, car l'éditeur doit attribuer un état à chaque commande.

Créer l'éditeur

Lorsque votre application publie des commandes dans l'interface utilisateur du système pour la première fois, elle ne connaît pas l'état de chaque commande. L'obtention de l'état peut être une opération chronophage impliquant de nombreux sauts dans le réseau du fournisseur de l'appareil. Utilisez la méthode createPublisherForAllAvailable pour annoncer les commandes disponibles au système. Cette méthode utilise la classe de compilateur Control.StatelessBuilder, car l'état de chaque commande est inconnu.

Une fois que les commandes s'affichent dans l'interface utilisateur Android, les utilisateurs peuvent sélectionner leurs commandes favorites.

Pour utiliser des coroutines Kotlin afin de créer un ControlsProviderService, ajoutez une nouvelle dépendance à votre build.gradle :

Groovy

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

Kotlin

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

Après avoir synchronisé vos fichiers Gradle, ajoutez l'extrait suivant à votre Service pour implémenter 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()
    }
}

Balayez le menu système vers le bas et localisez le bouton Commandes de l'appareil, illustré à la figure 4 :

Interface utilisateur du système pour les commandes de l'appareil
Figure 4 : Commandes de contrôle des appareils dans le menu système.

Appuyez sur Contrôle de l'appareil pour accéder à un deuxième écran où vous pouvez sélectionner votre application. Une fois que vous avez sélectionné votre application, vous pouvez voir comment l'extrait de code précédent crée un menu système personnalisé affichant vos nouvelles commandes, comme illustré dans la figure 5 :

Menu système affichant une commande d'éclairage et de thermostat
Figure 5 : Commandes d'éclairage et de thermostat à ajouter.

À présent, implémentez la méthode createPublisherFor en ajoutant ce qui suit à votre 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()
}

Dans cet exemple, la méthode createPublisherFor contient une fausse implémentation de ce que votre application doit faire : communiquer avec votre appareil pour récupérer son état et l'émettre vers le système.

La méthode createPublisherFor utilise des coroutines et des flux Kotlin pour répondre aux exigences de l'API Reactive Streams en procédant comme suit :

  1. Crée un objet Flow.
  2. Attend une seconde.
  3. Crée et émet l'état de l'ampoule connectée.
  4. Attend une seconde de plus.
  5. Crée et émet l'état du thermostat.

Gérer les actions

La méthode performControlAction signale quand l'utilisateur interagit avec une commande publiée. Le type de ControlAction envoyé détermine l'action. Effectuez l'action appropriée pour la commande donnée, puis mettez à jour l'état de l'appareil dans l'UI Android.

Pour terminer l'exemple, ajoutez ce qui suit à votre 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)
}

Exécutez l'application, accédez au menu Device controls (Contrôles de l'appareil) et affichez les commandes de l'éclairage et du thermostat.

Commandes affichant une lumière et un thermostat
Figure 6 : Commandes d'éclairage et de thermostat.