Controlar dispositivos externos

No Android 11 e versões mais recentes, o recurso de controles de acesso rápido do dispositivo permite que os usuários visualizem e controlem rapidamente dispositivos externos, como luzes, termostatos e câmeras, usando uma funcionalidade do usuário em até três interações de um inicializador padrão. O OEM do dispositivo escolhe qual inicializador usar. Agregadores de dispositivos, como o Google Home, e apps de fornecedores terceirizados podem fornecer dispositivos para exibição nesse espaço. Nesta página, mostramos como exibir controles de dispositivos nesse espaço e vinculá-los ao seu app de controle.

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

Para adicionar esse suporte, crie e declare um ControlsProviderService. Crie os controles que seu app aceita com base em tipos de controle predefinidos e, em seguida, 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 estão disponíveis, como mostrado na figura a seguir:

Ativar/desativar o widget para controles do dispositivo
Alternar
Alternar com o widget de controle deslizante
Alternar com o controle deslizante
Widget de controle deslizante de intervalo para controles do dispositivo
Intervalo (não pode ser ativado ou desativado)
Widget de controle 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 mantenha um widget pressionado para acessar o app e ter mais controle. É possível personalizar o ícone e a cor em cada widget, mas, para uma melhor experiência do usuário, use o ícone e a cor padrão se o conjunto padrão corresponder ao dispositivo.

Widget do painel de temperatura no estado aberto
Figura 3. Abra o widget do painel de temperatura.

Criar o serviço

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

A API ControlsProviderService pressupõe familiaridade com streams reativos, conforme definido no projeto do GitHub de streams reativos (link em inglês) e implementado nas interfaces de fluxo do Java 9 (link em inglês). A API se baseia nos seguintes conceitos:

  • Publisher:seu aplicativo é o publisher.
  • Assinante:a interface do sistema é o assinante e pode solicitar vários controles do editor.
  • Inscrição:o período em que o editor pode enviar atualizações para a IU do sistema. O editor ou o assinante pode fechar essa janela.

Declarar o serviço

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

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

Você também precisa de um label que é exibido nos controles na IU 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 estenda ControlsProviderService:

class MyCustomControlService : 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 compatíveis. O tipo é usado para determinar os ícones e as cores do dispositivo na interface.
  2. Determine o nome voltado ao usuário, o local do dispositivo (por exemplo, cozinha) e outros elementos do texto da interface 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 ao 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 que eles aceitam:
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ínimo, máximo e de etapa especificados. Quando o usuário interagir 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 é compatível com eventos de toque e um controle deslizante, como para controlar luzes reguláveis.
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction Além de encapsular as ações anteriores, esse modelo permite que o usuário defina um modo, como aquecer, resfriar, aquecer/resfriar, Eco ou desativado.
StatelessTemplate CommandAction Usado para indicar um controle que oferece capacidade de toque, mas cujo estado não pode ser determinado, como um controle remoto de TV por 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, é possível criar o controle:

Por exemplo, para controlar uma lâmpada inteligente e um termostato, adicione as seguintes constantes ao 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() {
    // ...
}

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 app. Use Control.StatelessBuilder para criar objetos Control para esse editor.
  • createPublisherFor: cria um Publisher para uma lista de controles, identificados pelos identificadores de string. Use Control.StatefulBuilder para criar esses objetos Control, porque o publisher precisa atribuir um estado a cada controle.

Criar o editor

Quando o app publica controles na interface do sistema pela primeira vez, ele não sabe o estado de cada controle. A obtenção do estado pode ser uma operação demorada, envolvendo muitos saltos na rede do provedor do dispositivo. Use o método createPublisherForAllAvailable para anunciar os controles disponíveis para o sistema. Esse método usa a classe builder Control.StatelessBuilder, porque o estado de cada controle é desconhecido.

Depois que os controles aparecerem na interface do Android, os usuários poderão selecionar os controles favoritos.

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 snippet abaixo ao Service para implementar o 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()
    }
}

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

Interface do usuário do sistema para controles do dispositivo
Figura 4. Controles de dispositivos no menu do sistema.

Toque em Controles do dispositivo para acessar uma segunda tela em que você pode selecionar seu app. Depois de selecionar o app, você vai ver como o snippet anterior cria um menu personalizado do sistema que mostra os novos controles, como mostrado na Figura 5:

Menu do sistema mostrando um controle de luz e termostato
Figura 5. Controles de luz e termostato para adicionar.

Agora, implemente o método createPublisherFor, adicionando o seguinte ao 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()
}

Neste exemplo, o método createPublisherFor contém uma implementação falsa do que o app precisa fazer: se comunicar com o dispositivo para recuperar o status dele e emitir esse status 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 uma Flow.
  2. Aguarda um segundo.
  3. Cria e emite o estado da iluminação inteligente.
  4. Aguarda 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. Realize a ação adequada para o controle fornecido e atualize o estado do dispositivo na IU do Android.

Para concluir o exemplo, adicione o seguinte ao seu 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)
}

Execute o app, acesse o menu Controles do dispositivo e confira os controles de luz e termostato.

Controles mostrando uma luz e um termostato
Figura 6. Controles de luz e termostato.