Создайте навигационное приложение

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

Укажите поддержку навигации в вашем манифесте.

Вашему навигационному приложению необходимо объявить категорию автомобильного приложения androidx.car.app.category.NAVIGATION в фильтре намерений своего 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>

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

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

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

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

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

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

Для соответствия требованиям качества 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 Level 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 (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 . Информация, предоставленная в этом вызове, может использоваться приборной панелью и проекционным дисплеем автомобиля. В зависимости от конкретного управляемого автомобиля, не вся информация отображается пользователю. Например, головное устройство (DHU) показывает добавленный к Trip Step , но не отображает информацию Destination .

Рисунок для кластерного дисплея

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

  • 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 . Однако возможно, что приложение получит отображение кластера, когда активная навигация не происходит, или никогда не получит его. Ваше приложение должно обрабатывать эти сценарии, отображая тайлы карты в состоянии ожидания.

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

Поддержка тестового кластера

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

Настройте расчет стоимости поездки, добавив текст или значок.

Чтобы настроить отображение расчетного времени в пути с помощью текста, значка или того и другого, используйте методы 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 , оно также отображается как всплывающее уведомление (HUN). Если важность не задана с помощью метода CarAppExtender.Builder.setImportance , используется важность канала уведомлений .

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

Если 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()

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

Обновите содержимое 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 установите значение use как AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE . Также установите усиление фокуса как AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK .

Имитация навигации

Для проверки функциональности навигации вашего приложения при отправке его в Google Play Store, приложение должно реализовывать функцию обратного вызова 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. Предупреждение при контекстной навигации.

После отображения Alert оно не переносится в другой шаблон, если взаимодействие с водителем приводит к выходу из NavigationTemplate . Оно остается в исходном 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 совпадает с ID уже отображаемого Alert , ничего не делает. Alert не обновляется. Чтобы обновить Alert , необходимо создать его заново с новым alertId .
  • Вызов функции showAlert с Alert , имеющим другой alertId , чем уже отображаемое Alert приводит к закрытию отображаемого Alert .

Отклонить оповещение

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

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

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