Как управлять внешними устройствами

В Android 11 и более поздних версиях функция быстрого доступа к управлению устройствами позволяет пользователям быстро просматривать внешние устройства, такие как светильники, термостаты и камеры, и управлять ими с помощью пользовательского интерфейса в пределах трех взаимодействий из средства запуска по умолчанию. OEM-производитель устройства выбирает, какой лаунчер использовать. Агрегаторы устройств, например Google Home, и приложения сторонних поставщиков могут предоставлять устройства для показа в этом разделе. На этой странице рассказывается, как показывать элементы управления устройствами в этом разделе и связывать их с приложением для управления.

Рисунок 1. Раздел управления устройствами в интерфейсе Android.

Чтобы добавить такую поддержку, создайте и объявите ControlsProviderService. Создайте элементы управления, которые поддерживает ваше приложение, на основе предопределенных типов элементов управления, а затем создайте издателей для этих элементов управления.

Пользовательский интерфейс

Устройства отображаются в разделе Управление устройствами в виде виджетов. Доступно пять виджетов управления устройствами, как показано на рисунке ниже.

Переключатель виджета для управления устройствами
Переключить
Переключатель с ползунком
Переключатель с ползунком
Виджет ползунка для выбора диапазона в настройках управления устройством
Диапазон (нельзя включить или отключить)
Виджет переключателя без сохранения состояния
Переключатель без сохранения состояния
Виджет панели температуры в закрытом состоянии
Панель температуры (закрыта)
Рисунок 2. Коллекция шаблонов виджетов.

Если нажать на виджет и удерживать, откроется приложение, в котором можно выполнить более точную настройку. Вы можете настроить значок и цвет для каждого виджета, но для удобства пользователей лучше использовать значок и цвет по умолчанию, если они соответствуют устройству.

Виджет панели температуры в открытом состоянии
Рисунок 3. Открыть виджет панели температуры.

Создайте сервис

В этом разделе рассказывается, как создать ControlsProviderService. Этот сервис сообщает системному интерфейсу Android, что ваше приложение содержит элементы управления устройствами, которые должны отображаться в разделе Управление устройствами.

Для работы с API ControlsProviderService необходимо иметь представление о реактивных потоках, описанных в проекте Reactive Streams на GitHub и реализованных в интерфейсах Java 9 Flow. 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 есть методы для создания элементов управления. Чтобы заполнить конструктор, определите, каким устройством вы хотите управлять и как пользователь взаимодействует с ним. Выполните следующие действия:

  1. Выберите тип устройства, которым управляет элемент. Класс DeviceTypes представляет собой перечисление всех поддерживаемых устройств. Тип используется для определения значков и цветов устройства в интерфейсе.
  2. Определите пользовательское название, местоположение устройства (например, кухня) и другие текстовые элементы интерфейса, связанные с элементом управления.
  3. Выберите шаблон, который лучше всего подходит для взаимодействия с пользователями. Элементам управления назначается ControlTemplate из приложения. Этот шаблон напрямую показывает пользователю состояние элемента управления, а также доступные методы ввода, то есть ControlAction. В таблице ниже перечислены некоторые доступные шаблоны и действия, которые они поддерживают.
Шаблон Действие Описание
ControlTemplate.getNoTemplateObject() None Приложение может использовать его для передачи информации об элементе управления, но пользователь не может взаимодействовать с ним.
ToggleTemplate BooleanAction Представляет элемент управления, который можно включать и отключать. Объект BooleanAction содержит поле, которое меняется, чтобы отразить новое состояние, когда пользователь нажимает на элемент управления.
RangeTemplate FloatAction Представляет виджет ползунка с указанными минимальным, максимальным и пошаговым значениями. Когда пользователь взаимодействует с ползунком, отправьте в приложение новый объект FloatAction с обновленным значением.
ToggleRangeTemplate BooleanAction, FloatAction Этот шаблон представляет собой комбинацию ToggleTemplate и RangeTemplate. Поддерживает события касания и ползунок, например для управления яркостью света.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Помимо перечисленных выше действий, этот шаблон позволяет пользователю задать режим, например обогрев, охлаждение, обогрев/охлаждение, эко или выкл.
StatelessTemplate CommandAction Используется для обозначения элемента управления, который поддерживает сенсорный ввод, но состояние которого определить невозможно, например инфракрасного пульта ДУ для телевизора. Этот шаблон можно использовать для определения программы или макроса, которые представляют собой совокупность изменений состояния и управления.

Теперь вы можете создать элемент управления:

  • Если состояние элемента управления неизвестно, используйте класс builder Control.StatelessBuilder.
  • Если состояние элемента управления известно, используйте класс builder 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 используйте Control.StatefulBuilder, поскольку издатель должен назначить состояние каждому элементу управления.

Создайте издателя

Когда приложение впервые публикует элементы управления в системном интерфейсе, оно не знает состояние каждого элемента управления. Получение состояния может занять много времени и потребовать нескольких переходов в сети поставщика устройства. Используйте метод createPublisherForAllAvailable, чтобы сообщить системе о доступных элементах управления. В этом методе используется класс builder 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.

Системный пользовательский интерфейс для управления устройствами
Рисунок 4. Управление устройствами в системном меню.

Нажав Управление устройствами, вы перейдете на второй экран, где можно выбрать приложение. После этого вы увидите, как предыдущий фрагмент создает настраиваемое системное меню с новыми элементами управления, как показано на рисунке 5.

Системное меню с элементами управления светом и термостатом
Рисунок 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. Для этого он выполняет следующие действия:

  1. Создает Flow.
  2. Ожидание в течение одной секунды.
  3. Создает и передает состояние умного светильника.
  4. Подождите ещё секунду.
  5. Создает и передает состояние термостата.

Как обрабатывать действия

Метод 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)
}

Запустите приложение, откройте меню Управление устройствами и посмотрите, как управлять светом и термостатом.

Управляет светильником и термостатом.
Рисунок 6. Управление освещением и термостатом.