Управление внешними устройствами

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

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

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

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

Устройства отображаются под элементами управления устройством в качестве шаблонных виджетов. Доступны пять виджетов управления устройством, как показано на следующем рисунке:

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

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

Изображение, показывающее виджет температурной панели (Open)
Рисунок 3. Виджет открытой панели температуры открыт.

Создать услугу

В этом разделе показано, как создать 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() {
        ...
    }
    

Ява

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Выберите правильный тип управления

API предоставляет методы строителя для создания элементов управления. Чтобы заполнить строитель, определите устройство, которое вы хотите управлять, и как пользователь взаимодействует с ним. Выполните следующие шаги:

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

С помощью этой информации вы можете создать элемент управления:

  • Используйте класс Builder Control.StatelessBuilder , когда состояние управления неизвестно.
  • Используйте класс Control.StatefulBuilder Builder, когда известно состояние управления.

Например, чтобы управлять умной лампочкой и термостатом, добавьте следующие константы в свой 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() {
      ...
    }
    

Ява

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

Создайте издателей для элементов управления

После создания элемента управления ему требуется издатель. Издатель сообщает системному пользовательскому интерфейсу о существовании элемента управления. Класс ControlsProviderService имеет два метода издателя, которые необходимо переопределить в коде приложения:

  • createPublisherForAllAvailable() : создает Publisher для всех элементов управления, доступных в вашем приложении. Используйте Control.StatelessBuilder() для создания объектов Control для этого издателя.
  • createPublisherFor() : создаёт Publisher для списка заданных элементов управления, определяемых их строковыми идентификаторами. Для создания этих объектов Control используйте Control.StatefulBuilder , поскольку издатель должен назначить состояние каждому элементу управления.

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

Когда ваше приложение впервые публикует элементы управления в системном пользовательском интерфейсе, оно не знает состояние каждого элемента управления. Получение состояния может быть трудоёмкой операцией, требующей множества переходов в сети поставщика устройства. Используйте метод createPublisherForAllAvailable() для объявления доступных элементов управления системе. Этот метод использует класс-конструктор Control.StatelessBuilder , поскольку состояние каждого элемента управления неизвестно.

Как только элементы управления появляются в пользовательском интерфейсе Android, пользователь может выбрать любимые элементы управления.

Чтобы использовать Cotlin Coroutines для создания ControlsProviderService , добавьте новую зависимость в свой build.gradle Gradle:

Круто

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

Котлин

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

Как только вы синхронизируете свои файлы Gradle, добавьте следующий фрагмент в свой Service для реализации createPublisherForAllAvailable() :

Котлин

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

Ява

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

Пройдите вниз по меню системы и найдите кнопку управления устройством , показанную на рисунке 4:

Изображение, показывающее системный пользовательский интерфейс для управления устройством
Рисунок 4. Элементы управления устройством в системном меню.

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

Изображение, показывающее меню системы, содержащее контроль света и термостата
Рисунок 5. Необходимо добавить элементы управления освещением и термостатом.

Теперь реализуйте метод createPublisherFor() , добавив следующее в свой Service :

Котлин

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

Ява

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

В этом примере метод createPublisherFor() содержит фиктивную реализацию того, что должно делать ваше приложение: взаимодействовать с вашим устройством для получения его статуса и передавать этот статус системе.

Метод createPublisherFor() использует коратики и течет котлин для удовлетворения требуемых API реактивных потоков, выполнив следующее:

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

Действия по обработке

Метод performControlAction() подаёт сигнал, когда пользователь взаимодействует с опубликованным элементом управления. Тип отправленного ControlAction определяет действие. Выполняет соответствующее действие для данного элемента управления, а затем обновляет состояние устройства в пользовательском интерфейсе Android.

Чтобы завершить пример, добавьте следующее в свой Service :

Котлин

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

Ява

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

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

Изображение, показывающее управление освещением и термостатом
Рисунок 6. Регуляторы освещения и термостата.