В Android 11 и более поздних версий есть функция быстрого доступа к элементам управления устройствами. Она позволяет пользователям быстро просматривать и контролировать внешние устройства, такие как светильники, термостаты и камеры, с помощью трех действий в стандартном лаунчере. Производитель устройства выбирает, какой лаунчер использовать. Агрегаторы устройств, например Google Home, и приложения сторонних поставщиков могут предоставлять устройства для показа в этом разделе. В этой статье рассказывается, как показывать в этом пространстве элементы управления устройствами и связывать их с приложением для управления.
Чтобы добавить поддержку, создайте и объявите ControlsProviderService. Создайте элементы управления, которые поддерживает ваше приложение, на основе предопределенных типов элементов управления, а затем создайте издателей для этих элементов управления.
Пользовательский интерфейс
Устройства отображаются в разделе Управление устройствами в виде виджетов с шаблонами. Доступны пять виджетов управления устройствами, как показано на рисунке ниже:
|
|
|
|
|
Если нажать на виджет и удерживать его, откроется приложение, в котором можно управлять настройками. Вы можете настроить значок и цвет каждого виджета, но для удобства пользователей лучше использовать значок и цвет по умолчанию, если они подходят для устройства.
Создайте сервис
В этом разделе рассказывается, как создать файл ControlsProviderService. Этот сервис сообщает системному интерфейсу Android, что в вашем приложении есть элементы управления устройствами, которые нужно показывать в разделе Управление устройствами интерфейса Android.
При работе с API ControlsProviderService предполагается, что вы знакомы с реактивными потоками, как они определены в проекте Reactive Streams на GitHub и реализованы в интерфейсах Flow в Java 9.
API основан на следующих концепциях:
- Издатель. Ваше приложение является издателем.
- Подписчик. Системный интерфейс является подписчиком и может запрашивать у издателя определенное количество элементов управления.
- Подписка – период времени, в течение которого издатель может отправлять обновления в системный интерфейс. Закрыть окно может как издатель, так и подписчик.
Как объявить сервис
В манифесте приложения должна быть объявлена служба, например MyCustomControlService.
Сервис должен включать фильтр интентов для ControlsProviderService. Этот фильтр позволяет приложениям добавлять элементы управления в системный интерфейс.
Вам также понадобится label, который будет показываться в элементах управления в системном интерфейсе.
Ниже приведен пример объявления службы:
<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>
Затем создайте новый файл Kotlin с названием MyCustomControlService.kt и сделайте так, чтобы он расширял ControlsProviderService:
class MyCustomControlService : ControlsProviderService() { // ... }
Как выбрать правильный тип управления
В API есть методы создания элементов управления. Чтобы заполнить конструктор, определите, каким устройством вы хотите управлять и как пользователь будет с ним взаимодействовать. Выполните следующие действия:
- Выберите тип устройства, которым управляет элемент. Класс
DeviceTypesпредставляет собой перечисление всех поддерживаемых устройств. Тип используется для определения значков и цветов устройства в интерфейсе. - Определите название, которое будет видеть пользователь, местоположение устройства (например, кухня) и другие текстовые элементы интерфейса, связанные с элементом управления.
- Выберите подходящий шаблон для взаимодействия с пользователями. Элементам управления присваивается
ControlTemplateиз приложения. Этот шаблон напрямую показывает пользователю состояние элемента управления, а также доступные способы ввода, то естьControlAction. В таблице ниже перечислены некоторые доступные шаблоны и поддерживаемые ими действия.
| Шаблон | Действие | Описание |
ControlTemplate.getNoTemplateObject()
|
None
|
Приложение может использовать его для передачи информации об элементе управления, но пользователь не может взаимодействовать с ним. |
ToggleTemplate
|
BooleanAction
|
Представляет собой элемент управления, который можно включить или отключить. Объект BooleanAction содержит поле, которое меняется,
чтобы отразить запрошенное новое состояние, когда пользователь нажимает на элемент управления.
|
RangeTemplate
|
FloatAction
|
Представляет виджет ползунка с указанными минимальным, максимальным и шаговым значениями. Когда пользователь взаимодействует со слайдером, отправьте в приложение новый объект FloatAction с обновленным значением.
|
ToggleRangeTemplate
|
BooleanAction, FloatAction
|
Этот шаблон представляет собой сочетание ToggleTemplate и RangeTemplate. Он поддерживает события касания, а также ползунок, например для управления диммируемыми светильниками.
|
TemperatureControlTemplate
|
ModeAction, BooleanAction, FloatAction
|
Этот шаблон не только включает перечисленные выше действия, но и позволяет пользователю задать режим, например "Обогрев", "Охлаждение", "Обогрев/охлаждение", "Экономичный" или "Выключено". |
StatelessTemplate
|
CommandAction
|
Используется для обозначения элемента управления, который поддерживает сенсорный ввод, но состояние которого определить невозможно, например пульта ДУ телевизора с инфракрасным портом. Этот шаблон можно использовать для определения процедуры или макроса, представляющего собой совокупность изменений состояния и элементов управления. |
Используя эту информацию, вы можете создать элемент управления:
- Используйте класс
Control.StatelessBuilder, если состояние элемента управления неизвестно. - Используйте класс конструктора
Control.StatefulBuilder, когда состояние элемента управления известно.
Например, чтобы управлять умной лампочкой и термостатом, добавьте в 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() { // ... }
Создайте издателей для элементов управления
После создания элемента управления ему нужно назначить издателя. Издатель сообщает о наличии элемента управления в интерфейс системы. В классе ControlsProviderService есть два метода издателя, которые необходимо переопределить в коде приложения:
createPublisherForAllAvailable– создаетPublisherдля всех элементов управления, доступных в приложении. ИспользуйтеControl.StatelessBuilder, чтобы создать объектыControlдля этого издателя.createPublisherFor: создаетPublisherдля списка заданных элементов управления, идентифицированных по строковым идентификаторам. ИспользуйтеControl.StatefulBuilder, чтобы создать эти объектыControl, поскольку издатель должен назначить статус каждому элементу управления.
Как создать издателя
Когда приложение впервые публикует элементы управления в системном интерфейсе, оно не знает состояние каждого из них. Получение состояния может быть длительной операцией, включающей множество переходов в сети поставщика устройства. Используйте метод createPublisherForAllAvailable, чтобы сообщить системе о доступных элементах управления. В этом методе используется класс Control.StatelessBuilder, поскольку состояние каждого элемента управления неизвестно.
После того как элементы управления появятся в интерфейсе Android, пользователи смогут выбрать избранные.
Чтобы использовать сопрограммы Kotlin для создания ControlsProviderService, добавьте новую зависимость в build.gradle:
Яркий
dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4" }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4") }
После синхронизации файлов Gradle добавьте в файл Service следующий фрагмент кода, чтобы реализовать 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() } }
Проведите вниз по системному меню и найдите кнопку Управление устройствами, как показано на рисунке 4:
Нажав Управление устройством, вы перейдете на второй экран, где можно выбрать приложение. После этого вы увидите, как предыдущий фрагмент кода создает специальное системное меню с новыми элементами управления (рис. 5).
Теперь реализуйте метод createPublisherFor, добавив в 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() }
В этом примере метод createPublisherFor содержит фиктивную реализацию того, что должно делать ваше приложение: взаимодействовать с устройством, чтобы получить его статус, и передавать этот статус системе.
Метод createPublisherFor использует сопрограммы и потоки Kotlin, чтобы соответствовать требованиям Reactive Streams API. Для этого он выполняет следующие действия:
- Создает
Flow. - Ждет одну секунду.
- Создает и передает состояние умного светильника.
- Ждет ещё одну секунду.
- Создает и передает состояние термостата.
Как обрабатывать действия
Метод performControlAction сигнализирует о том, что пользователь взаимодействует с опубликованным элементом управления. Действие определяется типом отправленного ControlAction.
Выполните нужное действие для заданного элемента управления, а затем обновите состояние устройства в интерфейсе Android.
Чтобы завершить пример, добавьте в 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) }
Запустите приложение, откройте меню Управление устройствами и найдите элементы управления освещением и термостатом.