Criar um app de navegação

Esta página detalha os diferentes recursos da biblioteca Car App que podem ser usados para implementar a funcionalidade do seu app de navegação guiada.

Declarar suporte à navegação no manifesto

Seu app de navegação precisa declarar a categoria de app para carros androidx.car.app.category.NAVIGATION no filtro de intent do CarAppService:

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
      </intent-filter>
    </service>
    ...
</application>

Suporte a intents de navegação

Para oferecer suporte a intents de navegação no seu app, incluindo as que vêm do Google Assistente com consultas por voz, o app precisa processar a intent CarContext.ACTION_NAVIGATE nos métodos Session.onCreateScreen e Session.onNewIntent.

Acesse a documentação de CarContext.startCarApp para consultar detalhes sobre o formato da intent.

Acessar os modelos de navegação

Os apps de navegação podem acessar os seguintes modelos, que mostram uma plataforma em segundo plano com o mapa e as instruções de navegação guiada (quando ativa).

  • NavigationTemplate: também mostra uma mensagem informativa opcional e estimativas da viagem durante a navegação ativa.
  • MapWithContentTemplate: um modelo que permite que um app renderize blocos de mapa com algum tipo de conteúdo (por exemplo, uma lista). O conteúdo geralmente é renderizado como uma sobreposição sobre os blocos do mapa, com as áreas visíveis e estáveis do mapa se ajustando ao conteúdo.

Para saber mais sobre como projetar a interface do usuário do seu app de navegação usando esses modelos, consulte Apps de navegação.

Para ter acesso aos modelos de navegação, seu app precisa declarar a permissão androidx.car.app.NAVIGATION_TEMPLATES no arquivo AndroidManifest.xml:

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
  ...
</manifest>

É necessária uma permissão adicional para criar mapas.

Migrar para o MapWithContentTemplate

A partir do nível 7 da API Car App, as funções MapTemplate, PlaceListNavigationTemplate e RoutePreviewNavigationTemplate foram descontinuadas. Os modelos descontinuados vão continuar sendo aceitos, mas a migração para o MapWithContentTemplate é altamente recomendada.

A funcionalidade fornecida por esses modelos pode ser implementada usando MapWithContentTemplate. Confira os exemplos nos snippets abaixo:

MapTemplate

Kotlin

// MapTemplate (deprecated)
val template = MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        PaneTemplate.Builder(paneBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build()

Java

// MapTemplate (deprecated)
MapTemplate template = new MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new PaneTemplate.Builder(paneBuilder.build())
        .setHeader(header)
        build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build();

PlaceListNavigationTemplate

Kotlin

// PlaceListNavigationTemplate (deprecated)
val template = PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(itemListBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// PlaceListNavigationTemplate (deprecated)
PlaceListNavigationTemplate template = new PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(itemListBuilder.build())
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

RoutePreviewNavigationTemplate

Kotlin

// RoutePreviewNavigationTemplate (deprecated)
val template = RoutePreviewNavigationTemplate.Builder()
    .setItemList(
        ItemList.Builder()
            .addItem(
                Row.Builder()
                    .setTitle(title)
                    .build())
            .build())
    .setHeader(header)
    .setNavigateAction(
        Action.Builder()
            .setTitle(actionTitle)
            .setOnClickListener { ... }
            .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(
                ItemList.Builder()
                    .addItem(
                        Row.Builder()
                            .setTitle(title)
                            .addAction(
                                Action.Builder()
                                    .setTitle(actionTitle)
                                    .setOnClickListener { ... }
                                    .build())
                            .build())
                    .build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// RoutePreviewNavigationTemplate (deprecated)
RoutePreviewNavigationTemplate template = new RoutePreviewNavigationTemplate.Builder()
    .setItemList(new ItemList.Builder()
        .addItem(new Row.Builder()
            .setTitle(title))
            .build())
        .build())
    .setHeader(header)
    .setNavigateAction(new Action.Builder()
        .setTitle(actionTitle)
        .setOnClickListener(() -> { ... })
        .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(new ItemList.Builder()
            .addItem(new Row.Builder()
                  .setTitle(title))
                  .addAction(new Action.Builder()
                      .setTitle(actionTitle)
                      .setOnClickListener(() -> { ... })
                      .build())
                  .build())
            .build()))
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

Os apps de navegação precisam comunicar outros metadados ao host, que usa as informações para fornecer dados à unidade principal do veículo e evitar que esses apps entrem em conflito com os recursos compartilhados.

Os metadados de navegação são fornecidos pelo serviço de carro NavigationManager, que pode ser acessado em CarContext:

Kotlin

val navigationManager = carContext.getCarService(NavigationManager::class.java)

Java

NavigationManager navigationManager = carContext.getCarService(NavigationManager.class);

Iniciar, encerrar e interromper a navegação

Para que o host gerencie vários apps de navegação, notificações de trajeto e dados de cluster do veículo, ele precisa estar ciente do estado de navegação atual. Quando um usuário iniciar a navegação, chame NavigationManager.navigationStarted. Da mesma forma, quando a navegação terminar, por exemplo, ao chegar no destino ou cancelar a navegação, chame NavigationManager.navigationEnded.

Chame NavigationManager.navigationEnded apenas quando o usuário terminar a navegação. Por exemplo, se você precisar recalcular o trajeto no meio de uma viagem, use Trip.Builder.setLoading(true).

Ocasionalmente, o host precisa que um app interrompa a navegação e chame onStopNavigation em um objeto NavigationManagerCallback fornecido pelo seu app usando o NavigationManager.setNavigationManagerCallback. Para isso, o app precisará parar de emitir informações da próxima curva na exibição do cluster, nas notificações de navegação e na orientação por voz.

Atualizar as informações da viagem

Durante a navegação ativa, chame NavigationManager.updateTrip. As informações fornecidas nessa chamada podem ser usadas pelo cluster do veículo e pelos avisos na tela. Dependendo do veículo específico que está sendo dirigido, nem todas as informações são mostradas para o usuário. Por exemplo, a unidade principal da área de trabalho (DHU, na sigla em inglês) mostra a Step adicionada à Trip, mas não mostra a informação de Destination.

Como desenhar na tela do cluster

Para oferecer uma experiência do usuário mais imersiva, vá além de mostrar metadados básicos na tela do cluster do veículo. A partir do nível 6 da API Car App, os apps de navegação têm a opção de renderizar o conteúdo diretamente na tela do cluster (em veículos com suporte), com as seguintes limitações:

  • A API de tela do cluster não oferece suporte a controles de entrada.
  • Diretriz de qualidade de apps para carros NF-9: a tela do cluster só pode mostrar blocos de mapas. Uma rota de navegação ativa pode ser mostrada nos blocos, se você quiser.
  • A API de tela do cluster só oferece suporte ao NavigationTemplate.
    • Ao contrário das telas principais, as telas do cluster podem não mostrar de maneira consistente todos os elementos da interface NavigationTemplate, como instruções passo a passo, cartões HEC e ações. Os blocos de mapas são o único elemento de interface mostrado de forma consistente.

Declarar suporte a cluster

Para informar ao aplicativo hospedado que seu app oferece suporte à renderização em telas de clusters, adicione um elemento androidx.car.app.category.FEATURE_CLUSTER <category> ao <intent-filter> do CarAppService, conforme mostrado no snippet a seguir:

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
        <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
      </intent-filter>
    </service>
    ...
</application>

Ciclo de vida e gerenciamento do estado

A partir do nível 6 da API, o fluxo de ciclo de vida do app para carro permanece o mesmo, mas agora CarAppService::onCreateSession aceita um parâmetro do tipo SessionInfo que oferece mais informações sobre a Session que está sendo criada, (ou seja, o tipo de tela e o conjunto de modelos aceitos).

Os apps têm a opção de usar a mesma classe Session para lidar com o cluster e a tela principal, ou de criar Sessions específicas para personalizar o comportamento em cada tela, conforme mostrado no snippet a seguir.

Kotlin

override fun onCreateSession(sessionInfo: SessionInfo): Session {
  return if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    ClusterSession()
  } else {
    MainDisplaySession()
  }
}

Java

@Override
@NonNull
public Session onCreateSession(@NonNull SessionInfo sessionInfo) {
  if (sessionInfo.getDisplayType() == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    return new ClusterSession();
  } else {
    return new MainDisplaySession();
  }
}

Não há garantias sobre quando ou se a tela do cluster é fornecida. Também é possível que a Session do cluster seja a única Session (por exemplo, o usuário trocou a tela principal por outro app enquanto seu app está navegando). O acordo "padrão" é de que o app assume o controle da tela do cluster somente depois que NavigationManager::navigationStarted é chamado. No entanto, é possível que o app receba a tela do cluster sem que haja nenhuma navegação ativa ou que nunca a receba. Cabe ao seu app lidar com esses cenários renderizando o estado inativo dos blocos de mapas.

O host cria instâncias separadas de binders e CarContext por Session. Isso significa que, ao usar métodos como ScreenManager::push ou Screen::invalidate, apenas a Session na qual eles são chamados é afetada. Os apps precisam criar os próprios canais de comunicação entre essas instâncias se a comunicação entre cada Session for necessária (por exemplo, usando transmissões, um Singleton compartilhado ou algo assim).

Como testar o suporte a clusters

Você pode testar a implementação no Android Auto e no Android Automotive OS. No Android Auto, isso é feito configurando a unidade principal do computador para emular uma tela de cluster secundária. No Android Automotive OS, as imagens genéricas do sistema para o nível 30 da API e versões mais recentes emulam uma tela de cluster.

Personalizar a TravelEstimate com texto ou ícone

Para personalizar a estimativa de viagem com texto, ícone ou ambos, use os métodos setTripIcon ou setTripText da classe TravelEstimate.Builder. O NavigationTemplate usa a TravelEstimate para definir texto e ícones opcionais ao lado ou no lugar dos dados de horário previsto de chegada, tempo restante e distância restante.

Figura 1. Estimativa de viagem com ícone e texto personalizados.

O snippet a seguir usa setTripIcon e setTripText para personalizar a estimativa de viagem:

Kotlin

TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build()

Java

new TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build();

Fornecer notificações de navegação guiada

Dê instruções de navegação guiada usando uma notificação atualizada com frequência. Para ser tratado como uma notificação de navegação na tela do carro, o builder da notificação precisa fazer o seguinte:

  1. Marcar a notificação como em andamento com o método NotificationCompat.Builder.setOngoing.
  2. Definir a categoria da notificação como Notification.CATEGORY_NAVIGATION.
  3. Estender a notificação com um CarAppExtender.

Uma notificação de navegação é mostrada no widget de coluna na parte de baixo da tela do carro. Se o nível de importância da notificação estiver definido como IMPORTANCE_HIGH, ela também será mostrada como notificação de alerta (HUN, na sigla em inglês). Se a importância não for definida com o método CarAppExtender.Builder.setImportance, a importância do canal de notificação será usada.

O app pode definir uma PendingIntent no CarAppExtender que é enviada ao app quando o usuário toca na HUN ou no widget de coluna.

Se o método NotificationCompat.Builder.setOnlyAlertOnce for chamado com um valor true, uma notificação de alta importância será emitida apenas uma vez na HUN.

O snippet a seguir mostra como criar uma notificação de navegação:

Kotlin

NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    Intent(ACTION_OPEN_APP).setComponent(
                        ComponentName(context, MyNotificationReceiver::class.java)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build()

Java

new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        new CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    new Intent(ACTION_OPEN_APP).setComponent(
                        new ComponentName(context, MyNotificationReceiver.class)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build();

Atualize a notificação da navegação guiada regularmente para mudanças de distância, o que atualiza o widget de coluna, e só mostre a notificação como uma HUN. É possível controlar o comportamento da HUN definindo a importância da notificação com CarAppExtender.Builder.setImportance. A definição da importância como IMPORTANCE_HIGH mostra uma HUN. A definição com qualquer outro valor atualiza apenas o widget de coluna.

Atualizar o conteúdo do PlaceListNavigationTemplate

Você pode permitir que os motoristas atualizem o conteúdo com um simples toque de botão enquanto navegam em listas de lugares criadas com o PlaceListNavigationTemplate. Para ativar a atualização da lista, implemente o método onContentRefreshRequested da interface OnContentRefreshListener e use PlaceListNavigationTemplate.Builder.setOnContentRefreshListener para definir o listener no modelo.

O snippet a seguir mostra como definir o listener no modelo:

Kotlin

PlaceListNavigationTemplate.Builder()
    ...
    .setOnContentRefreshListener {
        // Execute any desired logic
        ...
        // Then call invalidate() so onGetTemplate() is called again
        invalidate()
    }
    .build()

Java

new PlaceListNavigationTemplate.Builder()
        ...
        .setOnContentRefreshListener(() -> {
            // Execute any desired logic
            ...
            // Then call invalidate() so onGetTemplate() is called again
            invalidate();
        })
        .build();

O botão de atualização só aparece no cabeçalho do PlaceListNavigationTemplate quando o listener tem um valor.

Quando o motorista clica no botão, o método onContentRefreshRequested da sua implementação de OnContentRefreshListener é chamado. No onContentRefreshRequested, chame o método Screen.invalidate. Em seguida, o host vai chamar o método Screen.onGetTemplate do app para recuperar o modelo com o conteúdo atualizado. Consulte Atualizar o conteúdo de um modelo para saber mais sobre a atualização de modelos. Contanto que o próximo modelo retornado por onGetTemplate seja do mesmo tipo, ele será contabilizado como uma atualização, e não na cota do modelo.

Oferecer orientações por áudio

Para tocar a orientação de navegação nos alto-falantes do carro, seu app precisa solicitar seleção de áudio. Como parte da AudioFocusRequest, defina o uso como AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE. Além disso, defina o ganho da seleção como AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK.

Simular a navegação

Para verificar a funcionalidade de navegação do app ao enviá-lo à Google Play Store, é necessário que ele implemente o callback NavigationManagerCallback.onAutoDriveEnabled. Quando esse callback é chamado, o app precisa simular o trajeto até o destino escolhido quando o usuário inicia a navegação. O app poderá sair desse modo sempre que o ciclo de vida da Session atual atingir o estado Lifecycle.Event.ON_DESTROY.

Teste se a implementação de onAutoDriveEnabled é chamada executando o seguinte em uma linha de comando:

adb shell dumpsys activity service CAR_APP_SERVICE_NAME AUTO_DRIVE

Isso é mostrado neste exemplo:

adb shell dumpsys activity service androidx.car.app.samples.navigation.car.NavigationCarAppService AUTO_DRIVE

App de navegação padrão para carros

No Android Auto, o app de navegação padrão para carros corresponde ao último app desse tipo que o usuário iniciou. O app padrão recebe as intents de navegação quando o usuário invoca comandos pelo Google Assistente ou quando outro app envia uma intent para iniciar a navegação.

Mostrar alertas de navegação no contexto

O Alert mostra informações importantes para o motorista com ações opcionais sem sair do contexto da tela de navegação. Para oferecer a melhor experiência ao motorista, o Alert funciona no NavigationTemplate para evitar o bloqueio do trajeto de navegação e minimizar a distração do motorista.

O Alert está disponível apenas no NavigationTemplate. Para notificar um usuário fora do NavigationTemplate, use uma notificação de alerta, conforme explicado em Mostrar notificações.

Por exemplo, use Alert para:

  • informar o motorista sobre uma atualização relevante para a navegação atual, como uma mudança nas condições de trânsito;
  • pedir ao motorista uma atualização relacionada à navegação atual, como a existência de um radar móvel;
  • propor uma próxima tarefa e perguntar se o motorista a aceita. Por exemplo, se o motorista quer pegar alguém no caminho.

Na forma básica, um Alert consiste em um título e na duração do Alert. A duração é representada por uma barra de progresso. Também é possível adicionar um subtítulo, um ícone e até dois objetos Action.

Figura 2. Alerta de navegação no contexto.

Depois que um Alert é mostrado, ele não é transferido para outro modelo quando uma interação do motorista resulta na saída do NavigationTemplate. Ele permanece no NavigationTemplate original até o Alert expirar, o usuário realizar uma ação ou o app dispensar o Alert.

Criar um alerta

Use Alert.Builder para criar uma instância de Alert:

Kotlin

Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build()

Java

new Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build();

Para ouvir o cancelamento ou a dispensa do Alert, crie uma implementação da interface AlertCallback. Estes são os caminhos de chamada de AlertCallback:

Configurar a duração do alerta

Escolha uma duração de Alert que corresponda às necessidades do app. A duração recomendada para um Alert de navegação é de 10 segundos. Consulte Alertas de navegação para mais informações.

Mostrar um alerta

Para mostrar um Alert, chame o método AppManager.showAlert, disponível no CarContext do app.

// Show an alert
carContext.getCarService(AppManager.class).showAlert(alert)
  • Chamar showAlert com um Alert que tenha um alertId igual ao ID do Alert mostrado na tela não gera nenhuma ação. O Alert não é atualizado. Para atualizar um Alert, ele precisa ser recriado com um novo alertId.
  • Chamar showAlert com um Alert que tenha um alertId diferente do Alert mostrado na tela dispensa o Alert que está em exibição no momento.

Dispensar um alerta

Embora um Alert seja dispensado automaticamente devido ao tempo limite ou à interação do motorista, você também pode dispensar um Alert de forma manual, por exemplo, caso as informações fiquem desatualizadas. Para dispensar um Alert chame o método dismissAlert com o alertId do Alert.

// Dismiss the same alert
carContext.getCarService(AppManager.class).dismissAlert(alert.getId())

Chamar dismissAlert com um alertId que não corresponde ao Alert mostrado no momento não resulta em nenhuma ação. Isso não gera uma exceção.