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

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

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

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

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

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

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

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

Изображение, показывающее виджет панели температуры (открыто)
Рисунок 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 Представляет виджет ползунка с указанными минимальными, максимальными и шаговыми значениями. Когда пользователь взаимодействует с ползунком, отправляет новый объект 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() {
      ...
    }
    

Ява

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

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

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

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

Чтобы использовать сопрограммы Kotlin для создания ControlsProviderService , добавьте новую зависимость в ваш build.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. Элементы управления устройством в системном меню.

Нажатие на Device controls перенаправляет на второй экран, где вы можете выбрать свое приложение. После выбора приложения вы увидите, как предыдущий фрагмент создает пользовательское системное меню, показывающее ваши новые элементы управления, как показано на рисунке 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() использует сопрограммы и потоки Kotlin для удовлетворения требуемого API Reactive Streams, выполняя следующие действия:

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

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

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

Чтобы завершить пример, добавьте следующее в вашу 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. Управление освещением и термостатом.