Dans Android 11 et versions ultérieures, la fonctionnalité d'accès rapide aux appareils permet à l'utilisateur d'afficher et de contrôler rapidement les appareils externes tels que les lumières, les thermostats et les caméras à partir d'une affordance de l'utilisateur au cours de trois interactions à partir d'un lanceur d'applications par défaut. L'OEM de l'appareil choisit le lanceur d'applications 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 vous explique comment afficher les commandes de l'appareil dans cet espace et les associer à votre application de contrôle.
Pour ajouter cette prise en charge, 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 la forme de widgets basés sur un modèle. Cinq widgets de commande d'appareil sont disponibles, comme illustré dans la figure suivante:
|
|
|
|
|
Appuyez de manière prolongée sur un widget pour accéder à l'application et bénéficier d'un contrôle accru. 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 celles définies par défaut correspondent à l'appareil.
Créer le service
Cette section explique comment créer le ControlsProviderService
.
Ce service indique à l'UI 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
part du principe que vous connaissez les flux réactifs, tels que définis dans le projet GitHub Reactive Streams et mis en œuvre dans les interfaces de flux Java 9.
L'API repose sur les concepts suivants:
- Éditeur:votre application est l'éditeur.
- Abonné:l'UI du système correspond à l'abonné. Elle peut demander un certain nombre de commandes à l'éditeur.
- Abonnement:la 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 d'ajouter des commandes à 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()
:
Kotlin
class MyCustomControlService : ControlsProviderService() { ... }
Java
public class MyCustomJavaControlService extends ControlsProviderService { ... }
Sélectionnez le type de commande approprié
L'API fournit des méthodes de compilateur pour créer les commandes. Pour remplir 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 :
- Sélectionnez le type d'appareil représenté par la commande. La classe
DeviceTypes
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. - Déterminez le nom visible par l'utilisateur, l'emplacement de l'appareil (par exemple, cuisine) et les autres éléments textuels de l'interface utilisateur associés à la commande.
- Choisissez le meilleur modèle pour favoriser les interactions utilisateur. Un élément
ControlTemplate
est attribué aux commandes depuis l'application. Ce modèle présente directement l'état de contrôle à l'utilisateur ainsi que les modes de saisie disponibles, c'est-à-direControlAction
. Le tableau suivant présente certains des modèles disponibles et les actions qu'ils prennent en charge:
Template | 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 pouvant basculer entre l'état activé et l'état désactivé. 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 de pas spécifiées. Lorsque l'utilisateur interagit avec le curseur, renvoyez un nouvel objet FloatAction avec la valeur mise à jour à l'application.
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
Ce modèle est une combinaison des éléments ToggleTemplate et RangeTemplate . Elle accepte les événements tactiles ainsi qu'un curseur, par exemple pour contrôler la diminution de la luminosité 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, Chauff./Clim., Éco ou éteint. |
StatelessTemplate
|
CommandAction
|
Indique une commande qui offre une fonctionnalité tactile, mais dont l'état ne peut pas être déterminé, comme une télécommande de télévision infrarouge. Vous pouvez utiliser ce modèle pour définir une routine ou une macro, qui est une agrégation de changements de contrôle et d'état. |
À l'aide de ces informations, vous pouvez créer la commande:
- Utilisez la classe de compilateur
Control.StatelessBuilder
lorsque l'état de la commande est inconnu. - Utilisez la classe de compilateur
Control.StatefulBuilder
lorsque l'état de la commande est connu.
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 unPublisher
pour toutes les commandes disponibles dans votre application. UtilisezControl.StatelessBuilder()
afin de créer des objetsControl
pour cet éditeur.createPublisherFor()
: crée unPublisher
pour une liste de commandes spécifiques, identifiées par leurs identifiants de chaîne. UtilisezControl.StatefulBuilder
pour créer ces objetsControl
, car l'éditeur doit attribuer un état à chaque commande.
Créer l'éditeur
Lorsque votre application publie des commandes pour la première fois dans l'UI du système, elle ne connaît pas l'état de chaque commande. L'obtention de l'état peut être une opération fastidieuse impliquant de nombreux sauts sur le réseau du fournisseur d'appareils. Utilisez la méthode createPublisherForAllAvailable()
pour annoncer les contrôles 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 apparaissent dans l'interface utilisateur Android , l'utilisateur peut sélectionner les commandes préférées.
Pour utiliser des coroutines Kotlin afin de créer un ControlsProviderService
, ajoutez une 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 que vous avez synchronisé vos fichiers Gradle, ajoutez l'extrait suivant à votre fichier 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> 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 le menu système vers le bas et recherchez le bouton Commandes de contrôle des appareils, illustré dans la figure 4:
Si vous appuyez sur Commandes de l'appareil, vous accédez à un deuxième écran sur lequel vous pouvez sélectionner votre application. Une fois que vous avez sélectionné votre application, vous voyez comment l'extrait précédent crée un menu système personnalisé affichant vos nouvelles commandes, comme illustré dans la figure 5:
Implémentez maintenant la méthode createPublisherFor()
en ajoutant le code suivant à votre Service
:
Kotlin
private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) private val controlFlows = mutableMapOf>() 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.PublishercreatePublisherFor(@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, puis transmettre cet état au système.
La méthode createPublisherFor()
utilise des coroutines et des flux Kotlin pour satisfaire à l'API Reactive Streams requise en procédant comme suit:
- Permet de créer un
Flow
. - Attend une seconde.
- Crée et émet l'état de l'ampoule connectée.
- Attend encore une seconde.
- Crée et émet l'état du thermostat.
Gérer les actions
La méthode performControlAction()
indique quand l'utilisateur interagit avec une commande publiée. Le type d'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 le code suivant à 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 Consumerconsumer) { 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 Device controls (Commandes de contrôle des appareils), et consultez vos commandes d'éclairage et de thermostat.