Controlar dispositivos externos

No Android 11 e versões mais recentes, o recurso Controles de acesso rápido do dispositivo permite que o usuário visualize e controle rapidamente dispositivos externos, como luzes, termostatos e câmeras de uma affordance do usuário em três interações de um tela de início padrão. O OEM do dispositivo escolhe qual tela de início será usada. Dispositivo agregadores, como o Google Home, e apps de fornecedores terceirizados podem fornecem dispositivos para exibição neste espaço. Nesta página, mostramos como exibir controles de dispositivo neste espaço e vincule-os ao seu app de controle.

Figura 1. Espaço de controle do dispositivo na IU do Android.

Para adicionar esse suporte, crie e declare um ControlsProviderService. Crie o aos quais seu app oferece suporte com base em tipos de controles predefinidos e, em seguida, criar 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 do dispositivo estão disponíveis, conforme mostrado na figura abaixo:

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)
Figura 2. Coleção de widgets com modelos.

Toque e segurar um widget leva você ao aplicativo para ter um controle mais profundo. Você pode personalizar o ícone e a cor de cada widget, mas para a melhor experiência do usuário, use o ícone e a cor padrão se o conjunto padrão corresponder ao dispositivo.

Uma imagem mostrando o widget do painel de temperatura (aberto)
Figura 3. Abrir o widget do painel de temperatura.

Criar o serviço

Nesta seção, mostramos como criar a ControlsProviderService Esse serviço informa à interface do sistema Android que o app contém controles do dispositivo que precisam ser exibidos na área Controles do dispositivo da interface do Android.

A API ControlsProviderService pressupõe familiaridade com fluxos reativos, já que definido no GitHub sobre Reactive Streams projeto e implementadas no fluxo do Java 9 do Terraform. A API foi criada com base nestes conceitos:

  • Editor:seu aplicativo é o editor.
  • Assinante:a interface do sistema é o assinante e pode solicitar vários de controles do editor.
  • Assinatura:é o período em que o editor pode enviar atualizações. para a interface do sistema. O editor ou o assinante podem encerrar esta janela.

Declarar o serviço

O app precisa declarar um serviço, como MyCustomControlService, em o manifesto do app.

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.

Você também precisa de um label, que é mostrado nos controles da interface do sistema.

O exemplo a seguir mostra como declarar um serviço:

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

Em seguida, crie um novo arquivo Kotlin chamado MyCustomControlService.kt e faça com que ele se estenda a ControlsProviderService():

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

Selecione o tipo de controle correto

A API fornece métodos do builder para criar os controles. Para preencher o builder, determine o dispositivo que você quer controlar e como o usuário interage com ele. Siga estas etapas:

  1. Escolha o tipo de dispositivo que o controle representa. A classe DeviceTypes é uma enumeração de todos os dispositivos com suporte. O tipo é usado para determinar ícones e cores do dispositivo na interface.
  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 com suporte:
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 valores mínimos, máximos e de etapa especificados. Quando o usuário interage com o controle deslizante, envie um novo objeto FloatAction de volta ao aplicativo com o valor atualizado.
ToggleRangeTemplate BooleanAction, FloatAction Esse modelo é uma combinação de ToggleTemplate e RangeTemplate. Ele suporta eventos de toque, bem como um controle deslizante, por exemplo, para controlar luzes reguláveis.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Além de encapsular as ações anteriores, este modelo permite o usuário define um modo, como Aquecer, Resfriar, Aquecer/Resfriar, Eco ou Desligado.
StatelessTemplate CommandAction Usado para indicar um controle que fornece 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, você pode criar o controle:

Por exemplo, para controlar uma lâmpada inteligente e um termostato, adicione as seguintes constantes ao MyCustomControlService:

Kotlin

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

Java

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

Criar editores para os controles

Depois de criar o controle, 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. Usar Control.StatelessBuilder() para criar objetos Control 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 objetos Control, 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, ele não sabe 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. Esse método usa a classe de builder Control.StatelessBuilder, já que o estado de cada controle é desconhecido.

Quando os controles aparecem na interface do Android , o usuário pode selecionar um favorito controles de segurança.

Para usar corrotinas do Kotlin para criar um ControlsProviderService, adicione uma nova dependência ao build.gradle:

Groovy

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

Kotlin

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

Depois de sincronizar os arquivos do Gradle, adicione o seguinte snippet ao Service para implementar createPublisherForAllAvailable():

Kotlin

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

Java

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

Deslize o menu do sistema para baixo e localize o botão Controles do dispositivo, mostrado na Figura 4:

Uma imagem mostrando a interface do sistema para controles do dispositivo
Figura 4. Controles do dispositivo no menu do sistema.

Tocar em Controles do dispositivo leva a uma segunda tela, em que você pode selecionar seu app. Depois de selecionar seu aplicativo, você verá como o snippet anterior cria uma menu personalizado do sistema mostrando os novos controles, conforme mostrado na Figura 5:

Uma imagem mostrando o menu do sistema com um controle de luz e termostato
Figura 5. Controles de luzes e termostatos para adicionar.

Agora, implemente o método createPublisherFor(), adicionando o seguinte à Service:

Kotlin

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

Java

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

Neste exemplo, o método createPublisherFor() contém uma implementação do que o aplicativo deve fazer: comunicar-se com o dispositivo para recuperam o status e o emitem para o sistema.

O método createPublisherFor() usa corrotinas e fluxos do Kotlin para atender à API Reactive Streams necessária fazendo o seguinte:

  1. Cria um Flow.
  2. Aguarda um segundo.
  3. Cria e emite o estado da luz inteligente.
  4. Espera mais um segundo.
  5. Cria e emite o estado do termostato.

Processar ações

O método performControlAction() sinaliza quando o usuário interage com um controle publicado. O tipo de ControlAction enviado determina a ação. Executar a ação apropriada para o controle especificado e atualizar o estado do dispositivo na interface do Android.

Para concluir o exemplo, adicione o seguinte ao seu Service:

Kotlin

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

Java

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

Execute o app, acesse o menu Device controls e confira os controles de luz e termostato.

Imagem mostrando um controle de luz e termostato
Figura 6. Controles de luzes e termostatos.