Controlar dispositivos externos

O recurso controles de dispositivo de acesso rápido, disponível a partir do Android 11, permite que o usuário visualize e controle rapidamente dispositivos externos, como luzes, termostatos e câmeras, no menu liga/desliga do Android. Agregadores de dispositivos (por exemplo, Google Home) e apps de fornecedores terceirizados podem oferecer dispositivos para exibição nesse espaço. Este guia mostra como adicionar compatibilidade para controles de dispositivos ao seu app de controle.

Espaço de controle do dispositivo na IU do Android

Para adicionar esse suporte, crie e declare um ControlsProviderService, crie os controles compatíveis com seu app com base em tipos de controle predefinidos e crie editores para esses controles.

Interface do usuário

Os dispositivos são exibidos em Controles do dispositivo como widgets com modelo. Cinco widgets de controle de dispositivos diferentes estão disponíveis:

Alternar widget
Alternar
Alternar com o widget de controle deslizante
Alternar com controle deslizante
Widget de intervalo
Intervalo (não pode ser ativado ou desativado)
Widget de alternância sem estado
Alternância sem estado
Widget do painel de temperatura (fechado)
Painel de temperatura (fechado)
Widget do painel de temperatura (aberto)
Painel de temperatura (aberto)

Manter um widget pressionado leva você ao app para ter um controle mais profundo. Você pode personalizar o ícone e a cor em cada widget, mas, para ter a melhor experiência do usuário, use o ícone e a cor padrão, a menos que o conjunto padrão não corresponda ao dispositivo.

Criar o serviço

Esta seção mostra como criar o ControlsProviderService. Esse serviço informa à IU do sistema Android que seu app contém controles de dispositivo que precisam ser exibidos na área Controles do dispositivo da IU do Android.

A API ControlsProviderService pressupõe familiaridade com streams reativos, conforme definido no projeto GitHub de streams reativos e implementado nas interfaces de fluxo do Java 9. A API foi criada com base nestes conceitos:

  • Editor: seu aplicativo é o editor
  • Assinante: a IU do sistema é o assinante e pode solicitar vários controles do editor
  • Assinatura: uma janela de tempo em que o editor pode enviar atualizações para a IU do sistema. Esta janela pode ser fechada pelo editor ou pelo assinante

Declarar o serviço

Seu app precisa declarar um serviço no manifesto do app. Inclua a permissão BIND_CONTROLS.

O serviço precisa incluir um filtro de intent para ControlsProviderService. Esse filtro permite que os apps contribuam com controles para a IU do sistema.

<!-- New signature permission to ensure only systemui can bind to these services -->
<service android:name="YOUR-SERVICE-NAME" android:label="YOUR-SERVICE-LABEL"
    android:permission="android.permission.BIND_CONTROLS">
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

Selecione o tipo de controle correto

A API fornece métodos do builder para criar os controles. Para preencher o builder, você precisa determinar o dispositivo que quer controlar e como o usuário precisa interagir com ele. Em particular, você precisa fazer o seguinte:

  1. Escolha o tipo de dispositivo que o controle representa. A classe DeviceTypes é uma enumeração de todos os dispositivos compatíveis. O tipo é usado para determinar os ícones e as cores do dispositivo na IU.
  2. Determine o nome do usuário, a localização do dispositivo (por exemplo, cozinha) e outros elementos textuais da IU associados ao controle.
  3. Escolha o melhor modelo para oferecer suporte à interação do usuário. Os controles recebem um ControlTemplate do aplicativo. Esse modelo mostra diretamente o estado de controle para o usuário, bem como os métodos de entrada disponíveis (ou seja, o ControlAction). A tabela a seguir descreve alguns dos modelos disponíveis e as ações compatíveis:
Modelo Ação Descrição
ControlTemplate.getNoTemplateObject() None O aplicativo pode usar isso para transmitir informações sobre o controle, mas o usuário não pode interagir com ele.
ToggleTemplate BooleanAction Representa um controle que pode ser alternado entre estados ativados e desativados. O objeto BooleanAction contém um campo que muda para representar o novo estado solicitado quando o usuário toca no controle.
RangeTemplate FloatAction Representa um widget de controle deslizante com os valores mínimo, máximo e de etapa especificados. Quando o usuário interage com o controle deslizante, um novo objeto FloatAction precisa ser enviado de volta ao aplicativo com o valor atualizado.
ToggleRangeTemplate BooleanAction, FloatAction Esse modelo é uma combinação de ToggleTemplate e RangeTemplate. Ele é compatível com eventos de toque, bem como um controle deslizante, por exemplo, em um controle de luzes reguláveis.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Além de encapsular uma das ações acima, esse modelo permite que o usuário defina um modo, como calor, frio, calor/frio, ecológico ou desativado.
StatelessTemplate CommandAction Usado para indicar um controle que fornece a capacidade de toque, mas cujo estado não pode ser determinado, como um controle remoto de televisão de infravermelho. Você pode usar esse modelo para definir uma rotina ou macro, que é uma agregação de mudanças de controle e estado.

Com essas informações, agora você pode criar o controle:

Criar editores para os controles

Depois que o controle for criado, ele precisa de um editor. O editor informa a IU do sistema sobre a existência do controle. A classe ControlsProviderService tem dois métodos de editor que você precisa modificar no código do aplicativo:

  • createPublisherForAllAvailable(): cria um Publisher para todos os controles disponíveis no seu app. Use Control.StatelessBuilder() para criar Controls para este editor.
  • createPublisherFor(): cria um Publisher para uma lista de determinados controles, conforme identificado pelos identificadores de string. Use Control.StatefulBuilder para criar esses Controls, já que o editor precisa atribuir um estado a cada controle.

Criar o editor

Quando o app publica controles pela primeira vez na IU do sistema, o app desconhece o estado de cada controle. Reconhecer o estado pode ser uma operação demorada que envolve muitos saltos na rede do provedor de dispositivos. Use o método createPublisherForAllAvailable() para anunciar os controles disponíveis para o sistema. Observe que esse método usa a classe de builder Control.StatelessBuilder, porque o estado de cada controle é desconhecido.

Quando os controles aparecerem na IU do Android, o usuário poderá selecionar controles (ou seja, escolher favoritos) de interesse.

Kotlin

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
class MyCustomControlService : ControlsProviderService() {

    override fun createPublisherForAllAvailable(): Flow.Publisher {
        val context: Context = baseContext
        val i = Intent()
        val pi =
            PendingIntent.getActivity(
                context, CONTROL_REQUEST_CODE, i,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        val controls = mutableListOf()
        val control =
            Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
                // Required: The name of the control
                .setTitle(MY-CONTROL-TITLE)
                // Required: Usually the room where the control is located
                .setSubtitle(MY-CONTROL-SUBTITLE)
                // Optional: Structure where the control is located, an example would be a house
                .setStructure(MY-CONTROL-STRUCTURE)
                // Required: Type of device, i.e., thermostat, light, switch
                .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                .build()
        controls.add(control)
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls))
    }
}

Java

/* If you choose to use Reactive Streams API, you will need to put the following
 * into your module's build.gradle file:
 * implementation 'org.reactivestreams:reactive-streams:1.0.3'
 * implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
 */
public class MyCustomControlService extends ControlsProviderService {

    @Override
    public Publisher createPublisherForAllAvailable() {
        Context context = getBaseContext();
        Intent i = new Intent();
        PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);
        List controls = new ArrayList<>();
        Control control = Control.StatelessBuilder(MY-UNIQUE-DEVICE-ID, pi)
          // Required: The name of the control
          .setTitle(MY-CONTROL-TITLE)
          // Required: Usually the room where the control is located
          .setSubtitle(MY-CONTROL-SUBTITLE)
          // Optional: Structure where the control is located, an example would be a house
          .setStructure(MY-CONTROL-STRUCTURE)
          // Required: Type of device, i.e., thermostat, light, switch
          .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
          .build();
        controls.add(control);
        // Create more controls here if needed and add it to the ArrayList

        // Uses the RxJava 2 library
        return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
    }
}

Depois que o usuário selecionar um conjunto de controles, crie um editor apenas para esses controles. Use o método createPublisherFor(), já que esse método usa a classe de builder Control.StatefulBuilder, que fornece o estado atual de cada controle (por exemplo, ativado ou desativado).

Kotlin

class MyCustomControlService : ControlsProviderService() {
    private lateinit var updatePublisher: ReplayProcessor

    override fun createPublisherFor(controlIds: MutableList): Flow.Publisher {
        val context: Context = baseContext
        /* Fill in details for the activity related to this device. On long press,
         * this Intent will be launched in a bottomsheet. Please design the activity
         * accordingly to fit a more limited space (about 2/3 screen height).
         */
        val i = Intent(this, CustomSettingsActivity::class.java)
        val pi =
            PendingIntent.getActivity(context, CONTROL_REQUEST_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT)
        updatePublisher = ReplayProcessor.create()

        if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {
            val control =
                Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
                    // Required: The name of the control
                    .setTitle(MY-CONTROL-TITLE)
                    // Required: Usually the room where the control is located
                    .setSubtitle(MY -CONTROL-SUBTITLE)
                    // Optional: Structure where the control is located, an example would be a house
                    .setStructure(MY-CONTROL-STRUCTURE)
                    // Required: Type of device, i.e., thermostat, light, switch
                    .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
                    // Required: Current status of the device
                    .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
                    .build()

            updatePublisher.onNext(control)
        }

        // If you have other controls, check that they have been selected here

        // Uses the Reactive Streams API
        updatePublisher.onNext(control)
    }
}

Java

private ReplayProcessor updatePublisher;

@Override
public Publisher createPublisherFor(List controlIds) {

    Context context = getBaseContext();
    /* Fill in details for the activity related to this device. On long press,
     * this Intent will be launched in a bottomsheet. Please design the activity
     * accordingly to fit a more limited space (about 2/3 screen height).
     */
    Intent i = new Intent();
    PendingIntent pi = PendingIntent.getActivity(context, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);

    updatePublisher = ReplayProcessor.create();

    // For each controlId in controlIds

    if (controlIds.contains(MY-UNIQUE-DEVICE-ID)) {

      Control control = Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
        // Required: The name of the control
        .setTitle(MY-CONTROL-TITLE)
        // Required: Usually the room where the control is located
        .setSubtitle(MY-CONTROL-SUBTITLE)
        // Optional: Structure where the control is located, an example would be a house
        .setStructure(MY-CONTROL-STRUCTURE)
        // Required: Type of device, i.e., thermostat, light, switch
        .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
        // Required: Current status of the device
        .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
        .build();

      updatePublisher.onNext(control);
    }
    // Uses the Reactive Streams API
    return FlowAdapters.toFlowPublisher(updatePublisher);
}

Processar ações

O método performControlAction() indica que o usuário interagiu com um controle publicado. A ação é ditada pelo tipo de ControlAction enviado. Execute a ação apropriada para o controle fornecido e atualize o estado do dispositivo na IU do Android.

Kotlin

class MyCustomControlService : ControlsProviderService() {

    override fun performControlAction(
        controlId: String, action: ControlAction, consumer: Consumer) {

        /* First, locate the control identified by the controlId. Once it is located, you can
         * interpret the action appropriately for that specific device. For instance, the following
         * assumes that the controlId is associated with a light, and the light can be turned on
         * or off.
         */
        if (action is BooleanAction) {

            // Inform SystemUI that the action has been received and is being processed
            consumer.accept(ControlAction.RESPONSE_OK)

            // In this example, action.getNewState() will have the requested action: true for “On”,
            // false for “Off”.

            /* This is where application logic/network requests would be invoked to update the state of
             * the device.
             * After updating, the application should use the publisher to update SystemUI with the new
             * state.
             */
            Control control = Control.StatefulBuilder (MY-UNIQUE-DEVICE-ID, pi)
            // Required: The name of the control
              .setTitle(MY-CONTROL-TITLE)
              // Required: Usually the room where the control is located
              .setSubtitle(MY-CONTROL-SUBTITLE)
              // Optional: Structure where the control is located, an example would be a house
              .setStructure(MY-CONTROL-STRUCTURE)
              // Required: Type of device, i.e., thermostat, light, switch
              .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
              // Required: Current status of the device
              .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
              .build()

            // This is the publisher the application created during the call to createPublisherFor()
            updatePublisher.onNext(control)
        }
    }
}

Java

@Override
public void performControlAction(String controlId, ControlAction action,
    Consumer consumer) {

  /* First, locate the control identified by the controlId. Once it is located, you can
   * interpret the action appropriately for that specific device. For instance, the following
   * assumes that the controlId is associated with a light, and the light can be turned on
   * or off.
   */
  if (action instanceof BooleanAction) {

    // Inform SystemUI that the action has been received and is being processed
    consumer.accept(ControlAction.RESPONSE_OK);

    BooleanAction action = (BooleanAction) action;
    // In this example, action.getNewState() will have the requested action: true for “On”,
    // false for “Off”.

    /* This is where application logic/network requests would be invoked to update the state of
     * the device.
     * After updating, the application should use the publisher to update SystemUI with the new
     * state.
     */
    Control control = Control.StatefulBuilder(MY-UNIQUE-DEVICE-ID, pi)
      // Required: The name of the control
      .setTitle(MY-CONTROL-TITLE)
      // Required: Usually the room where the control is located
      .setSubtitle(MY-CONTROL-SUBTITLE)
      // Optional: Structure where the control is located, an example would be a house
      .setStructure(MY-CONTROL-STRUCTURE)
      // Required: Type of device, i.e., thermostat, light, switch
      .setDeviceType(DeviceTypes.DEVICE-TYPE) // For example, DeviceTypes.TYPE_THERMOSTAT
      // Required: Current status of the device
      .setStatus(Control.CURRENT-STATUS) // For example, Control.STATUS_OK
      .build();

    // This is the publisher the application created during the call to createPublisherFor()
    updatePublisher.onNext(control);
  }
}