Contrôler les appareils externes

Dans Android 11 et versions ultérieures, la fonctionnalité "Commandes de contrôle des appareils" permet à l'utilisateur d'afficher et de contrôler rapidement des appareils externes tels que des lumières, des thermostats et des caméras à partir d'une fonctionnalité utilisateur en trois interactions à partir d'un lanceur par défaut. L'OEM de l'appareil choisit le lanceur qu'il utilise. 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 explique comment afficher les commandes de contrôle des appareils dans cet espace et les associer à votre application de contrôle.

Figure 1. Espace de contrôle des appareils dans l'interface utilisateur 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 sont affichés sous Commandes de contrôle des appareils sous forme de widgets basés sur des modèles. Cinq widgets de contrôle des appareils sont disponibles, comme illustré dans la figure suivante :

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

Appuyer de manière prolongée sur un widget vous permet d'accéder à l'application pour un contrôle plus approfondi. Vous pouvez personnaliser l'icône et la couleur de chaque widget, mais 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.

Image montrant le widget du panneau de température (ouvert)
Figure 3. Widget de panneau de température ouvert.

Créer le service

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

L'API ControlsProviderService suppose une connaissance des flux réactifs, tels que définis dans le projet Reactive Streams GitHub et implémentés dans les interfaces Java 9 Flow. L'API est basée sur les concepts suivants :

  • Éditeur : votre application est l'éditeur.
  • Abonné : l'interface utilisateur 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 d'application.

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'interface utilisateur 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>

Créez ensuite un fichier Kotlin nommé MyCustomControlService.kt et faites-le étendre ControlsProviderService() :

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Sélectionner le type de commande approprié

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

  1. Choisissez le type d'appareil que la commande représente. La DeviceTypes classe est une énumération de tous les appareils compatibles. Le type est utilisé pour 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 textuels de l'interface utilisateur associés à la commande.
  3. Choisissez le meilleur modèle pour prendre en charge l'interaction de l'utilisateur. Une ControlTemplate est attribuée aux commandes à partir de l'application. Ce modèle affiche directement l'état de la commande à l' utilisateur, ainsi que les méthodes d'entrée disponibles, c'est-à-dire l' ControlAction. Le tableau suivant présente certains des modèles disponibles et les actions qu'ils prennent en charge :
Modèle Action Description
ControlTemplate.getNoTemplateObject() None L'application peut utiliser cette option 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 états. 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 minimales, maximales et de pas 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 prend en charge les événements tactiles ainsi qu'un curseur, par exemple pour contrôler les lumières à intensité variable.
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, refroidissement, chauffage/refroidissement, éco ou arrêt.
StatelessTemplate CommandAction Permet d'indiquer une commande qui offre une fonctionnalité tactile, mais dont l'état ne peut pas être déterminé, comme une télécommande infrarouge. Vous pouvez utiliser ce modèle pour définir une routine ou une macro, qui est une agrégation de modifications de commandes et d'états.

Grâce à ces informations, vous pouvez créer la commande :

Par exemple, pour contrôler une ampoule connectée et un thermostat, ajoutez les constantes suivantes à votre MyCustomControlService :

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

Créer des éditeurs pour les commandes

Une fois la commande créée, elle a besoin d'un éditeur. L'éditeur informe l'interface utilisateur 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 commandes données, identifiées par leurs identifiants de chaîne. Utilisez Control.StatefulBuilder pour créer ces Control objets, car l'éditeur doit attribuer un état à chaque commande.

Créer l'éditeur

Lorsque votre application publie pour la première fois des commandes dans l'interface utilisateur du système, elle ne connaît pas l'état de chaque commande. L'obtention de l'état peut être une opération longue impliquant de nombreux sauts dans le réseau du fournisseur de l'appareil. Utilisez la createPublisherForAllAvailable() méthode 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 les commandes affichées dans l'interface utilisateur Android , l'utilisateur peut sélectionner ses commandes favorites.

Pour utiliser les 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")
}

Une fois vos fichiers Gradle synchronisés, ajoutez l'extrait de code suivant à votre Service pour implémenter createPublisherForAllAvailable() :

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<String, ReplayProcessor> 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);
        }
    }
    

Balayez l'écran vers le bas dans le menu système et recherchez le bouton Commandes de contrôle des appareils, illustré dans la figure 4 :

Image montrant l&#39;interface utilisateur du système pour les commandes de l&#39;appareil
Figure 4. Commandes de contrôle des appareils dans le menu système.

Appuyez sur Commandes de contrôle des appareils pour accéder à un deuxième écran où vous pouvez sélectionner votre application. Une fois que vous avez sélectionné votre application, vous voyez 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 :

Image montrant le menu système contenant une commande d&#39;éclairage et de thermostat
Figure 5. Commandes de contrôle des lumières et du thermostat à ajouter.

Implémentez maintenant la méthode createPublisherFor(), en ajoutant les éléments suivants à votre Service :

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf<String, MutableSharedFlow>()
 
    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();
    }
    

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 émettre cet état vers le système.

La méthode createPublisherFor() utilise les coroutines et les flux Kotlin pour répondre à l'API Reactive Streams requise en procédant comme suit :

  1. Crée un Flow.
  2. Attend une seconde.
  3. Crée et émet l'état de la lumière connectée.
  4. Attend une autre seconde.
  5. Crée et émet l'état du thermostat.

Gérer les actions

La méthode performControlAction() signale lorsque 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'interface utilisateur Android.

Pour terminer l'exemple, ajoutez les éléments suivants à votre Service :

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

Exécutez l'application, accédez au menu Commandes de contrôle des appareils et consultez les commandes de contrôle des lumières et du thermostat.

Image montrant une commande d&#39;éclairage et de thermostat
Figure 6. Commandes de contrôle des lumières et du thermostat.