Executar comandos

Esta página descreve como atender comandos com interação por voz.

Atender comandos de mídia

Os comandos relacionados a mídia podem ser divididos em três grupos diferentes:

  • Fontes de mídia externas (como o Spotify instalado no AAOS).
  • Fontes de mídia de back-end (como músicas transmitidas pelo VIA).
  • Fontes de mídia locais (como rádio do carro).

Processar comandos de fonte de mídia externa

As fontes de mídia externas são definidas como apps Android que oferecem suporte às APIs MediaSessionCompat e MediaBrowseCompat. Consulte Criar apps de mídia para carros para uma explicação detalhada sobre o uso dessas APIs.

Importante:para que um app assistente se conecte ao MediaBrowseService de todos os apps de mídia instalados no sistema, ele precisa:

  1. Ser instalado como assinado pelo sistema (consulte as diretrizes de desenvolvimento de aplicativos de mídia para AAOS e o exemplo de código PackageValidator).
  2. Mantenha a permissão privilegiada do sistema android.permission.MEDIA_CONTENT_CONTROL (consulte Conceder permissões privilegiadas do sistema).

Além de MediaBrowserCompat e MediaControllerCompat, a AAOS oferece o seguinte:

  • CarMediaService fornece informações centralizadas sobre a fonte de mídia selecionada. Ele também é usado para retomar uma fonte de mídia que estava sendo reproduzida após a reinicialização do carro.
  • O car-media-common oferece métodos convenientes para listar, conectar e interagir com apps de mídia.

Confira abaixo as diretrizes específicas para a implementação de comandos de interação por voz comuns.

Receber uma lista de fontes de mídia instaladas

As origens de mídia podem ser detectadas usando PackageManager e filtrando os serviços que correspondem a MediaBrowserService.SERVICE_INTERFACE. Em alguns carros, pode haver algumas implementações especiais do serviço de navegador de mídia, que precisam ser excluídas. Confira um exemplo dessa lógica:

private Map<String, MediaSource> getAvailableMediaSources() {
    List<String> customMediaServices =
        Arrays.asList(mContext.getResources()
            .getStringArray(R.array.custom_media_packages));
    List<ResolveInfo> mediaServices = mPackageManager.queryIntentServices(
            new Intent(MediaBrowserService.SERVICE_INTERFACE),
            PackageManager.GET_RESOLVED_FILTER);
    Map<String, MediaSource> result = new HashMap<>();
    for (ResolveInfo info : mediaServices) {
        String packageName = info.serviceInfo.packageName;
        if (customMediaServices.contains(packageName)) {
            // Custom media sources should be ignored, as they might have a
            // specialized handling (e.g., radio).
            continue;
        }
        String className = info.serviceInfo.name;
        ComponentName componentName = new ComponentName(packageName,
            className);
        MediaSource source = MediaSource.create(mContext, componentName);
        result.put(source.getDisplayName().toString().toLowerCase(),
            source);
    }
    return result;
}

As fontes de mídia podem ser instaladas ou desinstaladas a qualquer momento. Para manter uma lista precisa, é recomendável implementar uma instância BroadcastReceiver para as ações de intent ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REPLACED e ACTION_PACKAGE_REMOVED.

Conectar à fonte de mídia em reprodução

CarMediaService fornece métodos para receber a fonte de mídia selecionada no momento e quando ela muda. Essas mudanças podem acontecer porque o usuário interagiu diretamente com a interface ou devido ao uso de botões físicos no carro. Por outro lado, a biblioteca car-media-common oferece maneiras convenientes de se conectar a uma determinada fonte de mídia. Confira um snippet simplificado sobre como se conectar ao app de mídia selecionado:

public class MediaActuator implements
        MediaBrowserConnector.onConnectedBrowserChanged {
    private final Car mCar;
    private CarMediaManager mCarMediaManager;
    private MediaBrowserConnector mBrowserConnector;

    

    public void initialize(Context context) {
        mCar = Car.createCar(context);
        mBrowserConnector = new MediaBrowserConnector(context, this);
        mCarMediaManager = (CarMediaManager)
            mCar.getCarManager(Car.CAR_MEDIA_SERVICE);
        mBrowserConnector.connectTo(mCarMediaManager.getMediaSource());
        
    }

    @Override
    public void onConnectedBrowserChanged(
            @Nullable MediaBrowserCompat browser) {
        // TODO: Handle connected/disconnected browser
    }

    
}

Controlar a reprodução da fonte de mídia atual

Com um MediaBrowserCompat conectado, é fácil enviar comandos de controle de transporte para o app de destino. Confira um exemplo simplificado:

public class MediaActuator   {
    
    private MediaControllerCompat mMediaController;

    @Override
    public void onConnectedBrowserChanged(
            @Nullable MediaBrowserCompat browser) {
        if (browser != null && browser.isConnected()) {
            mMediaController = new MediaControllerCompat(mContext,
                browser.getSessionToken());
        } else {
            mMediaController = null;
        }
    }

    private boolean playSongOnCurrentSource(String song) {
        if (mMediaController == null) {
            // No source selected.
            return false;
        }
        MediaControllerCompat.TransportControls controls =
            mMediaController.getTransportControls();
        PlaybackStateCompat state = controller.getPlaybackState();
        if (state == null || ((state.getActions() &
                PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH) == 0)) {
            // Source can't play from search
            return false;
        }
        controls.playFromSearch(query, null);
        return true;
    }

    
}

Processar comandos de fonte de mídia local (rádio, CD player, Bluetooth, USB)

As fontes de mídia locais expõem a funcionalidade ao sistema usando as mesmas APIs MediaSession e MediaBrowse detalhadas acima. Para acomodar as particularidades de cada tipo de hardware, esses serviços do MediaBrowse usam convenções específicas para organizar informações e comandos de mídia.

Processar rádio

O MediaBrowseService de rádio pode ser identificado pelo filtro de intent ACTION_PLAY_BROADCASTRADIO. Eles precisam seguir os controles de reprodução e a estrutura de navegação de mídia descrita em Implementar rádio. O AAOS oferece a biblioteca car-broadcastradio-support que contém constantes e métodos para ajudar os OEMs a criar implementações do MediaBrowseService para os próprios serviços de rádio que seguem o protocolo definido e oferece suporte a apps que consomem a árvore de navegação (por exemplo, VIAs).

Processar entrada auxiliar, áudio de CD e mídia USB

Não há implementação padrão dessas fontes de mídia como parte do AOSP. A abordagem sugerida é:

Processar Bluetooth

O conteúdo de mídia Bluetooth é exposto pelo perfil Bluetooth AVRCP. Para facilitar o acesso a essa funcionalidade, o AAOS inclui uma implementação do MediaBrowserService e do MediaSession que abstrai os detalhes de comunicação (consulte packages/apps/Bluetooth).

A respectiva estrutura de árvore do media player é definida na classe BrowseTree. Os comandos de controle de reprodução podem ser enviados de forma semelhante a qualquer outro app, usando a implementação da MediaSession.

Processar comandos de streaming de mídia

Para implementar o streaming de mídia do lado do servidor, o VIA precisa se tornar uma fonte de mídia, implementando a API MediaBrowse e a API MediaSession. Consulte Criar apps de mídia para carros. Ao implementar essas APIs, um app de controle de voz pode (entre outras coisas):

  • Participar da seleção da fonte de mídia
  • Ser retomado automaticamente após a reinicialização do carro
  • Fornecer controle de reprodução e navegação usando a interface do Media Center
  • Receber eventos de botão de mídia padrão de hardware

Não há uma maneira padronizada de interagir com todos os apps de navegação. Para integrações com o Google Maps, consulte Google Maps para intents do Android Automotive. Para integrações com outros apps, entre em contato diretamente com os desenvolvedores. Antes de iniciar uma intent para qualquer app (incluindo o Google Maps), verifique se ela pode ser resolvida (consulte Solicitações de intent). Isso cria a oportunidade de informar o usuário caso o app de destino não esteja disponível.

Atender comandos do veículo

O acesso às propriedades do veículo para leitura e gravação é fornecido pelo CarPropertyManager. Os tipos de propriedades do veículo, a implementação e outros detalhes são explicados nas Configurações de propriedade. Para uma descrição precisa das propriedades aceitas pelo Android, consulte diretamente hardware/interfaces/automotive/vehicle/2.0/types.hal. O tipo enumerado VehicleProperty definido aqui contém propriedades padrão e específicas do fornecedor, tipos de dados, modo de mudança, unidades e definição de acesso de leitura/gravação.

Para acessar essas mesmas constantes em Java, use VehiclePropertyIds e as classes complementares. Propriedades diferentes têm diferentes permissões do Android que controlam o acesso. Essas permissões são declaradas no manifesto do CarService, e o mapeamento entre propriedades e permissões é descrito no Javadoc VehiclePropertyIds e aplicado em PropertyHalServiceIds.

Ler uma propriedade do veículo

Confira a seguir um exemplo de como ler a velocidade do veículo:

public class CarActuator ... {
    private final Car mCar;
    private final CarPropertyManager mCarPropertyManager;
    private final TextToSpeech mTTS;

    /** Global VHAL area id */
    public static final int GLOBAL_AREA_ID = 0;

    public CarActuator(Context context, TextToSpeech tts) {
        mCar = Car.createCar(context);
        mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
        mTTS = tts;
        ...
    }

    @Nullable
    private void getSpeedInMetersPerSecond() {
        if (!mCarPropertyManager.isPropertyAvailable(VehiclePropertyIds.PERF_VEHICLE_SPEED,
                GLOBAL_AREA_ID)) {
            mTTS.speak("I'm sorry, but I can't read the speed of this vehicle");
            return;
        }
        // Data type and unit can be found in
        // automotive/vehicle/2.0/types.hal
        float speedInMps = mCarPropertyManager.getFloatProperty(
                VehiclePropertyIds.PERF_VEHICLE_SPEED, GLOBAL_AREA_ID);
        int speedInMph = (int)(speedInMetersPerSecond * 2.23694f);
        mTTS.speak(String.format("Sure. Your current speed is %d miles "
                + "per hour", speedInUserUnit);
    }

    ...
}

Definir uma propriedade do veículo

Confira a seguir um exemplo de como ligar e desligar o AC frontal.

public class CarActuator … {
    …

    private void changeFrontAC(boolean turnOn) {
        List<CarPropertyConfig> configs = mCarPropertyManager
                .getPropertyList(new ArraySet<>(Arrays.asList(
                    VehiclePropertyIds.HVAC_AC_ON)));
        if (configs == null || configs.size() != 1) {
            mTTS.speak("I'm sorry, but I can't control the AC of your vehicle");
            return;
        }

        // Find the front area Ids for the AC property.
        int[] areaIds = configs.get(0).getAreaIds();
        List<Integer> areasToChange = new ArrayList<>();
        for (int areaId : areaIds) {
            if ((areaId & (VehicleAreaSeat.SEAT_ROW_1_CENTER
                        | VehicleAreaSeat.SEAT_ROW_1_LEFT
                        | VehicleAreaSeat.SEAT_ROW_1_RIGHT)) == 0) {
                continue;
            }
            boolean isACInAreaAlreadyOn = mCarPropertyManager
                    .getBooleanProperty(VehiclePropertyIds.HVAC_AC_ON, areaId);
            if ((!isACInAreaAlreadyOn && turnOn) || (isACInAreaAlreadyOn && !turnOn)) {
                areasToChange.add(areaId);
            }
        }
        if (areasToChange.isEmpty()) {
            mTTS.speak(String.format("The AC is already %s", turnOn ? "on" : "off"));
            return;
        }

        for (int areaId : areasToChange) {
            mCarPropertyManager.setBooleanProperty(
                VehiclePropertyIds.HVAC_AC_ON, areaId, turnOn);
        }
        mTTS.speak(String.format("Okay, I'm turning your front AC %s",
            turnOn ? "on" : "off"));
    }

    …
}

Executar comandos de comunicação

Processar comandos de mensagens

Os VIAs precisam processar as mensagens recebidas seguindo o fluxo "toque para ler" descrito em Toque para ler do Google Assistente de voz, que pode, opcionalmente, processar o envio de respostas de volta ao remetente da mensagem recebida. Além disso, as VIAs podem usar SmsManager (parte do pacote android.telephony) para compor e enviar mensagens SMS diretamente do carro ou por Bluetooth.

Processar comandos de chamada

Da mesma forma, as VIAs podem usar TelephonyManager para fazer ligações e chamar o número de correio de voz do usuário. Nesses casos, as VIAs vão interagir com a pilha de telefonia diretamente ou com o app de discagem no carro. De qualquer forma, o app de discagem no carro precisa ser o que mostra a interface relacionada a chamadas de voz para o usuário.

Atender outros comandos

Para conferir uma lista de outros possíveis pontos de integração entre o VIA e o sistema, consulte a lista de intents do Android conhecidos. Muitos comandos do usuário podem ser resolvidos no servidor (por exemplo, leitura de e-mails e eventos da agenda dos usuários) e não exigem nenhuma interação com o sistema além da interação por voz.

Ações imersivas (exibir conteúdo visual)

Quando melhora as ações ou o entendimento do usuário, um VIA pode fornecer conteúdo visual complementar na tela do carro. Para minimizar a distração do motorista, mantenha esse conteúdo simples, breve e útil. Para detalhes sobre as diretrizes de UI/UX em ações imersivas, consulte Assistentes pré-carregados: orientação de UX.

Para permitir a personalização e a consistência com o restante do design da unidade principal (HU, na sigla em inglês), as VIAs precisam usar componentes da biblioteca de interface do carro (link em inglês) para a maioria dos elementos da interface. Para mais detalhes, consulte Personalização.