Как создать приложение для навигации

На этой странице описаны различные функции библиотеки приложений для автомобилей, которые можно использовать для реализации пошаговой навигации в приложении.

Как заявить о поддержке навигации в манифесте

В фильтре интентов CarAppService вашего навигационного приложения должна быть объявлена категория автомобильного приложения:androidx.car.app.category.NAVIGATION

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

Поддержка намерений навигации

Разные форматы намерений позволяют навигационным приложениям взаимодействовать с другими приложениями, например с приложениями для поиска мест и голосовыми помощниками.

Чтобы поддерживать эти форматы намерений, сначала объявите об этом, добавив фильтры намерений в манифест приложения. Расположение этих фильтров намерений зависит от платформы:

  • Android Auto. В элементе манифеста <activity> для компонента Activity, который используется для обработки намерения, когда пользователь не работает с Android Auto.
  • Android Automotive OS: в элементе манифеста <activity> для CarAppActivity.

Затем прочитайте и обработайте намерения в обратных вызовах onCreateScreen() и onNewIntent() в реализации Session вашего приложения.

Обязательные форматы на основе намерений

Чтобы соответствовать требованиям к качеству NF-6, ваше приложение должно обрабатывать намерения навигации.

Необязательные форматы на основе намерений

Вы также можете добавить поддержку следующих форматов намерений, чтобы повысить совместимость приложения:

Как получить доступ к шаблонам навигации

Приложения для навигации могут использовать следующие шаблоны, в которых на заднем плане показывается карта, а во время активной навигации – пошаговые инструкции.

  • NavigationTemplate: Шаблон, в котором во время активной навигации показывается необязательное информационное сообщение и расчетное время поездки.
  • MapWithContentTemplate: Шаблон, позволяющий приложению отрисовывать фрагменты карты с каким-либо контентом (например, списком). Обычно контент отображается как наложение поверх фрагментов карты, при этом карта видна, а ее стабильные области подстраиваются под контент.

Подробнее о том, как использовать эти шаблоны для создания интерфейса навигационного приложения, рассказывается в статье Навигационные приложения.

Чтобы получить доступ к шаблонам навигации, приложению необходимо объявить разрешение androidx.car.app.NAVIGATION_TEMPLATES в файле AndroidManifest.xml:

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

Для создания карт требуется дополнительное разрешение.

Как перейти на шаблон MapWithContentTemplate

Начиная с Car App API уровня 7, элементы MapTemplate, PlaceListNavigationTemplate и RoutePreviewNavigationTemplate считаются устаревшими. Поддержка устаревших шаблонов будет продолжена, но мы настоятельно рекомендуем перейти на MapWithContentTemplate.

Функциональность, предоставляемая этими шаблонами, можно реализовать с помощью MapWithContentTemplate. Примеры фрагментов кода приведены ниже.

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

Навигационные приложения должны передавать хосту дополнительные метаданные. Хост использует эту информацию, чтобы передавать данные на стереосистему автомобиля и предотвращать конфликты между навигационными приложениями при использовании общих ресурсов.

Метаданные навигации предоставляются через автомобильный сервис NavigationManager, доступный из CarContext:

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

Как начать, завершить или остановить навигацию

Чтобы управлять несколькими навигационными приложениями, уведомлениями о маршрутах и данными приборной панели, хост должен знать текущее состояние навигации. Когда пользователь начинает навигацию, вызовите функцию NavigationManager.navigationStarted. Аналогично, когда навигация заканчивается, например когда пользователь прибывает в пункт назначения или отменяет навигацию, вызовите NavigationManager.navigationEnded.

Вызывайте функцию NavigationManager.navigationEnded только после того, как пользователь завершит навигацию. Например, если вам нужно пересчитать маршрут в середине поездки, используйте Trip.Builder.setLoading(true).

Иногда организатору нужно, чтобы приложение прекратило навигацию и звонки onStopNavigation в объекте NavigationManagerCallback, предоставленном вашим приложением через NavigationManager.setNavigationManagerCallback. После этого приложение должно перестать выводить информацию о следующем повороте на приборную панель, отправлять уведомления о навигации и голосовые подсказки.

Как изменить информацию о поездке

Во время активной навигации вызовите метод NavigationManager.updateTrip. Информация, полученная в результате этого вызова, может использоваться приборной панелью и проекционным дисплеем автомобиля. В зависимости от автомобиля пользователю может быть показана не вся информация. Например, в Desktop Head Unit (DHU) показывается Step, добавленный в Trip, но не показывается информация Destination.

Рисование на приборной панели

Чтобы обеспечить пользователям максимально удобный интерфейс, вы можете не ограничиваться показом основных метаданных на приборной панели автомобиля. Начиная с версии 6 Car App API, навигационные приложения могут отображать собственный контент непосредственно на приборной панели (в поддерживаемых автомобилях) со следующими ограничениями:

  • API для кластерного дисплея не поддерживает элементы управления вводом
  • Рекомендации по обеспечению качества приложений для автомобилей NF-9: На приборной панели должны показываться только фрагменты карты. На этих элементах можно показывать активный маршрут навигации.
  • Кластерный API поддерживает только использование элемента NavigationTemplate
    • В отличие от основного экрана, на кластерном экране могут показываться не все элементы интерфейса, например пошаговые инструкции, карточки с расчетным временем прибытия и действия.NavigationTemplate Единственный элемент интерфейса, который всегда отображается, – это фрагменты карты.

Как заявить о поддержке кластеров

Чтобы сообщить хост-приложению, что ваше приложение поддерживает отрисовку на кластерных дисплеях, добавьте элемент androidx.car.app.category.FEATURE_CLUSTER <category> в <intent-filter> манифеста 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"/>
        <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
      </intent-filter>
    </service>
    ...
</application>

Управление жизненным циклом и состоянием

Начиная с уровня API 6, цикл жизни приложения для автомобилей остается прежним, но теперь метод CarAppService::onCreateSession принимает параметр типа SessionInfo, который предоставляет дополнительную информацию о создаваемом объекте Session (а именно тип дисплея и набор поддерживаемых шаблонов).

Приложения могут использовать один и тот же класс Session для обработки кластера и основного дисплея или создать отдельный класс Sessions для каждого дисплея, чтобы настроить поведение на каждом из них (как показано в следующем фрагменте кода).

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

Нет никаких гарантий, что кластер будет показан и когда именно это произойдет. Кроме того, может случиться так, что кластер Session будет единственным Session (например, пользователь переключил основной экран на другое приложение, пока ваше приложение активно строит маршрут). Согласно стандартному соглашению, приложение получает контроль над кластерным дисплеем только после вызова NavigationManager::navigationStarted. Однако приложение может получить доступ к приборной панели, когда активная навигация не выполняется, или не получить его вовсе. В таких случаях ваше приложение должно отображать фрагменты карт в режиме ожидания.

Хост создает отдельные экземпляры связывателя и CarContext для каждого объекта Session. Это означает, что при использовании таких методов, как ScreenManager::push или Screen::invalidate, изменения будут применены только к Session, из которого они вызываются. Приложения должны создавать собственные каналы связи между этими экземплярами, если требуется межпроцессное взаимодействие (например, с помощью широковещательных передач, общего объекта-одиночки или чего-то ещё).Session

Поддержка тестирования кластеров

Вы можете протестировать реализацию как в Android Auto, так и в Android Automotive OS. В Android Auto это делается путем настройки головного устройства для эмуляции дополнительного дисплея приборной панели. В Android Automotive OS системные образы для уровня API 30 и выше имитируют дисплей приборной панели.

Как добавить текст или значок в TravelEstimate

Чтобы добавить к расчетному времени поездки текст, значок или и то и другое, используйте методы setTripIcon или setTripText класса TravelEstimate.Builder. С помощью элемента NavigationTemplate можно задать текст и значки, которые будут показываться рядом с расчетным временем прибытия, оставшимся временем и расстоянием или вместо них. Для этого используется элемент TravelEstimate.

Рисунок 1. Расчетное время поездки с пользовательским значком и текстом.

В приведенном ниже фрагменте кода используются переменные setTripIcon и setTripText, чтобы настроить расчет времени поездки:

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

предоставлять пошаговые уведомления;

Предоставлять пошаговые инструкции по навигации с помощью часто обновляемых уведомлений. Чтобы уведомление считалось навигационным и отображалось на экране автомобиля, его конструктор должен:

  1. Отметьте уведомление как текущее с помощью метода NotificationCompat.Builder.setOngoing.
  2. Установите для категории уведомления значение Notification.CATEGORY_NAVIGATION.
  3. Дополните уведомление с помощью тега CarAppExtender.

Уведомление о навигации отображается в виджете в нижней части экрана автомобиля. Если уровень важности уведомления установлен на IMPORTANCE_HIGH, оно также будет показываться в виде всплывающего уведомления. Если важность не задана с помощью метода CarAppExtender.Builder.setImportance, используется важность канала уведомлений.

Приложение может задать PendingIntent в CarAppExtender, который отправляется в приложение, когда пользователь нажимает на значок или виджет боковой панели.

Если функция NotificationCompat.Builder.setOnlyAlertOnce вызывается со значением true, то в уведомлении высокой важности (HUN) оповещение будет показано только один раз.

В следующем фрагменте кода показано, как создать уведомление о навигации:

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

Регулярно обновляйте уведомление о пошаговых инструкциях, чтобы показывать актуальную информацию о расстоянии, а также обновлять виджет на панели. Уведомление должно показываться только как уведомление о навигации. Вы можете управлять поведением уведомлений, задавая их важность с помощью параметра CarAppExtender.Builder.setImportance. Если задать важность IMPORTANCE_HIGH, будет показано уведомление о нежелательном контенте. Если задать другое значение, обновится только виджет на боковой панели.

Как обновить контент шаблона PlaceListNavigationTemplate

Водители могут обновлять контент одним нажатием кнопки при просмотре списков мест, созданных с помощью PlaceListNavigationTemplate. Чтобы включить обновление списка, реализуйте метод onContentRefreshRequested интерфейса OnContentRefreshListener и используйте PlaceListNavigationTemplate.Builder.setOnContentRefreshListener, чтобы задать прослушиватель в шаблоне.

В следующем фрагменте кода показано, как задать прослушиватель для шаблона:

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

Кнопка обновления показывается в заголовке PlaceListNavigationTemplate, только если у прослушивателя есть значение.

Когда пользователь нажимает кнопку обновления, вызывается метод onContentRefreshRequested реализации OnContentRefreshListener. В методе onContentRefreshRequested вызовите метод Screen.invalidate. Затем хост снова вызывает метод Screen.onGetTemplate вашего приложения, чтобы получить шаблон с обновленным контентом. Подробнее о том, как обновить содержимое шаблона… Если следующий шаблон, возвращенный функцией onGetTemplate, относится к тому же типу, он считается обновлением и не учитывается в квоте шаблонов.

предоставлять аудиоподсказки;

Чтобы голосовые подсказки воспроизводились на динамиках автомобиля, приложению необходимо запросить аудиофокус. В рамках AudioFocusRequest укажите, что использование будет AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE. Также задайте коэффициент фокусировки как AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK.

Как имитировать навигацию

Чтобы мы могли проверить навигационные функции вашего приложения при его отправке в Google Play, в нем должна быть реализована функция обратного вызова NavigationManagerCallback.onAutoDriveEnabled. Когда вызывается этот обратный вызов, ваше приложение должно имитировать навигацию к выбранному пункту назначения, когда пользователь начинает навигацию. Приложение может выйти из этого режима, когда жизненный цикл текущего Session достигнет состояния Lifecycle.Event.ON_DESTROY.

Чтобы проверить, вызывается ли функция onAutoDriveEnabled, выполните в командной строке следующие действия:

adb shell dumpsys activity service CAR_APP_SERVICE_NAME AUTO_DRIVE

Пример:

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

Навигатор по умолчанию в автомобиле

В Android Auto приложение для навигации по умолчанию – это последнее приложение для навигации, которое запустил пользователь. Приложение, заданное по умолчанию, получает намерения навигации, когда пользователь вызывает навигационные команды через Ассистента или когда другое приложение отправляет намерение запустить навигацию.

Показывать контекстные уведомления о навигации

Alert показывает водителю важную информацию и предлагает выполнить действия, не покидая экран навигации. Чтобы не мешать водителю, Alert работает в рамках NavigationTemplate и не перекрывает навигационный маршрут.

Alert доступен только в NavigationTemplate. Чтобы уведомить пользователя вне NavigationTemplate, попробуйте использовать уведомление в верхней части экрана (HUN), как описано в разделе Показ уведомлений.

Например, с помощью Alert можно:

  • Сообщать водителю об изменениях, связанных с текущим маршрутом, например о загруженности дорог.
  • Запросить у водителя информацию о текущем маршруте, например о наличии камер контроля скорости.
  • Предложить водителю выполнить задачу, например забрать кого-нибудь по пути.

В базовой форме объект Alert состоит из названия и продолжительности Alert. Продолжительность представлена в виде индикатора выполнения. При необходимости можно добавить субтитры, значок и до двух объектов Action.

Рисунок 2. Предупреждение о навигации в контексте.

Если водитель взаимодействует с экраном и покидает NavigationTemplate, то после показа Alert оно не переносится в другой шаблон. Оно остается в исходном NavigationTemplate, пока не истечет время ожидания Alert, пользователь не выполнит действие или приложение не закроет Alert.

Как создать оповещение

Чтобы создать экземпляр Alert, используйте Alert.Builder.

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

Если вы хотите отслеживать Alertотмену или отклонение, создайте реализацию интерфейса AlertCallback. Пути звонков AlertCallback:

  • Если время ожидания Alert истекает, хост вызывает метод AlertCallback.onCancel со значением AlertCallback.REASON_TIMEOUT. Затем он вызывает метод AlertCallback.onDismiss.

  • Если водитель нажмет одну из кнопок действий, хост вызовет Action.OnClickListener, а затем AlertCallback.onDismiss.

  • Если Alert не поддерживается, хост вызывает AlertCallback.onCancel со значением AlertCallback.REASON_NOT_SUPPORTED. Ведущий не звонит AlertCallback.onDismiss, потому что Alert не показывали.

Как настроить длительность оповещения

Выберите Alert, подходящую для вашего приложения. Рекомендуемая продолжительность навигации Alert – 10 секунд. Дополнительную информацию можно найти в разделе Оповещения о навигации.

Показать оповещение

Чтобы показать Alert, вызовите метод AppManager.showAlert, доступный через CarContext.

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

  • Если вызвать showAlert с Alert, у которого alertId совпадает с идентификатором Alert, уже показанного на экране, ничего не произойдет. Alert не обновляется. Чтобы обновить Alert, его нужно создать заново с новым alertId.
  • Если вызвать функцию showAlert с параметром Alert, который отличается от alertIdAlert, уже отображаемого на экране, то предупреждение с параметром Alert будет закрыто.

Как закрыть оповещение

Alert автоматически закрывается по истечении времени ожидания или при взаимодействии с водителем. Вы также можете закрыть Alert вручную, например если информация в нем устарела. Чтобы закрыть Alert, вызовите метод dismissAlert, передав ему alertId Alert.

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

Если вызвать dismissAlert с alertId, который не совпадает с уже показанным Alert, ничего не произойдет. Исключение не возникает.