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

Uma variedade de formatos de intent permite que os apps de navegação funcionem com outros apps, como apps de ponto de interesse e assistentes de voz.

Para oferecer suporte a esses formatos de intent, primeiro declare o suporte adicionando filtros de intent no manifesto do app. A localização desses filtros de intent depende da plataforma:

  • Android Auto: no elemento de manifesto <activity> para a Activity usada para processar a intent quando um usuário não está usando o Android Auto.
  • Android Automotive OS: no elemento de manifesto <activity> para o CarAppActivity.

Em seguida, leia e processe as intents nos callbacks onCreateScreen() e onNewIntent() na implementação de Session do app.

Formatos de intent obrigatórios

Para atender ao requisito de qualidade NF-6, seu app precisa processar intents de navegação.

Formatos de intent opcionais

Você também pode oferecer suporte aos seguintes formatos de intent para aumentar ainda mais a interoperabilidade do app:

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: um modelo que mostra uma mensagem informativa opcional e estimativas de viagem durante a navegação ativa.
  • MapWithContentTemplate: um modelo que permite que um app renderize blocos de mapas com algum tipo de conteúdo (por exemplo, uma lista). O conteúdo geralmente é renderizado como uma sobreposição na parte de cima dos blocos de mapas, com o mapa visível e áreas estáveis ajustadas ao conteúdo.

Para mais detalhes sobre como projetar a interface do usuário do 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 outra permissão para desenhar mapas.

Migrar para o MapWithContentTemplate

A partir do nível 7 da API Car App, os MapTemplate, PlaceListNavigationTemplate, e RoutePreviewNavigationTemplate foram descontinuados. Os modelos descontinuados continuarão sendo aceitos, mas é altamente recomendável migrar para o MapWithContentTemplate.

A funcionalidade fornecida por esses modelos pode ser implementada usando o MapWithContentTemplate. Consulte os snippets a seguir para exemplos:

MapTemplate

// MapTemplate (deprecated)
val templateDeprecated = 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()

PlaceListNavigationTemplate

// PlaceListNavigationTemplate (deprecated)
val templateDeprecated = 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()

RoutePreviewNavigationTemplate

// RoutePreviewNavigationTemplate (deprecated)
val templateDeprecated = RoutePreviewNavigationTemplate.Builder()
    .setItemList(
        ItemList.Builder()
            .addItem(
                Row.Builder()
                    .setTitle(title)
                    .build()
            )
            .build()
    )
    .setHeader(header)
    .setNavigateAction(
        Action.Builder()
            .setTitle(actionTitle)
            .setOnClickListener { /* onClick */ }
            .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 { /* onClick */ }
                                    .build()
                            )
                            .build()
                    )
                    .build()
            )
            .setHeader(header)
            .build()
    )
    .setActionStrip(actionStrip)
    .setMapController(
        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. O host usa as informações para fornecer dados à unidade principal do veículo e impedir que aplicativos de navegação entrem em conflito com os recursos compartilhados.

Os metadados de navegação são fornecidos pelo NavigationManager serviço de carro acessível em CarContext:

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

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 o Step adicionado ao 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ó mostra blocos de mapas. Uma rota de navegação ativa pode ser mostrada nesses blocos, se desejar.
  • 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 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.

override fun onCreateSession(sessionInfo: SessionInfo): Session {
    return if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
        ClusterSession()
    } else {
        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:

TravelEstimate.Builder(
    Distance.create(350.0, Distance.UNIT_METERS),
    arrivalTimeAtDestination
)
    .setTripIcon(
        CarIcon.Builder(
            IconCompat.createWithResource(carContext, R.drawable.ic_garage)
        ).build()
    )
    .setTripText(CarText.create("Custom Text"))
    .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 NotificationCompat.Builder.setOngoing método.
  2. Definir a categoria da notificação como Notification.CATEGORY_NAVIGATION.
  3. Estender a notificação com a 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:

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)
                    ),
                    PendingIntent.FLAG_IMMUTABLE
                )
            )
            .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:

PlaceListNavigationTemplate.Builder()
    .setOnContentRefreshListener {
        // Execute any desired logic
        // Then call invalidate() so onGetTemplate() is called again
        screen.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 Screen.invalidate método. 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

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

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

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

Configurar a duração do alerta

Escolha uma Alert duração 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 AppManager.showAlert método disponível no CarContext do app.

carContext.getCarService(AppManager::class.java).showAlert(alert)

  • Chamar showAlert com um Alert que tenha um alertId igual ao ID do Alert já mostrado 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 já mostrado dispensa o alerta Alert mostrado.

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 dismissAlert método com o alertId do Alert.

carContext.getCarService(AppManager::class.java).dismissAlert(alert.id)

Chamar dismissAlert com um alertId que não corresponde ao Alert já mostrado não gera nenhuma ação. Isso não gera uma exceção.