summaryrefslogtreecommitdiff
path: root/Host/app/renderer/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'Host/app/renderer/src/main/java/com')
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt31
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt290
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java41
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt40
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java123
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java45
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt101
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt89
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java148
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt271
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt34
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt44
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java131
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt82
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java94
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt31
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt103
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt42
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt59
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt223
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt106
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt322
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt227
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt121
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt147
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt73
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt58
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt221
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml45
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt205
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml46
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml20
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml23
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml40
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml19
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml24
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml28
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml24
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml32
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml38
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml87
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml72
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml116
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml23
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml36
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml42
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml306
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml87
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt248
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt47
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt442
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt87
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt106
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt42
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java40
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java59
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java198
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java33
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java98
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java118
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java60
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java88
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java104
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java318
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java234
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java56
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java132
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java239
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java330
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml10
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml4
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml4
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml4
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml41
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml57
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml150
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml54
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml35
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml140
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml71
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java109
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java334
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml58
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml47
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java695
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java78
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java295
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java301
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml61
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml71
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml50
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml59
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml97
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml40
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml36
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml47
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml9
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml23
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml35
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml29
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml33
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml35
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml36
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml39
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml38
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml20
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml79
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml64
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml79
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml74
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml65
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml22
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml5
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml56
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml20
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml52
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml19
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml19
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml54
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml44
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml22
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml21
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml624
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml137
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml278
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml91
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml33
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml78
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml391
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml53
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml344
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java200
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java323
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java381
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java48
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java66
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java70
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java461
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml22
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java260
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java250
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java101
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java67
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java60
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java68
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java208
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java252
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java99
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java174
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java139
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java310
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java96
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java102
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java213
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java190
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java54
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java282
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java592
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java43
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java125
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java87
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java104
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java1019
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java143
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java577
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java197
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java129
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java64
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java242
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml37
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml37
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml27
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml19
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml34
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml24
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml31
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml48
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml58
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml51
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml31
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml31
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml35
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml50
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml48
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml25
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml26
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml89
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml202
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml89
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml36
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml82
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml57
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml188
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml69
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml98
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml53
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml60
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml28
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml30
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml32
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml23
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml33
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml161
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml20
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java51
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java58
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java109
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java79
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java134
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java210
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java45
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java139
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java225
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java114
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java61
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java170
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml51
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml99
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml62
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml37
-rw-r--r--Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml38
267 files changed, 26319 insertions, 0 deletions
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt
new file mode 100644
index 0000000..1df079e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/BootCompleteReceiver.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+
+/** A [BroadcastReceiver] used to pre-warm the host and set it as a foreground service. */
+class BootCompleteReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ L.d(LogTags.SERVICE) { "StartUpBootReceiver: received ${intent.action}" }
+ val serviceIntent = Intent(context, RendererService::class.java)
+ context.startForegroundService(serviceIntent)
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt
new file mode 100644
index 0000000..61c3935
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/RendererService.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
+import android.content.res.Configuration
+import android.os.Binder
+import android.os.IBinder
+import android.view.LayoutInflater
+import androidx.car.app.HandshakeInfo
+import androidx.car.app.activity.renderer.ICarAppActivity
+import androidx.car.app.activity.renderer.IRendererService
+import androidx.car.app.serialization.Bundleable
+import androidx.car.app.versioning.CarAppApiLevels
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.android.car.libraries.apphost.common.HostResourceIds
+import com.android.car.libraries.apphost.common.ThreadUtils
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.StatusReporter
+import com.android.car.libraries.apphost.view.TemplateConverterRegistry
+import com.android.car.libraries.apphost.view.TemplatePresenterRegistry
+import com.android.car.libraries.templates.host.di.FeaturesConfig
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig
+import com.android.car.libraries.templates.host.di.TelemetryHandlerFactory
+import com.android.car.libraries.templates.host.di.ThemeManager
+import com.android.car.libraries.templates.host.di.UxreConfig
+import com.android.car.libraries.templates.host.internal.CarActivityDispatcher
+import com.android.car.libraries.templates.host.internal.CommonUtils
+import com.android.car.libraries.templates.host.internal.LogUtil
+import com.android.car.libraries.templates.host.internal.StatusManager
+import com.android.car.libraries.templates.host.internal.debug.ClusterActivity
+import com.android.car.libraries.templates.host.renderer.ScreenRenderer
+import com.android.car.libraries.templates.host.renderer.ScreenRendererRepository
+import com.android.car.libraries.templates.host.view.presenters.common.CommonTemplateConverter
+import com.android.car.libraries.templates.host.view.presenters.common.CommonTemplatePresenterFactory
+import com.android.car.libraries.templates.host.view.presenters.maps.MapsTemplatePresenterFactory
+import com.android.car.libraries.templates.host.view.presenters.navigation.NavigationTemplatePresenterFactory
+import com.android.car.ui.CarUiLayoutInflaterFactory
+import dagger.hilt.android.AndroidEntryPoint
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** A service used to render content of a car app service inside a car app activity. */
+@AndroidEntryPoint
+class RendererService : Service() {
+ // TODO(b/182486338): Migrate the inject point to TemplateView
+ @Inject lateinit var mapsTemplatePresenterFactory: MapsTemplatePresenterFactory
+
+ @Inject lateinit var hostResourceIds: HostResourceIds
+
+ @Inject lateinit var uxreConfig: UxreConfig
+
+ @Inject lateinit var hostApiLevelConfig: HostApiLevelConfig
+
+ @Inject lateinit var themeManager: ThemeManager
+
+ @Inject lateinit var telemetryHandlerFactory: TelemetryHandlerFactory
+ @Inject lateinit var hostFeaturesConfig: FeaturesConfig
+
+ /** Whether the debug overlay is active. */
+ private var isDebugOverlayActive = false
+
+ override fun onCreate() {
+ super.onCreate()
+ L.d(LogTags.SERVICE) { "RendererService.onCreate" }
+
+ // This must be executed within ANR timeout (5 seconds) of the host being launched.
+ setAsForeground()
+ LogUtil.init(telemetryHandlerFactory, applicationContext)
+
+ val layoutInflater = LayoutInflater.from(this.applicationContext)
+ if (layoutInflater.factory2 == null) {
+ layoutInflater.factory2 = CarUiLayoutInflaterFactory()
+ }
+
+ // preload some MapView rendering code to speed things up before it is actually used
+ // by PlaceListMapTemplatePresenter.
+ ThreadUtils.enqueueOnMain { mapsTemplatePresenterFactory.preloadMapView(this) }
+ initClusterActivity()
+ }
+
+ private fun initClusterActivity() {
+ val state =
+ if (hostFeaturesConfig.isClusterActivityEnabled()) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ }
+ packageManager.setComponentEnabledSetting(
+ ComponentName(this, ClusterActivity::class.java),
+ state,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+
+ private fun setAsForeground() {
+ val channel =
+ NotificationChannel(
+ CHANNEL_ID,
+ application.applicationInfo.name,
+ NotificationManager.IMPORTANCE_NONE
+ )
+ val notificationManager = NotificationManagerCompat.from(this)
+ notificationManager.createNotificationChannel(channel)
+
+ val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
+ val notification =
+ notificationBuilder
+ .setOngoing(true)
+ .setSmallIcon(application.applicationInfo.icon)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setCategory(Notification.CATEGORY_SERVICE)
+ .build()
+
+ startForeground(
+ FOREGROUND_SERVICE_NOTIFICATION_ID,
+ notification,
+ FOREGROUND_SERVICE_TYPE_LOCATION
+ )
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ registerPresenters()
+ return RendererServiceBinder(this)
+ }
+
+ override fun onUnbind(intent: Intent): Boolean {
+ L.d(LogTags.SERVICE) { "RendererService.onUnbind" }
+
+ // Note that even when the RendererService is unbound. The CarAppService remains bound
+ // because the car app can remain alive in the background (e.g. nav apps sending TBT
+ // instructions). Hence we do not clear the Carhosts here.
+ ScreenRendererRepository.clear()
+ return super.onUnbind(intent)
+ }
+
+ override fun onDestroy() {
+ L.d(LogTags.SERVICE) { "RendererService.onDestroy" }
+
+ // Note that even when the RendererService is unbound. The CarAppService remains bound
+ // because the car app can remain alive in the background (e.g. nav apps sending TBT
+ // instructions). Hence we do not clear the Carhosts here.
+ ScreenRendererRepository.clear()
+ super.onDestroy()
+ }
+
+ override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
+ if (args?.contains("debug_overlay") == true) {
+ if (!CommonUtils.isDebugEnabled(/* context= */ this)) {
+ writer?.println("Debug enabled required for debug overlay")
+ return
+ }
+ isDebugOverlayActive = !isDebugOverlayActive
+ ScreenRendererRepository.getAll().forEach { it.showDebugOverlay(isDebugOverlayActive) }
+ if (isDebugOverlayActive) {
+ writer?.println("Debug overlay enabled")
+ } else {
+ writer?.println("Debug overlay disabled")
+ }
+ } else {
+ writer?.let { StatusManager.reportStatus(writer, StatusReporter.Pii.HIDE) }
+ }
+ }
+
+ override fun onConfigurationChanged(config: Configuration) {
+ super.onConfigurationChanged(config)
+
+ ScreenRendererRepository.getAll().forEach { it.onConfigurationChanged(config) }
+ }
+
+ private fun registerPresenters() {
+ TemplatePresenterRegistry.get().clear()
+ TemplatePresenterRegistry.get().register(NavigationTemplatePresenterFactory.get())
+ TemplatePresenterRegistry.get().register(CommonTemplatePresenterFactory.get())
+ TemplateConverterRegistry.get().register(CommonTemplateConverter.get())
+ TemplatePresenterRegistry.get().register(mapsTemplatePresenterFactory)
+ }
+
+ private inner class RendererServiceBinder(val context: Context) :
+ IRendererService.Stub(), CarActivityDispatcher.Callback {
+
+ override fun initialize(
+ carActivity: ICarAppActivity,
+ serviceName: ComponentName,
+ displayId: Int
+ ): Boolean {
+ L.d(LogTags.SERVICE) { "RendererServiceBinder.initialize: $serviceName" }
+ val renderer = findRenderer(serviceName, displayId) ?: return false
+ ThreadUtils.runOnMain { renderer.onCreateActivity(carActivity) }
+ return true
+ }
+
+ override fun terminate(serviceName: ComponentName) {
+ if (!isValid(serviceName)) return
+ L.d(LogTags.SERVICE) { "RendererServiceBinder.terminate: $serviceName" }
+ doTerminate(serviceName)
+ }
+
+ override fun onDisconnect(serviceName: ComponentName) {
+ L.d(LogTags.SERVICE) { "RendererServiceBinder.onDisconnect: $serviceName" }
+ doTerminate(serviceName)
+ }
+
+ private fun doTerminate(serviceName: ComponentName) {
+ ThreadUtils.runOnMain { ScreenRendererRepository.remove(serviceName)?.onDestroy() }
+ }
+
+ override fun onNewIntent(intent: Intent, serviceName: ComponentName, displayId: Int): Boolean {
+ L.i(LogTags.SERVICE) { "RendererServiceBinder.onNewIntent: $serviceName" }
+ val renderer = findRenderer(serviceName, displayId) ?: return false
+ ThreadUtils.runOnMain { renderer.onNewIntent(intent) }
+ return true
+ }
+
+ override fun performHandshake(serviceName: ComponentName, appLatestApiLevel: Int): Bundleable {
+ val apiLevel = Math.min(appLatestApiLevel, CarAppApiLevels.getLatest())
+ L.i(LogTags.SERVICE) {
+ "RendererServiceBinder.performHandshake: $serviceName, " +
+ "appLatestApiLevel: $appLatestApiLevel, chosen api level: $apiLevel"
+ }
+ // Store in the host whenever we need to start checking versions.
+ return Bundleable.create(HandshakeInfo(context.packageName, apiLevel))
+ }
+
+ private fun findRenderer(serviceName: ComponentName, displayId: Int): ScreenRenderer? {
+ if (!isValid(serviceName)) return null
+
+ return ScreenRendererRepository.computeIfAbsent(serviceName) {
+ L.d(LogTags.SERVICE) {
+ "RendererServiceBinder.findRenderer: $serviceName - " + "created new ScreenRenderer"
+ }
+ ScreenRenderer(
+ context.applicationContext,
+ serviceName,
+ displayId,
+ this,
+ hostResourceIds,
+ uxreConfig,
+ hostApiLevelConfig,
+ themeManager,
+ telemetryHandlerFactory.create(context, serviceName),
+ hostFeaturesConfig,
+ isDebugOverlayActive
+ )
+ }
+ }
+
+ private fun isValid(serviceName: ComponentName?): Boolean {
+ if (serviceName == null) {
+ L.e(LogTags.SERVICE) { "Service name was not specified!" }
+ return false
+ }
+ val senderPackage = context.packageManager.getNameForUid(Binder.getCallingUid())
+ if (senderPackage == null || senderPackage != serviceName.packageName) {
+ L.e(LogTags.SERVICE) { "Could not verify the caller!" }
+ return false
+ }
+ return true
+ }
+ }
+
+ companion object {
+ const val CHANNEL_ID = "default"
+ const val FOREGROUND_SERVICE_NOTIFICATION_ID = 1
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java
new file mode 100644
index 0000000..5bb5d9a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/FeaturesConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+/** An interface for providing host features config */
+public interface FeaturesConfig {
+ /** Returns whether the host has cluster activity features enabled */
+ boolean isClusterActivityEnabled();
+
+ /** Returns whether the host supports pan and zoom features in the navigation template */
+ boolean isNavPanZoomEnabled();
+
+ /** Returns whether the host supports pan and zoom features in POI and route preview templates */
+ boolean isPoiRoutePreviewPanZoomEnabled();
+
+ /** Returns whether the host supports content refresh on POI templates */
+ boolean isPoiContentRefreshEnabled();
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java
new file mode 100644
index 0000000..80aea9d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/HostApiLevelConfig.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+
+import android.content.ComponentName;
+
+/** An interface for providing host API overrides */
+public interface HostApiLevelConfig {
+ /** The min api level for given car app */
+ int getHostMinApiLevel(int defaultValue, ComponentName componentName);
+
+ /** The max api level for given car app */
+ int getHostMaxApiLevel(int defaultValue, ComponentName componentName);
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java
new file mode 100644
index 0000000..84f5c91
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/MapViewContainerFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+
+import android.content.Context;
+import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer;
+
+/** An interface used for creating a {@link AbstractMapViewContainer} */
+public interface MapViewContainerFactory {
+
+ /** returns a AbstractMapViewContainer that used in {@link AbstractMapViewContainer} */
+ AbstractMapViewContainer create(Context context, int theme);
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java
new file mode 100644
index 0000000..f52df92
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/TelemetryHandlerFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+
+import android.content.ComponentName;
+import android.content.Context;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+/** An interface used for creating a {@link TelemetryHandler} */
+public interface TelemetryHandlerFactory {
+
+ /** Returns a new {@link TelemetryHandler} instance */
+ TelemetryHandler create(Context context, ComponentName componentName);
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java
new file mode 100644
index 0000000..beca618
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/ThemeManager.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+
+import android.content.Context;
+
+/** Provides the theme that should be used throughout the UI. */
+public interface ThemeManager {
+
+ /** Applies appropriate theme to the context. */
+ void applyTheme(Context context);
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java
new file mode 100644
index 0000000..5c86c4c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/di/UxreConfig.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.di;
+
+/** An interface used for providing UXRE configs */
+public interface UxreConfig {
+
+ /** The max size of a car app template stack */
+ int getTemplateStackMaxSize(int defaultValue);
+
+ /** The max length of a car app list for showing routes. */
+ int getRouteListMaxLength(int defaultValue);
+
+ /** The max length of a car app list for showing pane information. */
+ int getPaneMaxLength(int defaultValue);
+
+ /** The max length of a car app grid view. */
+ int getGridMaxLength(int defaultValue);
+
+ /**
+ * The max length of a generic, uniform car app list for cases where the OEM did not override the
+ * default UXRE cumulative content limit value.
+ */
+ int getListMaxLength(int defaultValue);
+
+ /** Default max string length */
+ int getCarAppDefaultMaxStringLength(int defaultValue);
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt
new file mode 100644
index 0000000..8ba55af
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/AppIconLoaderImpl.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.android.car.libraries.apphost.common.AppIconLoader
+
+/** Android Automotive implementation of [AppIconLoader] */
+object AppIconLoaderImpl : AppIconLoader {
+ override fun getRoundAppIcon(context: Context, componentName: ComponentName): Drawable {
+ return try {
+ val pm = context.packageManager
+ val applicationInfo = pm.getApplicationInfo(componentName.packageName, 0)
+ val appIconResId = applicationInfo.icon
+
+ pm.getResourcesForApplication(componentName.packageName).getDrawable(appIconResId, null)
+ } catch (ex: Exception) {
+ getDefaultAppIcon(context)
+ }
+ }
+
+ private fun getDefaultAppIcon(context: Context): Drawable {
+ return context.resources.getDrawable(android.R.drawable.sym_def_app_icon, null)
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java
new file mode 100644
index 0000000..fdff26b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarActivityDispatcher.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal;
+
+import android.content.ComponentName;
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.util.Log;
+import androidx.car.app.activity.renderer.ICarAppActivity;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/**
+ * A dispatcher that can be used to send messages to {@link ICarAppActivity}, handling any remote
+ * errors.
+ */
+public class CarActivityDispatcher {
+ private final ComponentName mAppName;
+ private final ICarAppActivity mCarAppActivity;
+ private final Callback mCallback;
+ private boolean mIsConnected;
+
+ public CarActivityDispatcher(
+ ComponentName appName, ICarAppActivity carActivity, Callback callback) {
+ mAppName = appName;
+ mCarAppActivity = carActivity;
+ mCallback = callback;
+ mIsConnected = true;
+ }
+
+ /** {@link CarActivityDispatcher} callbacks */
+ public interface Callback {
+ /** Notifies that the client associated with this {@link ICarAppActivity} is disconnected */
+ void onDisconnect(ComponentName appName);
+ }
+
+ /** An IPC call that can be dispatched by this dispatcher */
+ public interface IPCCall {
+ /** Remote invocation to execute */
+ void call(ICarAppActivity carActivity) throws RemoteException;
+ }
+
+ /** Returns true if the application is still considered to be connected */
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+
+ /**
+ * Dispatches an IPC call to the {@link ICarAppActivity} associated with this dispatcher. If this
+ * result in an error, the dispatcher will handle the error and returns false.
+ *
+ * @return true iif dispatch is successful.
+ */
+ public boolean dispatchNoFail(IPCCall call) {
+ if (!mIsConnected) {
+ // Ignoring request as we have already disconnected from the client app
+ return false;
+ }
+
+ try {
+ call.call(mCarAppActivity);
+ return true;
+ } catch (DeadObjectException e) {
+ Log.w(LogTags.APP_HOST, "App " + mAppName + " is dead", e);
+ return false;
+ } catch (RemoteException e) {
+ Log.w(LogTags.APP_HOST, "App " + mAppName + " has caused a remote exception", e);
+ return false;
+ } catch (Throwable e) {
+ Log.w(LogTags.APP_HOST, "App " + mAppName + " caused an unknown error", e);
+ return false;
+ }
+ }
+
+ /**
+ * Dispatches an IPC call to the {@link ICarAppActivity} associated with this dispatcher. If this
+ * result in an error, the dispatcher will handle the error and then call {@link
+ * Callback#onDisconnect(ComponentName)} to notify that this client is not longer valid.
+ */
+ public void dispatch(IPCCall call) {
+ if (!mIsConnected) {
+ // Ignoring request as we have already disconnected from the client app
+ return;
+ }
+
+ try {
+ call.call(mCarAppActivity);
+ } catch (DeadObjectException e) {
+ Log.e(LogTags.APP_HOST, "App " + mAppName + " is dead", e);
+ mIsConnected = false;
+ mCallback.onDisconnect(mAppName);
+ } catch (RemoteException e) {
+ Log.e(LogTags.APP_HOST, "App " + mAppName + " has caused a remote exception", e);
+ disconnect();
+ } catch (Throwable e) {
+ Log.e(LogTags.APP_HOST, "App " + mAppName + " caused an unknown error", e);
+ disconnect();
+ }
+ }
+
+ /** Disconnects this dispatcher from its associated {@link ICarAppActivity} */
+ public void disconnect() {
+ mIsConnected = false;
+ try {
+ mCarAppActivity.finishCarApp();
+ } catch (Throwable e) {
+ // Ignoring error as we are already finishing anyways (avoid spamming the logs).
+ }
+ mCallback.onDisconnect(mAppName);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java
new file mode 100644
index 0000000..6ea5d12
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarAppServiceInfo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal;
+
+import static android.content.pm.PackageManager.GET_RESOLVED_FILTER;
+import static androidx.car.app.CarAppService.CATEGORY_NAVIGATION_APP;
+import static androidx.car.app.CarAppService.SERVICE_INTERFACE;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+/** This class provides information about car app services. */
+public class CarAppServiceInfo {
+ private final PackageManager mPackageManager;
+ private final ComponentName mServiceName;
+
+ public CarAppServiceInfo(Context context, ComponentName serviceName) {
+ mPackageManager = context.getPackageManager();
+ mServiceName = serviceName;
+ }
+
+ /** Returns true for navigation services. */
+ public boolean isNavigationService() {
+ Intent intent =
+ new Intent(SERVICE_INTERFACE)
+ .setPackage(mServiceName.getPackageName())
+ .addCategory(CATEGORY_NAVIGATION_APP);
+ return !mPackageManager.queryIntentServices(intent, GET_RESOLVED_FILTER).isEmpty();
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt
new file mode 100644
index 0000000..bca23e9
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostConfigImpl.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.StyleableRes
+import androidx.car.app.versioning.CarAppApiLevels
+import com.android.car.libraries.apphost.common.CarHostConfig
+import com.android.car.libraries.templates.host.R
+import com.android.car.libraries.templates.host.di.FeaturesConfig
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig
+
+/** Configuration options from the car host. */
+class CarHostConfigImpl(
+ private val context: Context,
+ appName: ComponentName,
+ hostApiLevelConfig: HostApiLevelConfig,
+ private val featuresConfig: FeaturesConfig
+) : CarHostConfig(appName) {
+ private val hostMinApi: Int =
+ hostApiLevelConfig.getHostMinApiLevel(CarAppApiLevels.getOldest(), appName)
+ private val hostMaxApi: Int =
+ hostApiLevelConfig.getHostMaxApiLevel(CarAppApiLevels.getLatest(), appName)
+
+ override fun getHostMinApi(): Int {
+ return hostMinApi
+ }
+
+ override fun getHostMaxApi(): Int {
+ return hostMaxApi
+ }
+
+ override fun isButtonColorOverriddenByOEM(): Boolean {
+ return getBooleanAttr(R.attr.templateActionButtonUseOemColors)
+ }
+
+ override fun getAppUnbindSeconds(): Int {
+ return context.resources.getInteger(
+ R.integer.app_unbind_delay_seconds
+ )
+ }
+
+ override fun getHostIntentExtrasToRemove(): MutableList<String> {
+ return mutableListOf()
+ }
+
+ override fun isNewTaskFlowIntent(intent: Intent?): Boolean {
+ return true
+ }
+
+ override fun getPrimaryActionOrder(): Int {
+ return getIntAttr(R.attr.templateActionButtonPrimaryHorizontalOrder)
+ }
+
+ override fun isClusterEnabled(): Boolean {
+ return featuresConfig.isClusterActivityEnabled()
+ }
+
+ override fun isNavPanZoomEnabled(): Boolean {
+ return featuresConfig.isNavPanZoomEnabled()
+ }
+
+ override fun isPoiRoutePreviewPanZoomEnabled(): Boolean {
+ return featuresConfig.isPoiRoutePreviewPanZoomEnabled()
+ }
+
+ override fun isPoiContentRefreshEnabled(): Boolean {
+ return featuresConfig.isPoiContentRefreshEnabled()
+ }
+
+ private fun getBooleanAttr(attr: Int): Boolean {
+ @StyleableRes val themeAttrs = intArrayOf(attr)
+ val ta = context.obtainStyledAttributes(themeAttrs)
+ val value = ta.getBoolean(0, false)
+ ta.recycle()
+ return value
+ }
+
+ private fun getIntAttr(attr: Int): Int {
+ @StyleableRes val themeAttrs = intArrayOf(attr)
+ val ta = context.obtainStyledAttributes(themeAttrs)
+ val value = ta.getInt(0, 0)
+ ta.recycle()
+ return value
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt
new file mode 100644
index 0000000..bc3138f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarHostRepository.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ComponentName
+import androidx.annotation.MainThread
+import com.android.car.libraries.apphost.CarHost
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.StatusReporter
+import com.google.common.base.Supplier
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+
+/** Manages a cache of [CarHost]s. */
+@MainThread
+object CarHostRepository : StatusReporter {
+ private val cache: MutableMap<ComponentName, CarHost> = ConcurrentHashMap()
+
+ init {
+ StatusManager.addStatusReporter(StatusManager.ReportSection.APP_HOST, this)
+ }
+
+ /**
+ * Returns a [CarHost] for the given `appName` to use for app communication. If the key is not
+ * present in the cache, uses the [Supplier] to retrieve the [CarHost] and puts it in the cache.
+ */
+ @Synchronized
+ fun computeIfAbsent(appName: ComponentName, carHostSupplier: Supplier<CarHost>): CarHost {
+ return cache.computeIfAbsent(appName) { carHostSupplier.get() }
+ }
+
+ /**
+ * @return a [CarHost] for the given [appName] to use for app communication, or `null` if the key
+ * is not present in the cache.
+ */
+ @Synchronized
+ fun get(appName: ComponentName): CarHost? {
+ return cache[appName]
+ }
+
+ /** Invalidates and removes the [CarHost] from cache for the given [appName]. */
+ @Synchronized
+ fun remove(appName: ComponentName) {
+ val carHost = cache.remove(appName)
+ carHost?.unbindFromApp()
+ carHost?.invalidate()
+ }
+
+ /** Invalidates all the [CarHost] objects in the cache and empties the cache. */
+ @Synchronized
+ fun clear() {
+ if (cache.isNotEmpty()) {
+ for (carHost in cache.values) {
+ carHost.unbindFromApp()
+ carHost.invalidate()
+ }
+ }
+ cache.clear()
+ }
+
+ override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) {
+ try {
+ pw.println("Car host cache")
+ pw.printf("- size: %d\n", cache.size)
+ pw.printf("- hosts: %d\n", cache.size)
+ for ((name, value) in cache) {
+ pw.println("\n-------------------------------")
+ pw.printf("Host: %s\n", name.flattenToShortString())
+ value.reportStatus(pw, piiHandling)
+ }
+ } catch (t: Throwable) {
+ L.e(LogTags.APP_HOST, t, "Failed to produce status report for car host cache")
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java
new file mode 100644
index 0000000..2441bb5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CarUxRestrictionsUtil.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal;
+
+import android.car.Car;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Utility class to access Car Restriction Manager.
+ *
+ * <p>This class must be a singleton because only one listener can be registered with {@link
+ * CarUxRestrictionsManager} at a time, as documented in {@link
+ * CarUxRestrictionsManager#registerListener}.
+ */
+// TODO(b/187312393): Rename to CarUxRestrictionsManager
+public class CarUxRestrictionsUtil {
+ private static final String TAG = "CarUxRestrictionsUtil";
+
+ @NonNull private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions();
+
+ private final Set<OnUxRestrictionsChangedListener> mObservers =
+ Collections.newSetFromMap(new WeakHashMap<>());
+
+ private static CarUxRestrictionsUtil sInstance = null;
+
+ private CarUxRestrictionsUtil(Context context) {
+ CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener =
+ (carUxRestrictions) -> {
+ if (carUxRestrictions == null) {
+ mCarUxRestrictions = getDefaultRestrictions();
+ } else {
+ mCarUxRestrictions = carUxRestrictions;
+ }
+
+ ThreadUtils.runOnMain(
+ () -> {
+ for (OnUxRestrictionsChangedListener observer : mObservers) {
+ observer.onRestrictionsChanged(mCarUxRestrictions);
+ }
+ });
+ };
+
+ try {
+ Car carApi = Car.createCar(context.getApplicationContext());
+
+ try {
+ CarUxRestrictionsManager carUxRestrictionsManager =
+ (CarUxRestrictionsManager) carApi.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
+ carUxRestrictionsManager.registerListener(listener);
+ listener.onUxRestrictionsChanged(carUxRestrictionsManager.getCurrentCarUxRestrictions());
+ } catch (NullPointerException e) {
+ Log.e(TAG, "Car not connected", e);
+ // mCarUxRestrictions will be the default
+ }
+ } catch (SecurityException e) {
+ Log.w(TAG, "Unable to connect to car service, assuming unrestricted", e);
+ listener.onUxRestrictionsChanged(
+ new CarUxRestrictions.Builder(false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0)
+ .build());
+ }
+ }
+
+ @NonNull
+ private static CarUxRestrictions getDefaultRestrictions() {
+ return new CarUxRestrictions.Builder(
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0)
+ .build();
+ }
+
+ /** Listener interface used to update clients on UxRestrictions changes */
+ public interface OnUxRestrictionsChangedListener {
+ /** Called when CarUxRestrictions changes */
+ void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions);
+ }
+
+ /** Returns the singleton sInstance of this class */
+ @NonNull
+ public static CarUxRestrictionsUtil getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new CarUxRestrictionsUtil(context);
+ }
+
+ return sInstance;
+ }
+
+ /**
+ * Registers a listener on this class for updates to CarUxRestrictions. Multiple listeners may be
+ * registered. Note that this class will only hold a weak reference to the listener, you must
+ * maintain a strong reference to it elsewhere.
+ */
+ public void register(OnUxRestrictionsChangedListener listener) {
+
+ ThreadUtils.runOnMain(
+ () -> {
+ mObservers.add(listener);
+ listener.onRestrictionsChanged(mCarUxRestrictions);
+ });
+ }
+
+ /** Unregisters a registered listener */
+ public void unregister(OnUxRestrictionsChangedListener listener) {
+ ThreadUtils.runOnMain(() -> mObservers.remove(listener));
+ }
+
+ @NonNull
+ public CarUxRestrictions getCurrentRestrictions() {
+ return mCarUxRestrictions;
+ }
+
+ /**
+ * Returns whether any of the given flags are blocked by the specified restrictions. If null is
+ * given, the method returns true for safety.
+ */
+ public static boolean isRestricted(
+ @CarUxRestrictionsInfo int restrictionFlags, @Nullable CarUxRestrictions uxr) {
+ return (uxr == null) || ((uxr.getActiveRestrictions() & restrictionFlags) != 0);
+ }
+
+ /** Sets car UX restrictions. Only used for testing. */
+ @VisibleForTesting
+ public void setUxRestrictions(CarUxRestrictions carUxRestrictions) {
+ mCarUxRestrictions = carUxRestrictions;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt
new file mode 100644
index 0000000..8f757a1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ClusterIconContentProvider.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import androidx.core.content.contentValuesOf
+import androidx.core.graphics.scale
+import androidx.core.net.toUri
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction.HOST_FAILURE_CLUSTER_ICON
+import com.android.car.libraries.templates.host.internal.ClusterIconContentProvider.Companion.addToCache
+import com.android.car.libraries.templates.host.internal.ClusterIconContentProvider.Companion.queryIconData
+import com.google.common.cache.Cache
+import com.google.common.cache.CacheBuilder
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import com.android.car.libraries.templates.host.R
+
+/**
+ * A [ContentProvider] for providing navigation state icons to the Cluster.
+ *
+ * It uses an in-memory cache of [Bitmap]s that can be retrieved with a [Uri]. Use [addToCache] to
+ * add a bitmap to the cache. Use [queryIconData] to check for the existence of a bitmap in cache
+ * (note that this will refresh the validity of the entry).
+ */
+class ClusterIconContentProvider : ContentProvider() {
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private lateinit var iconProviderDelegate: IconProviderDelegate
+
+ override fun onCreate(): Boolean {
+ val context = checkNotNull(context)
+
+ val timeoutMillis =
+ context.resources.getInteger(R.integer.cluster_icon_cache_duration_millis).toLong()
+
+ val authority = authority(context)
+ iconProviderDelegate = IconProviderDelegate(authority, timeoutMillis, scope)
+
+ return true
+ }
+
+ override fun shutdown() {
+ scope.cancel("ContentProvider shutting down")
+ iconProviderDelegate.shutdown()
+ super.shutdown()
+ }
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+ return iconProviderDelegate.openFile(uri)
+ }
+
+ /**
+ * Get the uri and aspect ratio of an icon if it exists in cache. Prefer to use the convenience
+ * method [queryIconData], instead of calling this directly.
+ *
+ * @param selection the iconId to look up
+ * @return a [Cursor] with or 1 row if a match was found, or 0 rows otherwise
+ */
+ override fun query(
+ uri: Uri,
+ projection: Array<String>?,
+ selection: String?,
+ selectionArgs: Array<String>?,
+ sortOrder: String?
+ ): Cursor {
+ val cursor =
+ MatrixCursor(
+ arrayOf(QUERY_RESULT_CONTENT_URI, QUERY_RESULT_ASPECT_RATIO),
+ /* initialCapacity */ 1
+ )
+ iconProviderDelegate.query(selection)?.let { (contentUri, aspectRatio) ->
+ cursor.addRow(arrayOf(contentUri, aspectRatio))
+ }
+
+ return cursor
+ }
+
+ /**
+ * Converts the provided [ByteArray] to a Bitmap and caches it, returning the URI path for this
+ * icon. There are no stability guarantees for the keys / expected values, so prefer to use the
+ * convenience method [addToCache], instead of calling this directly.
+ */
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ if (values == null) return null
+ val iconId = values.getAsString(INSERT_PARAM_ICON_ID) ?: return null
+ val bytes = values.getAsByteArray(INSERT_PARAM_BITMAP_BYTES) ?: return null
+ return iconProviderDelegate.cacheIcon(bytes, iconId)
+ }
+
+ override fun getType(uri: Uri): String? = null
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array<String>?
+ ): Int = 0
+
+ companion object {
+
+ private const val INSERT_PARAM_ICON_ID = "iconId"
+ private const val INSERT_PARAM_BITMAP_BYTES = "data"
+
+ private const val QUERY_RESULT_CONTENT_URI = "contentUri"
+ private const val QUERY_RESULT_ASPECT_RATIO = "aspectRatio"
+
+ /**
+ * @return a uri and aspect ratio for the icon if it already exists in cache. [null] otherwise
+ */
+ fun queryIconData(iconId: String, context: Context): Pair<String, Double>? {
+ context.contentResolver.query(contentUri(context), null, iconId, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val contentUriIndex = cursor.getColumnIndex(QUERY_RESULT_CONTENT_URI)
+ val aspectRatioIndex = cursor.getColumnIndex(QUERY_RESULT_ASPECT_RATIO)
+ if (contentUriIndex >= 0 && aspectRatioIndex >= 0) {
+ val contentUri = cursor.getString(contentUriIndex)
+ val aspectRatio = cursor.getDouble(aspectRatioIndex)
+ return contentUri to aspectRatio
+ } else {
+ L.w(LogTags.CLUSTER) {
+ "Icon for id $iconId exists, but failed to extract URI/aspectRatio"
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ /** Saves the bitmap to an in-memory cache, and returns a Uri that can be used to access it. */
+ fun addToCache(iconId: String, bitmapBytes: ByteArray, context: Context): Uri? {
+ return context.contentResolver.insert(
+ contentUri(context),
+ contentValuesOf(INSERT_PARAM_ICON_ID to iconId, INSERT_PARAM_BITMAP_BYTES to bitmapBytes)
+ )
+ }
+
+ private fun contentUri(context: Context) = "content://${authority(context)}".toUri()
+
+ /** Returns the provider's authority, as defined in the Manifest. */
+ private fun authority(context: Context) = "${context.packageName}.ClusterIconContentProvider"
+ }
+}
+
+/**
+ * This class extracts most of the logic out of [ClusterIconContentProvider] so it can be more
+ * easily tested.
+ */
+class IconProviderDelegate(
+ private val authority: String,
+ cacheTimeoutMillis: Long,
+ private val coroutineScope: CoroutineScope
+) {
+ private var cache: Cache<String, Bitmap> =
+ CacheBuilder.newBuilder().expireAfterAccess(cacheTimeoutMillis, TimeUnit.MILLISECONDS).build()
+
+ private val uriMatcher =
+ UriMatcher(UriMatcher.NO_MATCH).apply { addURI(authority, "img/*", URI_IMAGE_CODE) }
+
+ fun cacheIcon(bytes: ByteArray, iconId: String): Uri {
+ val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+ val key = keyForIconId(iconId)
+
+ cache.put(key, bitmap)
+
+ return uriForKey(key)
+ }
+
+ /** @return a [Pair] with ContentUri and AspectRatio if a match was found, [null] otherwise */
+ fun query(iconId: String?): Pair<Uri, Double>? {
+ if (iconId == null) return null
+ val key = keyForIconId(iconId)
+ val bitmap = cache.getIfPresent(key) ?: return null
+
+ val contentUri = uriForKey(key)
+ val aspectRatio = bitmap.width.toDouble() / bitmap.height.toDouble()
+ return contentUri to aspectRatio
+ }
+
+ /** Returns a [ParcelFileDescriptor] that will be written to asynchronously */
+ fun openFile(uri: Uri): ParcelFileDescriptor {
+ return when (uriMatcher.match(uri)) {
+ URI_IMAGE_CODE -> {
+ val key =
+ requireNotNull(uri.lastPathSegment) {
+ "Cluster icon requested but no key provided. URI=$uri"
+ }
+
+ val bitmap =
+ cache.getIfPresent(key)
+ ?: run {
+ LogUtil.log(HOST_FAILURE_CLUSTER_ICON)
+ throw IllegalStateException("Requested cluster icon that's not in cache. (key=$key)")
+ }
+ val width = uri.getQueryParameter("w")?.toIntOrNull() ?: bitmap.width
+ val height = uri.getQueryParameter("h")?.toIntOrNull() ?: bitmap.height
+ // TODO(b/197754774): Cache scaled bitmaps
+ val scaledBitmap =
+ if (width != bitmap.width || height != bitmap.height) {
+ bitmap.scale(width, height)
+ } else {
+ bitmap
+ }
+
+ // Use a pipe to avoid eagerly saving bitmaps to disk (or at all)
+ val (readPipe, writePipe) = ParcelFileDescriptor.createReliablePipe()
+
+ // asynchronously write bitmap to output stream
+ writeToPipeAsync(writePipe, scaledBitmap)
+
+ // Give the readPipe for cluster to consume the bitmap
+ readPipe
+ }
+ else ->
+ throw IllegalArgumentException("Requested a path that doesn't correspond to an icon: $uri")
+ }
+ }
+
+ private fun writeToPipeAsync(writePipe: ParcelFileDescriptor, bitmap: Bitmap) =
+ coroutineScope.launch {
+ runCatching {
+ L.d(LogTags.CLUSTER) { "Writing bitmap to pipe" }
+ ParcelFileDescriptor.AutoCloseOutputStream(writePipe).use { outputStream ->
+ bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream)
+ }
+ }
+ .onFailure {
+ L.e(LogTags.CLUSTER, it) { "IOException writing cluster icon to pipe" }
+ writePipe.closeWithError("IOException writing to pipe")
+ }
+ }
+
+ fun shutdown() {
+ cache.invalidateAll()
+ }
+
+ private fun keyForIconId(iconId: String) = "cluster_icon_$iconId"
+
+ private fun uriForKey(key: String) = "content://${authority}/img/$key".toUri()
+
+ companion object {
+ private const val URI_IMAGE_CODE = 1
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt
new file mode 100644
index 0000000..21e694e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ColorContrastCheckStateImpl.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import com.android.car.libraries.apphost.common.ColorContrastCheckState
+
+/** Manages the state of color contrast checks in template apps. */
+class ColorContrastCheckStateImpl : ColorContrastCheckState {
+ private var checkPassed = true
+ override fun setCheckPassed(passed: Boolean) {
+ checkPassed = passed
+ }
+
+ override fun getCheckPassed(): Boolean {
+ return checkPassed
+ }
+
+ override fun checksContrast(): Boolean {
+ return true
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt
new file mode 100644
index 0000000..21b74cc
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/CommonUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.os.Build
+
+/** Holds static util methods for common usage in the host. */
+object CommonUtils {
+ /**
+ * Key for the extra that we insert into an Intent to mark it as coming from a notification
+ * action.
+ */
+ const val EXTRA_NOTIFICATION_INTENT = "CAR_APP_NOTIFICATION_INTENT"
+
+ /** Checks whether the templates host is currently running on an emulator. */
+ private fun isConnectedToEmulator(): Boolean {
+ return Build.PRODUCT.contains("gcar") ||
+ Build.FINGERPRINT.contains("unknown") ||
+ Build.FINGERPRINT.contains("emu") ||
+ Build.DEVICE.contains("generic") ||
+ Build.DEVICE.contains("emu")
+ }
+
+ /** Checks whether the templates host has debug mode enabled */
+ fun isDebugEnabled(context: Context): Boolean {
+ return isConnectedToEmulator() ||
+ (0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java
new file mode 100644
index 0000000..d081426
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ConstraintsProviderImpl.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.res.TypedArray;
+import androidx.annotation.NonNull;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.constraints.ConstraintManager;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.distraction.constraints.ConstraintsProvider;
+import com.android.car.libraries.templates.host.di.UxreConfig;
+import com.android.car.libraries.templates.host.R;
+
+/** Provides different limit values for the car app. */
+public final class ConstraintsProviderImpl implements ConstraintsProvider {
+ private final Context mContext;
+ private final EventManager mEventManager;
+ private final UxreConfig mUxreConfig;
+ private final int mListMaxLength;
+ private final int mGridMaxLength;
+
+ private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener =
+ new UxRestrictionChangedListener();
+
+ private CarUxRestrictions mCurrentRestrictions;
+
+ @SuppressWarnings({"ResourceType"})
+ public ConstraintsProviderImpl(
+ Context context, EventManager eventManager, UxreConfig uxreConfig) {
+ mContext = context;
+ mEventManager = eventManager;
+ mUxreConfig = uxreConfig;
+
+ CarUxRestrictionsUtil carUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext);
+ carUxRestrictionsUtil.register(mListener);
+ mCurrentRestrictions = carUxRestrictionsUtil.getCurrentRestrictions();
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateListMaxLength, R.attr.templateGridMaxLength,
+ };
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mListMaxLength = ta.getInt(0, 6);
+ mGridMaxLength = ta.getInt(1, 6);
+ ta.recycle();
+ }
+
+ @Override
+ public int getContentLimit(int contentType) {
+
+ switch (contentType) {
+ case ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST:
+ case ConstraintManager.CONTENT_LIMIT_TYPE_LIST:
+ // TODO(b/186693941): For Android T and above, introduce an accurate UXRE API
+ // representing single-page limit so that we don't have to rely on the
+ // the getMaxCumulativeContentItems() API.
+ return mUxreConfig.getListMaxLength(mListMaxLength);
+ case ConstraintManager.CONTENT_LIMIT_TYPE_GRID:
+ // TODO(b/186693941): For Android T and above, introduce an accurate UXRE API
+ // representing single-page limit so that we don't have to rely on the
+ // the getMaxCumulativeContentItems() API.
+ return mUxreConfig.getGridMaxLength(mGridMaxLength);
+ case ConstraintManager.CONTENT_LIMIT_TYPE_PANE:
+ return mUxreConfig.getPaneMaxLength(
+ mContext.getResources().getInteger(R.integer.pane_max_length));
+ case ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST:
+ return mUxreConfig.getRouteListMaxLength(
+ mContext.getResources().getInteger(R.integer.route_list_max_length));
+ default:
+ throw new IllegalArgumentException("Unknown content type: " + contentType);
+ }
+ }
+
+ @Override
+ public int getTemplateStackMaxSize() {
+ return mUxreConfig.getTemplateStackMaxSize(
+ mContext.getResources().getInteger(R.integer.template_stack_max_size));
+ }
+
+ @Override
+ public boolean isKeyboardRestricted() {
+ return CarUxRestrictionsUtil.isRestricted(
+ CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD, mCurrentRestrictions);
+ }
+
+ @Override
+ public boolean isConfigRestricted() {
+ return CarUxRestrictionsUtil.isRestricted(
+ CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP, mCurrentRestrictions);
+ }
+
+ @Override
+ public boolean isFilteringRestricted() {
+ return CarUxRestrictionsUtil.isRestricted(
+ CarUxRestrictions.UX_RESTRICTIONS_NO_FILTERING, mCurrentRestrictions);
+ }
+
+ @Override
+ public int getStringCharacterLimit() {
+ return mCurrentRestrictions.getMaxRestrictedStringLength();
+ }
+
+ void update(CarUxRestrictions restrictions) {
+ mCurrentRestrictions = restrictions;
+ mEventManager.dispatchEvent(EventManager.EventType.CONSTRAINTS);
+ }
+
+ private class UxRestrictionChangedListener
+ implements CarUxRestrictionsUtil.OnUxRestrictionsChangedListener {
+
+ @Override
+ public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
+ update(carUxRestrictions);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt
new file mode 100644
index 0000000..39018d6
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/DebugOverlayHandlerImpl.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import androidx.car.app.model.TemplateWrapper
+import com.android.car.libraries.apphost.common.DebugOverlayHandler
+import java.util.LinkedHashMap
+
+/** The handler for the template-specific debug overlay. */
+class DebugOverlayHandlerImpl(private var isDebugOverlayActive: Boolean) : DebugOverlayHandler {
+ /** Using a linked hashmap here to keep track of debug entries' orders */
+ private val debugTextMap: HashMap<String, String> = LinkedHashMap()
+
+ private val builder = StringBuilder()
+
+ private var observer: DebugOverlayHandler.Observer? = null
+
+ override fun setActive(active: Boolean) {
+ isDebugOverlayActive = active
+ observer?.entriesUpdated()
+ }
+
+ override fun isActive(): Boolean {
+ return isDebugOverlayActive
+ }
+
+ override fun clearAllEntries() {
+ debugTextMap.clear()
+ }
+
+ override fun removeDebugOverlayEntry(debugKey: String) {
+ debugTextMap.remove(debugKey)
+ }
+
+ override fun updateDebugOverlayEntry(debugKey: String, debugOverlayText: String) {
+ debugTextMap[debugKey] = debugOverlayText
+ }
+
+ override fun getDebugOverlayText(): CharSequence {
+ builder.setLength(0)
+ var needsNewLineBefore = false
+ for (key in debugTextMap.keys) {
+ if (needsNewLineBefore) {
+ builder.append("\n")
+ }
+ builder.append(key).append(": ").append(debugTextMap[key])
+ needsNewLineBefore = true
+ }
+ return builder.toString()
+ }
+
+ override fun setObserver(observer: DebugOverlayHandler.Observer?) {
+ this.observer = observer
+ observer?.entriesUpdated()
+ }
+
+ override fun resetTemplateDebugOverlay(templateWrapper: TemplateWrapper) {
+ clearAllEntries()
+ updateDebugOverlayEntry(
+ /* debugKey= */ "Step",
+ Integer.toString(templateWrapper.currentTaskStep)
+ )
+ updateDebugOverlayEntry(
+ /* debugKey= */ "Template",
+ templateWrapper.template.javaClass.simpleName
+ )
+ observer?.entriesUpdated()
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java
new file mode 100644
index 0000000..40e8681
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ErrorHandlerImpl.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal;
+
+import android.content.ComponentName;
+import android.content.Context;
+import androidx.car.app.CarContext;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.CarHost;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.CarAppManager;
+import com.android.car.libraries.apphost.common.ErrorHandler;
+import com.android.car.libraries.apphost.common.ErrorMessageTemplateBuilder;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.AppHost;
+
+/**
+ * Handles error cases, allowing classes that do not handle ui to be able to display an error screen
+ * to the user.
+ */
+public class ErrorHandlerImpl implements ErrorHandler {
+ private final Context mContext;
+ private final ComponentName mAppName;
+ private final CarAppManager mCarAppManager;
+ private final HostResourceIds mHostResourceIdsImpl;
+
+ /** Returns a {@link ErrorHandlerImpl} to show an error screen */
+ public static ErrorHandlerImpl create(
+ Context context,
+ ComponentName appName,
+ CarAppManager carAppManager,
+ HostResourceIds hostResourceIds) {
+ return new ErrorHandlerImpl(context, appName, carAppManager, hostResourceIds);
+ }
+
+ private ErrorHandlerImpl(
+ Context context,
+ ComponentName appName,
+ CarAppManager carAppManager,
+ HostResourceIds hostResourceIds) {
+ mContext = context;
+ mAppName = appName;
+ mCarAppManager = carAppManager;
+ mHostResourceIdsImpl = hostResourceIds;
+ }
+
+ @Override
+ public void showError(CarAppError error) {
+ Throwable cause = error.getCause();
+ if (cause != null) {
+ if (error.logVerbose()) {
+ L.v(LogTags.TEMPLATE, cause, "Error: %s", error);
+ } else {
+ L.e(LogTags.TEMPLATE, cause, "Error: %s", error);
+ }
+ } else {
+ if (error.logVerbose()) {
+ L.v(LogTags.TEMPLATE, "Error: %s", error);
+ } else {
+ L.e(LogTags.TEMPLATE, "Error: %s", error);
+ }
+ }
+
+ MessageTemplate errorMessageTemplate =
+ new ErrorMessageTemplateBuilder(
+ mContext,
+ error,
+ mHostResourceIdsImpl,
+ // TODO(b/183145188): finish car app should not kill the host, just
+ // the activity
+ mCarAppManager::finishCarApp)
+ .build();
+
+ CarHost carHost = CarHostRepository.INSTANCE.get(mAppName);
+ AppHost apphost = (AppHost) carHost.getHostOrThrow(CarContext.APP_SERVICE);
+ apphost.getUIController().setTemplate(mAppName, TemplateWrapper.wrap(errorMessageTemplate));
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt
new file mode 100644
index 0000000..f49181f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputConfigImpl.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import com.android.car.libraries.apphost.input.InputConfig
+
+/** Manages the state of routing information in template apps. */
+class InputConfigImpl : InputConfig {
+ override fun hasTouchpadForUiNavigation(): Boolean {
+ // TODO(b/188454942): Retrieve the input configuration from AAOS system
+ return false
+ }
+
+ override fun hasTouch(): Boolean {
+ // TODO(b/188454942): Retrieve the input configuration from AAOS system
+ return true
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt
new file mode 100644
index 0000000..922cbc7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InputManagerImpl.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.inputmethod.EditorInfo
+import androidx.car.app.activity.renderer.IProxyInputConnection
+import com.android.car.libraries.apphost.input.CarEditable
+import com.android.car.libraries.apphost.input.CarEditableListener
+import com.android.car.libraries.apphost.input.InputManager
+import com.android.car.libraries.apphost.logging.LogTags
+
+/** The app specific implementation of [InputManager]. */
+class InputManagerImpl(private val listener: InputManagerListener) : InputManager {
+
+ /** A listener to be notified for input related events. */
+ interface InputManagerListener {
+ /* Should start the input, i.e. show soft keyboard */
+ fun onStartInput()
+
+ /* Should stop the input, i.e. hide soft keyboard */
+ fun onStopInput()
+
+ /*
+ * Update the text selection. Gets called whenever text selection changes on the
+ * [currentEditable].
+ */
+ fun onUpdateSelection(oldSelStart: Int, oldSelEnd: Int, newSelStart: Int, newSelEnd: Int)
+ }
+
+ private var currentEditable: CarEditable? = null
+ private val handler = Handler(Looper.getMainLooper())
+
+ private var stopInputRunnable = Runnable {
+ if (isInputActive) {
+ currentEditable = null
+ listener.onStopInput()
+ }
+ }
+
+ private val carEditableListener =
+ CarEditableListener { oldSelStart, oldSelEnd, newSelStart, newSelEnd ->
+ listener.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)
+ }
+
+ override fun startInput(view: CarEditable) {
+ currentEditable?.setCarEditableListener(null)
+ currentEditable = view
+ currentEditable?.setCarEditableListener(carEditableListener)
+
+ // Cancel any ongoing stop input to avoid jarring keyboard animations.
+ handler.removeCallbacks(stopInputRunnable)
+
+ listener.onStartInput()
+ }
+
+ override fun stopInput() {
+ currentEditable?.setCarEditableListener(null)
+
+ // Perform stop input with a delay to avoid jarring keyboard disappear+reappear animation
+ // when switching form one focusable to another.
+ handler.removeCallbacks(stopInputRunnable)
+ handler.postDelayed(stopInputRunnable, STOP_INPUT_DELAY_MILLIS)
+ }
+
+ override fun isValid() = true
+ override fun isInputActive() = currentEditable != null
+
+ fun onCreateInputConnection(editorInfo: EditorInfo): IProxyInputConnection? {
+ val currentEditable =
+ currentEditable
+ ?: run {
+ Log.d(LogTags.APP_HOST, "There is no focusable target selected.")
+ return null
+ }
+ val inputConnection =
+ currentEditable.onCreateInputConnection(editorInfo)
+ ?: run {
+ Log.d(LogTags.APP_HOST, "Failed to create input connection for editorInfo $editorInfo")
+ return null
+ }
+ return ProxyInputConnection(inputConnection, editorInfo)
+ }
+
+ companion object {
+ const val STOP_INPUT_DELAY_MILLIS = 100L
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt
new file mode 100644
index 0000000..edd4d00
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/InsetsListener.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.graphics.Insets
+import android.os.Build
+import android.view.WindowInsets
+import androidx.car.app.activity.renderer.IInsetsListener
+import androidx.car.app.utils.ThreadUtils
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.view.AbstractTemplateView
+
+/** Handles window insets from the car app. */
+class InsetsListener(private val templateView: AbstractTemplateView) : IInsetsListener.Stub() {
+ override fun onInsetsChanged(insets: Insets) {
+ ThreadUtils.runOnMain {
+ L.i(LogTags.APP_HOST) { "Received insets: $insets" }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ templateView.windowInsets =
+ WindowInsets.Builder()
+ .setInsets(WindowInsets.Type.systemBars() or WindowInsets.Type.ime(), insets)
+ .build()
+ } else {
+ templateView.windowInsets = WindowInsets.Builder().setSystemWindowInsets(insets).build()
+ }
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt
new file mode 100644
index 0000000..c6b2272
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/LogUtil.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ComponentName
+import android.content.Context
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.TelemetryEvent
+import com.android.car.libraries.apphost.logging.TelemetryHandler
+import com.android.car.libraries.templates.host.di.TelemetryHandlerFactory
+
+/**
+ * Holds static telemetry logging methods for common usage in the host. These methods should only be
+ * used by service level component, e.g. [ClusterIconContentProvider]. Car app level logging should
+ * use [TelemetryHandler] from TemplateContext
+ */
+class LogUtil(private val telemetryHandler: TelemetryHandler) {
+
+ companion object {
+ private lateinit var instance: LogUtil
+
+ fun init(telemetryHandlerFactory: TelemetryHandlerFactory, applicationContext: Context?) {
+ checkNotNull(applicationContext)
+ instance =
+ LogUtil(
+ telemetryHandlerFactory.create(
+ applicationContext,
+ ComponentName(applicationContext, LogUtil::class.java)
+ )
+ )
+ }
+
+ fun log(uiAction: TelemetryEvent.UiAction) {
+ log(TelemetryEvent.newBuilder(uiAction))
+ }
+
+ private fun log(builder: TelemetryEvent.Builder) {
+ if (!this::instance.isInitialized) {
+ L.d(LogTags.APP_HOST) { "CommonLogger is not initialized" }
+ return
+ }
+ instance.telemetryHandler.logCarAppTelemetry(builder)
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt
new file mode 100644
index 0000000..443559b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationCoordinator.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.car.Car
+import android.car.CarAppFocusManager
+import android.car.CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION as APP_TYPE_NAVIGATION
+import android.car.CarAppFocusManager.OnAppFocusOwnershipCallback as FocusCallback
+import android.car.cluster.navigation.NavigationState
+import android.car.navigation.CarNavigationStatusManager
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.car.app.navigation.model.Trip
+import androidx.core.os.bundleOf
+import com.android.car.libraries.apphost.common.CarAppPackageInfo
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import com.android.car.libraries.templates.host.R
+
+
+/**
+ * Coordinate navigation and focus control between all Template Apps. Apps can start sending
+ * navigation events after requesting focus, but it's possible some events will be dropped while
+ * focus is being obtained.
+ */
+class NavigationCoordinator
+private constructor(
+ carAppFocusManagerProvider: () -> CarAppFocusManager?,
+ carNavStatusManagerProvider: () -> CarNavigationStatusManager?,
+ private val shouldShareNavState: Boolean
+) : FocusCallback {
+ /**
+ * Instances of this interface will be compared against each other for equality. Either make sure
+ * you are sending the same instance per app, or implement [equals] to account for this.
+ */
+ interface NavAppFocusOwner {
+ val packageInfo: CarAppPackageInfo
+ fun onFocusLost()
+ }
+
+ private val focusManager: CarAppFocusManager? by lazy(carAppFocusManagerProvider)
+ private val navigationManager: CarNavigationStatusManager? by lazy(carNavStatusManagerProvider)
+
+ private val _navigationState = MutableStateFlow<HostNavState>(HostNavState.NotNavigating)
+ val navigationState = _navigationState.asStateFlow()
+
+ /** Whether or not the Host has navigation focus currently */
+ private val isOwningFocus = AtomicBoolean(false)
+ private var currentNavApp: NavAppFocusOwner? = null
+
+ /**
+ * Apps must request Focus (see [requestAppFocus]) before sending navigation events. Also, there's
+ * a chance that some events will be dropped on the floor while focus is being obtained.
+ */
+ fun sendNavigationStateChange(
+ navApp: NavAppFocusOwner,
+ // TODO(b/206694446): Only accept Trip and do the Proto conversion here.
+ navigationState: NavigationState.NavigationStateProto,
+ templateContext: TemplateContext,
+ trip: Trip? = null
+ ) =
+ synchronized(this) {
+ if (isFocused(navApp)) {
+ _navigationState.value =
+ if (trip != null) HostNavState.Navigating(trip, templateContext, navApp.packageInfo)
+ else HostNavState.NotNavigating
+ if (shouldShareNavState) {
+ navigationManager?.sendNavigationStateChange(navigationState.asBundle())
+ }
+ } else {
+ L.w(LogTags.NAVIGATION) {
+ val packageName = navApp.packageInfo.componentName.packageName
+ "Package $packageName is trying to send NavigationState updates without owning focus"
+ }
+ }
+ }
+
+ /**
+ * Note that a result of [CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED] does not mean you have
+ * focus yet. This call is asynchronous and
+ * [CarAppFocusManager.OnAppFocusOwnershipCallback.onAppFocusOwnershipGranted] will be called when
+ * focus is granted for the app.
+ */
+ fun requestAppFocus(navApp: NavAppFocusOwner) =
+ synchronized(this) {
+ val focusManager =
+ focusManager
+ ?: run {
+ L.w(LogTags.NAVIGATION) {
+ "Couldn't obtain focusManager. Are you missing a permission?"
+ }
+ navApp.onFocusLost()
+ return
+ }
+
+ if (navApp != currentNavApp) {
+ currentNavApp?.onFocusLost()
+ currentNavApp = navApp
+ }
+
+ if (!isOwningFocus.get()) {
+ // request focus from system
+ val result = focusManager.requestAppFocus(APP_TYPE_NAVIGATION, this)
+ if (result == CarAppFocusManager.APP_FOCUS_REQUEST_FAILED) {
+ onAppFocusOwnershipLost(APP_TYPE_NAVIGATION)
+ }
+ } else {
+ clearNavState()
+ }
+ }
+
+ /** Notify that [navApp] is done navigation and no longer requires focus. */
+ fun abandonAppFocus(navApp: NavAppFocusOwner) =
+ synchronized(this) {
+ if (isFocused(navApp)) {
+ onAppFocusOwnershipLost(APP_TYPE_NAVIGATION)
+ focusManager?.abandonAppFocus(this, APP_TYPE_NAVIGATION)
+ _navigationState.value = HostNavState.NotNavigating
+ }
+ }
+
+ override fun onAppFocusOwnershipLost(appType: Int) =
+ synchronized(this) {
+ L.d(LogTags.NAVIGATION) { "Host focus Lost" }
+ isOwningFocus.set(false)
+ currentNavApp?.onFocusLost()
+ currentNavApp = null
+ }
+
+ override fun onAppFocusOwnershipGranted(appType: Int) =
+ synchronized(this) {
+ L.d(LogTags.NAVIGATION) { "Host focus granted" }
+ isOwningFocus.set(true)
+ if (!shouldShareNavState) {
+ L.d(LogTags.NAVIGATION, "NavState data will not sent to system.")
+ clearNavState()
+ }
+ }
+
+ private fun clearNavState() {
+ val emptyNavState = NavigationState.NavigationStateProto.getDefaultInstance()
+ navigationManager?.sendNavigationStateChange(emptyNavState.asBundle())
+ }
+
+ /** returns whether or not [navApp] has focus currently */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ fun isFocused(navApp: NavAppFocusOwner): Boolean =
+ synchronized(this) { isOwningFocus.get() && this.currentNavApp == navApp }
+
+ companion object {
+ private lateinit var instance: NavigationCoordinator
+
+ fun getInstance(context: Context): NavigationCoordinator {
+ if (!this::instance.isInitialized) {
+ val themeAttrs = intArrayOf(R.attr.templateSendNavStateToSystem)
+ val ta = context.obtainStyledAttributes(themeAttrs)
+ val shouldShareNavState =
+ ta.getBoolean(0, context.resources.getBoolean(R.bool.send_navstates_to_system))
+ ta.recycle()
+ instance =
+ NavigationCoordinator(
+ carAppFocusManagerProvider = { context.getCarService(Car.APP_FOCUS_SERVICE) },
+ carNavStatusManagerProvider = { context.getCarService(Car.CAR_NAVIGATION_SERVICE) },
+ shouldShareNavState
+ )
+ }
+ return instance
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ fun testInstance(
+ carAppFocusManager: CarAppFocusManager,
+ carNavStatusManager: CarNavigationStatusManager
+ ) = NavigationCoordinator({ carAppFocusManager }, { carNavStatusManager }, true)
+
+ private inline fun <reified T> Context.getCarService(serviceName: String): T? {
+ val car: Car? = Car.createCar(this)
+ if (car == null) {
+ L.e(LogTags.NAVIGATION) { "Nav state disabled: Unable to connect to CarService" }
+ return null
+ }
+ return runCatching { car.getCarManager(serviceName) as T? }
+ .onSuccess { L.d(LogTags.NAVIGATION) { "Obtained service: $serviceName" } }
+ .onFailure {
+ L.e(LogTags.NAVIGATION, it) {
+ "Nav state disabled: Unable to obtain access to $serviceName."
+ }
+ }
+ .getOrNull()
+ }
+ }
+}
+
+private const val NAVIGATION_STATE_PROTO_BUNDLE_KEY = "navstate2"
+
+private fun NavigationState.NavigationStateProto.asBundle() =
+ bundleOf(NAVIGATION_STATE_PROTO_BUNDLE_KEY to this.toByteArray())
+
+sealed class HostNavState {
+ object NotNavigating : HostNavState()
+ class Navigating(
+ val trip: Trip,
+ val templateContext: TemplateContext,
+ val packageInfo: CarAppPackageInfo
+ ) : HostNavState()
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt
new file mode 100644
index 0000000..80d68fc
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateCallbackImpl.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.car.cluster.navigation.NavigationState
+import androidx.car.app.navigation.model.Trip
+import com.android.car.libraries.apphost.common.CarAppPackageInfo
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.nav.NavigationHost
+import com.android.car.libraries.apphost.nav.NavigationStateCallback
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+import com.android.car.libraries.templates.host.R
+
+/** Handles navigation state change events from [NavigationHost]. */
+class NavigationStateCallbackImpl
+private constructor(
+ private val templateContext: TemplateContext,
+ private val navigationStateConverter: NavigationStateConverter
+) : NavigationStateCallback {
+ private var onNavigationStopRunnable: Runnable? = null
+
+ private val packageInfo = templateContext.carAppPackageInfo
+ private val navigationCoordinator by lazy { NavigationCoordinator.getInstance(templateContext) }
+
+ private val navApp =
+ object : NavigationCoordinator.NavAppFocusOwner {
+ override val packageInfo: CarAppPackageInfo
+ get() = templateContext.carAppPackageInfo
+
+ override fun onFocusLost() {
+ onNavigationStopRunnable?.run()
+ }
+ }
+
+ override fun onUpdateTrip(trip: Trip): Boolean {
+ L.v(LogTags.NAVIGATION) { "onUpdateTrip $packageInfo" }
+ CoroutineScope(Dispatchers.Default).launch {
+ // Conversion shouldn't take a long time. If it hangs for too long, just kill it.
+ val timeMillis =
+ templateContext
+ .resources
+ .getInteger(R.integer.cluster_trip_to_navstate_conversion_timeout_millis)
+ .toLong()
+ val navigationState =
+ withTimeout(timeMillis) { navigationStateConverter.tripToNavigationState(trip) }
+ navigationCoordinator.sendNavigationStateChange(
+ navApp,
+ navigationState,
+ templateContext,
+ trip
+ )
+ }
+ return true
+ }
+
+ override fun onNavigationStarted(onNavigationStopRunnable: Runnable) {
+ L.v(LogTags.NAVIGATION) { "onNavigationStarted ${templateContext.carAppPackageInfo}" }
+
+ this.onNavigationStopRunnable = onNavigationStopRunnable
+ if (templateContext.carHostConfig.isClusterEnabled) {
+ navigationCoordinator.requestAppFocus(navApp)
+ }
+ }
+
+ override fun onNavigationEnded() {
+ L.v(LogTags.NAVIGATION) { "onNavigationEnded ${templateContext.carAppPackageInfo}" }
+
+ // Remove directions from cluster
+ navigationCoordinator.sendNavigationStateChange(
+ navApp,
+ NavigationState.NavigationStateProto.getDefaultInstance(),
+ templateContext
+ )
+
+ navigationCoordinator.abandonAppFocus(navApp)
+
+ onNavigationStopRunnable = null
+ }
+
+ companion object {
+ fun create(templateContext: TemplateContext): NavigationStateCallback {
+ return NavigationStateCallbackImpl(
+ templateContext,
+ NavigationStateConverterImpl(templateContext)
+ )
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt
new file mode 100644
index 0000000..2510f4d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.car.cluster.navigation.NavigationState
+import android.car.navigation.CarNavigationStatusManager
+import androidx.car.app.navigation.NavigationManager
+import androidx.car.app.navigation.model.Trip
+
+/**
+ * Convert a [Trip] (from [NavigationManager]) to a [NavigationState.NavigationStateProto] that the
+ * Cluster can parse and display (via [CarNavigationStatusManager.sendNavigationStateChange]).
+ */
+interface NavigationStateConverter {
+ suspend fun tripToNavigationState(trip: Trip): NavigationState.NavigationStateProto
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt
new file mode 100644
index 0000000..2ff9d6a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/NavigationStateConverterImpl.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.car.cluster.navigation.CueKt.cueElement
+import android.car.cluster.navigation.LaneKt.laneDirection
+import android.car.cluster.navigation.NavigationState
+import android.car.cluster.navigation.NavigationState.Lane.LaneDirection.Shape
+import android.car.cluster.navigation.NavigationState.Maneuver.Type as ManeuverType
+import android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.NORMAL
+import android.car.cluster.navigation.NavigationState.NavigationStateProto.ServiceStatus.REROUTING
+import android.car.cluster.navigation.cue
+import android.car.cluster.navigation.destination
+import android.car.cluster.navigation.distance
+import android.car.cluster.navigation.lane
+import android.car.cluster.navigation.maneuver
+import android.car.cluster.navigation.navigationStateProto
+import android.car.cluster.navigation.road
+import android.car.cluster.navigation.step
+import android.car.cluster.navigation.timestamp
+import android.car.navigation.CarNavigationStatusManager
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import androidx.car.app.model.CarIcon
+import androidx.car.app.model.CarText
+import androidx.car.app.model.Distance
+import androidx.car.app.navigation.NavigationManager
+import androidx.car.app.navigation.model.Lane
+import androidx.car.app.navigation.model.LaneDirection
+import androidx.car.app.navigation.model.Maneuver
+import androidx.car.app.navigation.model.TravelEstimate
+import androidx.car.app.navigation.model.Trip
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.graphics.drawable.toBitmap
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.view.common.DateTimeUtils
+import com.android.car.libraries.apphost.view.common.DistanceUtils
+import com.android.car.libraries.apphost.view.common.ImageUtils
+import com.android.car.libraries.apphost.view.common.ImageViewParams
+import java.io.ByteArrayOutputStream
+import java.time.Duration
+import java.util.UUID
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Convert a [Trip] (from [NavigationManager]) to a [NavigationState.NavigationStateProto] that the
+ * Cluster can parse and display (via [CarNavigationStatusManager.sendNavigationStateChange]).
+ */
+class NavigationStateConverterImpl(private val templateContext: TemplateContext) :
+ NavigationStateConverter {
+
+ /** If the Provider gave an error once, we don't want to keep hitting it */
+ private var skipIcons = false
+
+ override suspend fun tripToNavigationState(trip: Trip) = coroutineScope {
+ navigationStateProto {
+ serviceStatus = if (trip.isLoading) REROUTING else NORMAL
+ trip.currentRoad?.let { currentRoad = road { name = it.toString() } }
+ steps += trip.getNavigationStateSteps()
+ destinations += trip.getNavigationStateDestinations()
+ }
+ }
+
+ private fun Trip.getNavigationStateDestinations(): List<NavigationState.Destination> =
+ destinations.zip(destinationTravelEstimates).map { (destination, estimate) ->
+ destination {
+ destination.name?.toString()?.let { title = it }
+ destination.address?.toString()?.let { address = it }
+ distance = estimate.toNavStateDistance()
+ estimate.arrivalTimeAtDestination?.timeSinceEpochMillis?.let { epochMillis ->
+ estimatedTimeAtArrival = timestamp { seconds = epochMillis / 1000 }
+ }
+ formattedDurationUntilArrival = estimate.getFormattedRemainingDuration(templateContext)
+ estimate.arrivalTimeAtDestination?.zoneShortName?.let { zoneId = it }
+ }
+ }
+
+ private fun Trip.getNavigationStateSteps(): List<NavigationState.Step> {
+ return steps.zip(stepTravelEstimates).map { (step, estimate) ->
+ step {
+ step.maneuver?.let { maneuver = it.toNavStateManeuver() }
+ distance = estimate.toNavStateDistance()
+ step.cue?.let { cue = it.toNavStateCue() }
+ lanes += step.lanes.map { it.toNavStateLane() }
+ step.lanesImage?.toImageReference()?.let { lanesImage = it }
+ }
+ }
+ }
+
+ private fun Lane.toNavStateLane() = lane {
+ laneDirections +=
+ directions.map { laneDirection ->
+ laneDirection {
+ shape = laneDirection.shape.toNavStateShape()
+ isHighlighted = laneDirection.isRecommended
+ }
+ }
+ }
+
+ private fun Int.toNavStateShape() =
+ when (this) {
+ LaneDirection.SHAPE_UNKNOWN -> Shape.UNKNOWN
+ LaneDirection.SHAPE_STRAIGHT -> Shape.STRAIGHT
+ LaneDirection.SHAPE_SLIGHT_LEFT -> Shape.SLIGHT_LEFT
+ LaneDirection.SHAPE_SLIGHT_RIGHT -> Shape.SLIGHT_RIGHT
+ LaneDirection.SHAPE_NORMAL_LEFT -> Shape.NORMAL_LEFT
+ LaneDirection.SHAPE_NORMAL_RIGHT -> Shape.NORMAL_RIGHT
+ LaneDirection.SHAPE_SHARP_LEFT -> Shape.SHARP_LEFT
+ LaneDirection.SHAPE_SHARP_RIGHT -> Shape.SHARP_RIGHT
+ else -> Shape.UNRECOGNIZED
+ }
+
+ private fun CarText.toNavStateCue() = cue {
+ val cueText = this@toNavStateCue.toString()
+ alternateText = cueText
+ elements += cueElement { text = cueText }
+ }
+
+ private fun TravelEstimate.toNavStateDistance() = distance {
+ meters = DistanceUtils.getMeters(remainingDistance)
+ remainingDistance?.let {
+ displayValue = DistanceUtils.convertDistanceToDisplayStringNoUnit(templateContext, it)
+ }
+ displayUnits =
+ when (remainingDistance?.displayUnit) {
+ Distance.UNIT_METERS -> NavigationState.Distance.Unit.METERS
+ Distance.UNIT_KILOMETERS, Distance.UNIT_KILOMETERS_P1 ->
+ NavigationState.Distance.Unit.KILOMETERS
+ Distance.UNIT_MILES, Distance.UNIT_MILES_P1 -> NavigationState.Distance.Unit.MILES
+ Distance.UNIT_FEET -> NavigationState.Distance.Unit.FEET
+ Distance.UNIT_YARDS -> NavigationState.Distance.Unit.YARDS
+ else -> NavigationState.Distance.Unit.UNKNOWN
+ }
+ }
+
+ /**
+ * Only Resource/Bitmap icons are supported. Will return [null] for all other types of [CarIcon]
+ */
+ private fun CarIcon.toImageReference(): NavigationState.ImageReference? {
+ if (skipIcons) return null
+
+ val iconId = hash(this).toString()
+
+ // Don't extract a Drawable unless needed
+ ClusterIconContentProvider.queryIconData(iconId, templateContext)?.let {
+ (contentUri, aspectRatio) ->
+ return NavigationState.ImageReference.newBuilder()
+ .setContentUri(contentUri)
+ .setAspectRatio(aspectRatio)
+ .build()
+ }
+
+ // No cache for icon, get Drawable and cache it
+ val drawable =
+ ImageUtils.getIconDrawable(templateContext, this, ImageViewParams.DEFAULT)
+ ?: run {
+ L.d(LogTags.NAVIGATION) {
+ "Couldn't obtain Drawable from CarIcon (uri icons not supported): $this"
+ }
+ return null
+ }
+
+ val aspectRatio =
+ if (drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0) {
+ drawable.intrinsicWidth.toDouble() / drawable.intrinsicHeight.toDouble()
+ } else {
+ L.w(LogTags.NAVIGATION) {
+ "Drawable has no intrinsic dimensions aspect ratio. carIcon=$this"
+ }
+ return null
+ }
+
+ val contentUri =
+ runCatching {
+ val bytes = drawable.toByteArray()
+ ClusterIconContentProvider.addToCache(iconId, bytes, templateContext)
+ }
+ .onFailure {
+ skipIcons = true
+ L.w(LogTags.NAVIGATION, it) {
+ "Failed to cache icon in provider." +
+ " Disabling cluster icons for ${templateContext.appPackageName}"
+ }
+ }
+ .getOrNull()
+ ?.toString()
+ ?: return null
+
+ return NavigationState.ImageReference.newBuilder()
+ .setContentUri(contentUri)
+ .setAspectRatio(aspectRatio)
+ .build()
+ }
+
+ private fun Drawable.toByteArray(): ByteArray {
+ val bitmap = this.toBitmap()
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ return stream.toByteArray()
+ }
+
+ private fun hash(icon: CarIcon): Int {
+ return when (icon.icon?.type) {
+ IconCompat.TYPE_RESOURCE, IconCompat.TYPE_URI, null -> icon.hashCode()
+ else -> {
+ // For any iconCompat type that exists and isn't URI or Resource, we don't really
+ // know how to tell if two instances represent the same set of pixels.
+ // So we just consider them unique.
+ UUID.randomUUID().hashCode()
+ }
+ }
+ }
+
+ private fun Maneuver.toNavStateManeuver() = maneuver {
+ val maneuver = this@toNavStateManeuver
+ type = maneuver.getNavStateType()
+ roundaboutExitNumber = maneuver.roundaboutExitNumber
+ maneuver.icon?.toImageReference()?.let { icon = it }
+ }
+
+ private fun Maneuver.getNavStateType(): ManeuverType =
+ when (type) {
+ Maneuver.TYPE_UNKNOWN -> ManeuverType.UNKNOWN
+ Maneuver.TYPE_DEPART -> ManeuverType.DEPART
+ Maneuver.TYPE_NAME_CHANGE -> ManeuverType.NAME_CHANGE
+ Maneuver.TYPE_KEEP_LEFT -> ManeuverType.KEEP_LEFT
+ Maneuver.TYPE_KEEP_RIGHT -> ManeuverType.KEEP_RIGHT
+ Maneuver.TYPE_TURN_SLIGHT_LEFT -> ManeuverType.TURN_SLIGHT_LEFT
+ Maneuver.TYPE_TURN_SLIGHT_RIGHT -> ManeuverType.TURN_SLIGHT_RIGHT
+ Maneuver.TYPE_TURN_NORMAL_LEFT -> ManeuverType.TURN_NORMAL_LEFT
+ Maneuver.TYPE_TURN_NORMAL_RIGHT -> ManeuverType.TURN_NORMAL_RIGHT
+ Maneuver.TYPE_TURN_SHARP_LEFT -> ManeuverType.TURN_SHARP_LEFT
+ Maneuver.TYPE_TURN_SHARP_RIGHT -> ManeuverType.TURN_SHARP_RIGHT
+ Maneuver.TYPE_U_TURN_LEFT -> ManeuverType.U_TURN_LEFT
+ Maneuver.TYPE_U_TURN_RIGHT -> ManeuverType.U_TURN_RIGHT
+ Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT -> ManeuverType.ON_RAMP_SLIGHT_LEFT
+ Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT -> ManeuverType.ON_RAMP_SLIGHT_RIGHT
+ Maneuver.TYPE_ON_RAMP_NORMAL_LEFT -> ManeuverType.ON_RAMP_NORMAL_LEFT
+ Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT -> ManeuverType.ON_RAMP_NORMAL_RIGHT
+ Maneuver.TYPE_ON_RAMP_SHARP_LEFT -> ManeuverType.ON_RAMP_SHARP_LEFT
+ Maneuver.TYPE_ON_RAMP_SHARP_RIGHT -> ManeuverType.ON_RAMP_SHARP_RIGHT
+ Maneuver.TYPE_ON_RAMP_U_TURN_LEFT -> ManeuverType.ON_RAMP_U_TURN_LEFT
+ Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT -> ManeuverType.ON_RAMP_U_TURN_RIGHT
+ Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT -> ManeuverType.OFF_RAMP_SLIGHT_LEFT
+ Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> ManeuverType.OFF_RAMP_SLIGHT_RIGHT
+ Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT -> ManeuverType.OFF_RAMP_NORMAL_LEFT
+ Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT -> ManeuverType.OFF_RAMP_NORMAL_RIGHT
+ Maneuver.TYPE_FORK_LEFT -> ManeuverType.FORK_LEFT
+ Maneuver.TYPE_FORK_RIGHT -> ManeuverType.FORK_RIGHT
+ Maneuver.TYPE_MERGE_LEFT -> ManeuverType.MERGE_LEFT
+ Maneuver.TYPE_MERGE_RIGHT -> ManeuverType.MERGE_RIGHT
+ Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED -> ManeuverType.MERGE_SIDE_UNSPECIFIED
+ Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW
+ Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE -> {
+ when (roundaboutExitAngle) {
+ in 6..45 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_LEFT
+ in 46..135 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_LEFT
+ in 136..170 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_LEFT
+ in 171..189 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_STRAIGHT
+ in 190..224 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SLIGHT_RIGHT
+ in 225..314 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_NORMAL_RIGHT
+ in 315..354 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_SHARP_RIGHT
+ in 1..5, in 355..360 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW_U_TURN
+ else -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW
+ }
+ }
+ Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW
+ Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE -> {
+ when (roundaboutExitAngle) {
+ in 6..45 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_RIGHT
+ in 46..135 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_RIGHT
+ in 136..170 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_RIGHT
+ in 171..189 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_STRAIGHT
+ in 190..224 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SLIGHT_LEFT
+ in 225..314 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_NORMAL_LEFT
+ in 315..354 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_SHARP_LEFT
+ in 1..5, in 355..360 -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW_U_TURN
+ else -> ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW
+ }
+ }
+ Maneuver.TYPE_STRAIGHT -> ManeuverType.STRAIGHT
+ Maneuver.TYPE_FERRY_BOAT -> ManeuverType.FERRY_BOAT
+ Maneuver.TYPE_FERRY_TRAIN -> ManeuverType.FERRY_TRAIN
+ Maneuver.TYPE_DESTINATION -> ManeuverType.DESTINATION
+ Maneuver.TYPE_DESTINATION_STRAIGHT -> ManeuverType.DESTINATION_STRAIGHT
+ Maneuver.TYPE_DESTINATION_LEFT -> ManeuverType.DESTINATION_LEFT
+ Maneuver.TYPE_DESTINATION_RIGHT -> ManeuverType.DESTINATION_RIGHT
+ Maneuver.TYPE_ROUNDABOUT_ENTER_CW, Maneuver.TYPE_ROUNDABOUT_EXIT_CW ->
+ ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CW
+ Maneuver.TYPE_ROUNDABOUT_ENTER_CCW, Maneuver.TYPE_ROUNDABOUT_EXIT_CCW ->
+ ManeuverType.ROUNDABOUT_ENTER_AND_EXIT_CCW
+ Maneuver.TYPE_FERRY_BOAT_LEFT,
+ Maneuver.TYPE_FERRY_BOAT_RIGHT,
+ Maneuver.TYPE_FERRY_TRAIN_LEFT,
+ Maneuver.TYPE_FERRY_TRAIN_RIGHT -> ManeuverType.FERRY_TRAIN
+ else -> ManeuverType.UNKNOWN
+ }
+
+ private fun TravelEstimate.getFormattedRemainingDuration(templateContext: TemplateContext) =
+ if (remainingTimeSeconds == TravelEstimate.REMAINING_TIME_UNKNOWN) ""
+ else
+ DateTimeUtils.formatDurationString(templateContext, Duration.ofSeconds(remainingTimeSeconds))
+}
+
+/** Just a convenience to get the Client package name */
+private val TemplateContext.appPackageName
+ get() = carAppPackageInfo.componentName.packageName
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt
new file mode 100644
index 0000000..a7f0b59
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ProxyInputConnection.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.KeyEvent
+import android.view.inputmethod.CompletionInfo
+import android.view.inputmethod.CorrectionInfo
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.ExtractedText
+import android.view.inputmethod.ExtractedTextRequest
+import android.view.inputmethod.InputConnection
+import androidx.car.app.activity.renderer.IProxyInputConnection
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.FutureTask
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/**
+ * Proxies an [InputConnection] across a binder interface. All InputConnection calls are made on the
+ * main thread.
+ *
+ * Please note that once an InputConnection is invalid, it never becomes valid again. An invalid
+ * InputConnection simply ignores calls that are made to it.
+ *
+ * Some InputConnection methods simply return a boolean indicating whether the input connection is
+ * still valid. For these methods, we run the action and update the validity of the input connection
+ * asynchronously - there's no need to synchronize this so long as the action happens on the main
+ * thread. For all other methods where the return value matters, we block on the Binder thread until
+ * the value has been provided on the main thread.
+ */
+class ProxyInputConnection(
+ private val inputConnection: InputConnection,
+ private val editorInfo: EditorInfo
+) : IProxyInputConnection.Stub() {
+ @Volatile private var inputConnectionValid = true
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun getTextBeforeCursor(n: Int, flags: Int): CharSequence? {
+ return runOnMainAndAwaitResult(null) { inputConnection.getTextBeforeCursor(n, flags) }
+ }
+
+ override fun getTextAfterCursor(n: Int, flags: Int): CharSequence? {
+ return runOnMainAndAwaitResult(null) { inputConnection.getTextAfterCursor(n, flags) }
+ }
+
+ override fun getSelectedText(flags: Int): CharSequence? {
+ return runOnMainAndAwaitResult(null) { inputConnection.getSelectedText(flags) }
+ }
+
+ override fun getCursorCapsMode(reqModes: Int): Int {
+ return runOnMainAndAwaitResult(0) { inputConnection.getCursorCapsMode(reqModes) }
+ }
+
+ override fun beginBatchEdit(): Boolean {
+ return runOnMainAndAwaitResult(false) { inputConnection.beginBatchEdit() }
+ }
+
+ override fun endBatchEdit(): Boolean {
+ return runOnMainAndAwaitResult(false) { inputConnection.endBatchEdit() }
+ }
+
+ override fun sendKeyEvent(event: KeyEvent): Boolean {
+ return runOnMainAndAwaitResult(false) { inputConnection.sendKeyEvent(event) }
+ }
+
+ override fun commitCorrection(correctionInfo: CorrectionInfo): Boolean {
+ return runOnMainAndAwaitResult(false) { inputConnection.commitCorrection(correctionInfo) }
+ }
+
+ override fun commitCompletion(text: CompletionInfo?): Boolean {
+ return runOnMainAndAwaitResult(false) { inputConnection.commitCompletion(text) }
+ }
+
+ override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText? {
+ return runOnMainAndAwaitResult(null) { inputConnection.getExtractedText(request, flags) }
+ }
+
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
+ return runOnMainAndUpdateValidity {
+ inputConnection.deleteSurroundingText(beforeLength, afterLength)
+ }
+ }
+
+ override fun setComposingText(text: CharSequence, newCursorPosition: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.setComposingText(text, newCursorPosition) }
+ }
+
+ override fun setComposingRegion(start: Int, end: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.setComposingRegion(start, end) }
+ }
+
+ override fun finishComposingText(): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.finishComposingText() }
+ }
+
+ override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.commitText(text, newCursorPosition) }
+ }
+
+ override fun setSelection(start: Int, end: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.setSelection(start, end) }
+ }
+
+ override fun performEditorAction(editorAction: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.performEditorAction(editorAction) }
+ }
+
+ override fun performContextMenuAction(id: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.performContextMenuAction(id) }
+ }
+
+ override fun clearMetaKeyStates(states: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.clearMetaKeyStates(states) }
+ }
+
+ override fun reportFullscreenMode(enabled: Boolean): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.reportFullscreenMode(enabled) }
+ }
+
+ override fun performPrivateCommand(action: String, data: Bundle): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.performPrivateCommand(action, data) }
+ }
+
+ override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
+ return runOnMainAndUpdateValidity { inputConnection.requestCursorUpdates(cursorUpdateMode) }
+ }
+
+ override fun closeConnection() {
+ runOnMainDirect {
+ inputConnection.closeConnection()
+ inputConnectionValid = false
+ }
+ }
+
+ override fun getEditorInfo(): EditorInfo {
+ return editorInfo
+ }
+
+ /**
+ * Runs code on the main thread, and blocks for the result on another.
+ *
+ * @param defaultResult the value to return if event timeout or if the connection is invalid.
+ * @param action the code to execute, that should return a result.
+ * @return the value produced by [action].
+ */
+ private fun <T> runOnMainAndAwaitResult(defaultResult: T, action: Callable<T>): T {
+ if (!inputConnectionValid) {
+ return defaultResult
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ return try {
+ action.call()
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+ val futureTask = FutureTask(action)
+ handler.post(futureTask)
+ return try {
+ futureTask[ASYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS]
+ } catch (e: ExecutionException) {
+ throw RuntimeException(e)
+ } catch (e: InterruptedException) {
+ defaultResult
+ } catch (e: TimeoutException) {
+ defaultResult
+ }
+ }
+
+ /**
+ * Runs code on the main thread, and updates the [inputConnectionValid] with the result.
+ *
+ * @param action the code to execute, that should return a boolean indicating the validity of the
+ * connection.
+ * @return the value produced by [action]
+ */
+ private fun runOnMainAndUpdateValidity(action: Callable<Boolean>): Boolean {
+ if (!inputConnectionValid) {
+ return false
+ }
+
+ runOnMainDirect {
+ try {
+ inputConnectionValid = action.call()
+ } catch (ex: Exception) {
+ inputConnectionValid = false
+ throw RuntimeException("Input connection action failed", ex)
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Runs code on the main thread. Does not jump thread if already on the main thread.
+ *
+ * @param action the code to execute.
+ */
+ private fun runOnMainDirect(action: Runnable) {
+ if (Looper.myLooper() == handler.looper) {
+ action.run()
+ } else {
+ handler.post(action)
+ }
+ }
+
+ companion object {
+ private const val ASYNC_TIMEOUT_MILLIS: Long = 1000
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt
new file mode 100644
index 0000000..305ad38
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RendererCallback.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.os.Handler
+import android.os.Looper
+import android.view.inputmethod.EditorInfo
+import androidx.car.app.CarContext
+import androidx.car.app.activity.renderer.IProxyInputConnection
+import androidx.car.app.activity.renderer.IRendererCallback
+import androidx.car.app.utils.ThreadUtils
+import androidx.lifecycle.Lifecycle
+import com.android.car.libraries.apphost.CarHost
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.template.AppHost
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.FutureTask
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+/** Handles events from the car app. */
+class RendererCallback(private val carHost: CarHost, private val inputManager: InputManagerImpl) :
+ IRendererCallback.Stub() {
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onBackPressed() {
+ val appHost = carHost.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.onBackPressed()
+ }
+
+ override fun onCreate() {
+ ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_CREATE) }
+ }
+
+ override fun onStart() {
+ ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_START) }
+ }
+
+ override fun onResume() {
+ ThreadUtils.runOnMain { carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_RESUME) }
+ }
+
+ override fun onPause() {
+ ThreadUtils.runOnMain {
+ try {
+ carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ } catch (e: IllegalStateException) {
+ // Don't crash when dispatching on shutdown as you can run into race conditions.
+ }
+ }
+ }
+
+ override fun onStop() {
+ ThreadUtils.runOnMain {
+ try {
+ carHost.dispatchAppLifecycleEvent(Lifecycle.Event.ON_STOP)
+ } catch (e: IllegalStateException) {
+ // Don't crash when dispatching on shutdown as you can run into race conditions.
+ }
+ }
+ }
+
+ override fun onDestroyed() {
+ // Unlike the other lifecycle events, the fact that the CarAppActivity is destroyed does
+ // not mean that the CarAppBinding should be destroyed or unbound. We already have logic
+ // in CarHost to unbind the CarAppService after a specific timeout if the app remains in the
+ // STOPPED state (for non-nav apps).
+ }
+
+ override fun onCreateInputConnection(editorInfo: EditorInfo): IProxyInputConnection? {
+ return runOnMainAndAwaitResult { inputManager.onCreateInputConnection(editorInfo) }
+ }
+
+ /**
+ * Runs code on the main thread, and waits for the result.
+ *
+ * @param action the code to execute, that should return a result.
+ * @return the value produced by [action]. Returns null if times out or interrupted.
+ */
+ private fun <T> runOnMainAndAwaitResult(action: Callable<T>): T? {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ return try {
+ action.call()
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+ val futureTask = FutureTask(action)
+ handler.post(futureTask)
+ return try {
+ futureTask[ASYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS]
+ } catch (e: ExecutionException) {
+ throw RuntimeException(e)
+ } catch (e: InterruptedException) {
+ L.e(LogTags.APP_HOST, e, "Running call on main was interrupted.")
+ null
+ } catch (e: TimeoutException) {
+ L.e(LogTags.APP_HOST, e, "Running call on main was timed out.")
+ null
+ }
+ }
+
+ companion object {
+ private const val ASYNC_TIMEOUT_MILLIS: Long = 1000
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt
new file mode 100644
index 0000000..f696426
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/RoutingInfoStateImpl.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import com.android.car.libraries.apphost.common.RoutingInfoState
+
+/** Manages the state of routing information in template apps. */
+class RoutingInfoStateImpl : RoutingInfoState {
+ private var isVisible = false
+
+ override fun setIsRoutingInfoVisible(isVisible: Boolean) {
+ this.isVisible = isVisible
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt
new file mode 100644
index 0000000..5f9bf8b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StartCarAppUtil.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.VisibleForTesting
+import androidx.car.app.CarContext
+import androidx.car.app.activity.CarAppActivity
+import com.android.car.libraries.apphost.NavigationIntentConverter
+import com.google.common.base.Objects.equal
+import java.lang.UnsupportedOperationException
+import java.security.InvalidParameterException
+
+/** An utility class to validate calls to start a car app, and to perform them. */
+object StartCarAppUtil {
+ private const val PHONE_URI_PREFIX = "tel:"
+
+ /**
+ * Metadata tag that points to the component name of the car app service that is linked to the car
+ * app service.
+ */
+ @VisibleForTesting const val ACTIVITY_METADATA_KEY = "androidx.car.app.CAR_APP_ACTIVITY"
+
+ /**
+ * Asserts that the `intent` follows the guidelines set in [CarContext.startCarApp] and starts the
+ * app.
+ *
+ * @param packageName the package name of the app that sent the intent
+ * @param intent the intent for starting the car app.
+ * @param allowedToStartSelf whether the calling app is allowed to start itself. Only nav apps
+ * ```
+ * can call via [CarContext.startCarApp], and all apps can via a
+ * notification action.
+ * @throws SecurityException
+ * ```
+ * if the app attempts to start a different app explicitly or
+ * ```
+ * does not have permissions for the requested action.
+ * @throws InvalidParameterException
+ * ```
+ * if the [Intent] does not meet the criteria listed at
+ * ```
+ * [CarContext.startCarApp].
+ * ```
+ */
+ fun validateStartCarAppIntent(
+ context: Context,
+ packageName: String,
+ intent: Intent,
+ allowedToStartSelf: Boolean
+ ): Intent {
+ val intentComponent = intent.component
+ val action = intent.action
+
+ if (intentComponent != null && equal(intentComponent.packageName, packageName)) {
+ if (!allowedToStartSelf) {
+ throw SecurityException(
+ "The app is not a turn by turn navigation app, therefore it cannot start " +
+ "itself in the car"
+ )
+ }
+ intent.setClassName(packageName, CarAppActivity::class.qualifiedName!!)
+ } else if (equal(action, CarContext.ACTION_NAVIGATE)) {
+ assertNavigationIntentIsValid(intent)
+
+ // TODO(b/171308515): Add telemetry support.
+ } else if (equal(action, Intent.ACTION_DIAL) || equal(action, Intent.ACTION_CALL)) {
+ assertPhoneIntentIsValid(intent)
+
+ // TODO(b/171308515): Add telemetry support.
+ } else if (intentComponent == null) {
+ throw InvalidParameterException("The intent is not for a supported action")
+ } else {
+ throw SecurityException("Explicitly starting a separate app is not supported")
+ }
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ intent.resolveActivity(context.packageManager)
+ ?: throw UnsupportedOperationException(
+ "No component found to handle the startCarApp intent: $intent"
+ )
+
+ return intent
+ }
+
+ /**
+ * Checks that the [Intent] is for a phone call by validating it meets the following:
+ *
+ * * The data is correctly formatted starting with "tel:""
+ * * Has no component name set
+ */
+ private fun assertPhoneIntentIsValid(intent: Intent) {
+ if (!intent.dataString.orEmpty().startsWith(PHONE_URI_PREFIX)) {
+ throw InvalidParameterException("Phone intent data is not properly formatted")
+ }
+ if (intent.component != null) {
+ throw SecurityException("Phone intent cannot have a component")
+ }
+ }
+
+ /**
+ * Checks that the [Intent] is for navigation by validating it meets the following:
+ *
+ * * The data is formatted as described in [CarContext.startCarApp]
+ * * Has no component name set
+ */
+ private fun assertNavigationIntentIsValid(intent: Intent) {
+ val uri = intent.data
+ if (uri == null || !equal(NavigationIntentConverter.GEO_QUERY_PREFIX, uri.scheme)) {
+ throw InvalidParameterException("Navigation intent has a malformed uri")
+ }
+
+ val queryString = NavigationIntentConverter.getQueryString(uri)
+ if (queryString == null) {
+ if (NavigationIntentConverter.getCarLocation(uri) == null) {
+ throw InvalidParameterException(
+ "Navigation intent has neither a location nor a query string"
+ )
+ }
+ } else {
+ if (uri.encodedSchemeSpecificPart.contains("daddr=")) {
+ // Other intent URIs support daddr, we do not as of right now.
+ throw InvalidParameterException(
+ "Navigation intent has neither latitude,longitude nor a query string"
+ )
+ }
+ }
+ if (intent.component != null) {
+ throw SecurityException("Navigation intent cannot have a component")
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt
new file mode 100644
index 0000000..563ddde
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/StatusManager.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import androidx.annotation.VisibleForTesting
+import com.android.car.libraries.apphost.logging.StatusReporter
+import java.io.PrintWriter
+import java.util.SortedMap
+import java.util.TreeMap
+
+/** Manager to handle collecting status information from components to be added to a bug report. */
+object StatusManager : StatusReporter {
+ /** Sections to include in the status information */
+ enum class ReportSection {
+ APP_HOST,
+ SCREEN_RENDERES,
+ }
+
+ private val statusReporters: SortedMap<ReportSection, StatusReporter> = TreeMap()
+ private val lock = Any()
+
+ /**
+ * Adds a [StatusReporter] to be called for a bug report.
+ *
+ * @param section The section to be added to the bug report.
+ * @param reporter The [StatusReporter] that will fill in the information for the section.
+ */
+ fun addStatusReporter(section: ReportSection, reporter: StatusReporter) {
+ synchronized(lock) { statusReporters.put(section, reporter) }
+ }
+
+ /**
+ * Removes the [StatusReporter] for a given bug report section.
+ *
+ * @param section The section to remove, as passed to [.addStatusReporter].
+ */
+ fun removeStatusReporter(section: ReportSection) {
+ synchronized(lock) { statusReporters.remove(section) }
+ }
+
+ @VisibleForTesting
+ fun clear() {
+ synchronized(lock) { statusReporters.clear() }
+ }
+
+ override fun reportStatus(writer: PrintWriter, piiHandling: StatusReporter.Pii) {
+ synchronized(lock) {
+ for ((key, value) in statusReporters) {
+ writer.format("=== %s ===\n", key.name)
+ try {
+ value.reportStatus(writer, piiHandling)
+ } catch (throwable: Throwable) {
+ writer.format("\nError capturing dump for section: %s\n", throwable.message)
+ throwable.printStackTrace(writer)
+ }
+ writer.println()
+ }
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt
new file mode 100644
index 0000000..d7b696c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/SurfaceInfoProviderImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.graphics.Rect
+import com.android.car.libraries.apphost.common.EventManager
+import com.android.car.libraries.apphost.common.EventManager.EventType
+import com.android.car.libraries.apphost.common.SurfaceInfoProvider
+
+/** Provides surface properties necessary for efficiently rendering partial content. */
+// TODO (b/206636788): Consolidate redundant code and improve documentation
+internal class SurfaceInfoProviderImpl(private val eventManager: EventManager) :
+ SurfaceInfoProvider {
+ private var visibleArea: Rect? = null
+ private var stableArea: Rect? = null
+
+ override fun getVisibleArea() = visibleArea
+ override fun getStableArea() = stableArea
+
+ override fun setVisibleArea(area: Rect) {
+ val currentAreaNeedUpdated = visibleArea == null || area != visibleArea
+ visibleArea = area
+ if (currentAreaNeedUpdated) {
+ eventManager.dispatchEvent(EventType.SURFACE_VISIBLE_AREA)
+ }
+
+ val stableAreaToUpdate = calculateStableArea(area, stableArea)
+ if (stableArea != stableAreaToUpdate) {
+ stableArea = stableAreaToUpdate
+ eventManager.dispatchEvent(EventType.SURFACE_STABLE_AREA)
+ }
+ }
+
+ private fun calculateStableArea(visibleArea: Rect, stableArea: Rect?): Rect {
+ return if (stableArea == null || !stableArea.setIntersect(stableArea, visibleArea)) {
+ visibleArea
+ } else {
+ stableArea
+ }
+ }
+
+ override fun invalidateStableArea() {
+ stableArea = null
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt
new file mode 100644
index 0000000..c0430fb
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/TemplateContextImpl.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.res.Configuration
+import android.hardware.display.DisplayManager
+import com.android.car.libraries.apphost.common.ANRHandler
+import com.android.car.libraries.apphost.common.AppBindingStateProvider
+import com.android.car.libraries.apphost.common.AppDispatcher
+import com.android.car.libraries.apphost.common.BackPressedHandler
+import com.android.car.libraries.apphost.common.CarAppError
+import com.android.car.libraries.apphost.common.CarAppManager
+import com.android.car.libraries.apphost.common.CarAppPackageInfo
+import com.android.car.libraries.apphost.common.CarHostConfig
+import com.android.car.libraries.apphost.common.ColorContrastCheckState
+import com.android.car.libraries.apphost.common.ColorUtils
+import com.android.car.libraries.apphost.common.DebugOverlayHandler
+import com.android.car.libraries.apphost.common.ErrorHandler
+import com.android.car.libraries.apphost.common.EventManager
+import com.android.car.libraries.apphost.common.EventManager.EventType
+import com.android.car.libraries.apphost.common.HostResourceIds
+import com.android.car.libraries.apphost.common.RoutingInfoState
+import com.android.car.libraries.apphost.common.StatusBarManager
+import com.android.car.libraries.apphost.common.SurfaceCallbackHandler
+import com.android.car.libraries.apphost.common.SurfaceInfoProvider
+import com.android.car.libraries.apphost.common.SystemClockWrapper
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.common.ToastController
+import com.android.car.libraries.apphost.distraction.constraints.ConstraintsProvider
+import com.android.car.libraries.apphost.input.InputConfig
+import com.android.car.libraries.apphost.input.InputManager
+import com.android.car.libraries.apphost.internal.ANRHandlerImpl
+import com.android.car.libraries.apphost.internal.AppDispatcherImpl
+import com.android.car.libraries.apphost.internal.CarAppPackageInfoImpl
+import com.android.car.libraries.apphost.logging.TelemetryHandler
+import com.android.car.libraries.templates.host.di.FeaturesConfig
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig
+import com.android.car.libraries.templates.host.di.ThemeManager
+import com.android.car.libraries.templates.host.di.UxreConfig
+import java.io.PrintWriter
+
+/** A [TemplateContext] to provide to hosts and presenters. */
+class TemplateContextImpl
+private constructor(
+ context: Context,
+ appName: ComponentName,
+ private val backPressedHandler: BackPressedHandler,
+ private val surfaceCallbackHandler: SurfaceCallbackHandler,
+ private val statusBarManager: StatusBarManager,
+ errorHandler: ErrorHandler,
+ private val toastController: ToastController,
+ private val displayId: Int,
+ private val inputManager: InputManager,
+ private val inputConfig: InputConfig,
+ private val carAppManager: CarAppManager,
+ private val telemetryHandler: TelemetryHandler,
+ private val debugOverlayHandler: DebugOverlayHandler,
+ private val routingInfoState: RoutingInfoState,
+ private val colorContrastCheckState: ColorContrastCheckState,
+ private val carHostConfig: CarHostConfig,
+ private val systemClockWrapper: SystemClockWrapper,
+ isNavigationApp: Boolean,
+ private val hostResourceIds: HostResourceIds,
+ uxreConfig: UxreConfig,
+ themeManager: ThemeManager
+) : TemplateContext(context) {
+
+ private val carAppPackageInfo: CarAppPackageInfo =
+ CarAppPackageInfoImpl.create(
+ context,
+ appName,
+ isNavigationApp,
+ hostResourceIds,
+ AppIconLoaderImpl
+ )
+ private val eventManager: EventManager
+ private val surfaceInfoProvider: SurfaceInfoProvider
+ private val appDispatcher: AppDispatcher
+ private var appConfigurationContext: Context? = null
+ private var errorHandler: ErrorHandler
+ private val anrHandler: ANRHandler
+ private val constraintsProvider: ConstraintsProvider
+ private var lastError: CarAppError? = null
+ private val appBindingStateProvider: AppBindingStateProvider
+
+ init {
+ this.errorHandler =
+ ErrorHandler { error ->
+ lastError = error
+ errorHandler.showError(error)
+ }
+
+ themeManager.applyTheme(context)
+
+ appBindingStateProvider = AppBindingStateProvider()
+ eventManager = EventManager()
+ anrHandler = ANRHandlerImpl.create(appName, errorHandler, telemetryHandler, eventManager)
+ constraintsProvider = ConstraintsProviderImpl(context, eventManager, uxreConfig)
+ surfaceInfoProvider = SurfaceInfoProviderImpl(eventManager)
+ appDispatcher =
+ AppDispatcherImpl.create(
+ appName,
+ errorHandler,
+ anrHandler,
+ telemetryHandler,
+ appBindingStateProvider
+ )
+
+ // Create a context configured with this context's configuration, the car display's display
+ // metrics, and the remote app's theme.
+ val packageContext = ColorUtils.getPackageContext(context, appName.packageName)
+ if (packageContext == null) {
+ appConfigurationContext = null
+ } else {
+ val configuration = resources.configuration
+ val display = context.getSystemService(DisplayManager::class.java).getDisplay(displayId)
+ appConfigurationContext =
+ packageContext.createDisplayContext(display).createConfigurationContext(configuration)
+ appConfigurationContext?.setTheme(ColorUtils.loadThemeId(context, appName))
+ }
+ }
+
+ override fun getErrorHandler() = errorHandler
+ override fun getAppConfigurationContext() = appConfigurationContext
+ override fun getStatusBarManager() = statusBarManager
+ override fun getInputManager() = inputManager
+ override fun getInputConfig() = inputConfig
+ override fun getCarAppPackageInfo() = carAppPackageInfo
+ override fun getBackPressedHandler() = backPressedHandler
+ override fun getSurfaceCallbackHandler() = surfaceCallbackHandler
+ override fun getSurfaceInfoProvider() = surfaceInfoProvider
+ override fun getEventManager() = eventManager
+ override fun getAnrHandler() = anrHandler
+ override fun getAppDispatcher() = appDispatcher
+ override fun getToastController() = toastController
+ override fun getCarAppManager() = carAppManager
+ override fun getTelemetryHandler() = telemetryHandler
+ override fun getDebugOverlayHandler() = debugOverlayHandler
+ override fun getHostResourceIds() = hostResourceIds
+ override fun getRoutingInfoState() = routingInfoState
+ override fun getColorContrastCheckState() = colorContrastCheckState
+ override fun getConstraintsProvider() = constraintsProvider
+ override fun getCarHostConfig() = carHostConfig
+ override fun getSystemClockWrapper() = systemClockWrapper
+ override fun getAppBindingStateProvider() = appBindingStateProvider
+
+ override fun updateConfiguration(configuration: Configuration?) {
+ appConfigurationContext =
+ configuration?.let { appConfigurationContext?.createConfigurationContext(configuration) }
+
+ // Propagate the configuration changed event to any listeners.
+ getEventManager().dispatchEvent(EventType.CONFIGURATION_CHANGED)
+ }
+
+ override fun reportStatus(pw: PrintWriter) {
+ pw.printf("- app package info: %s\n", carAppPackageInfo)
+ pw.printf("- last error: %s\n", if (lastError != null) lastError else "n/a")
+ }
+
+ companion object {
+ /** Creates a [TemplateContextImpl] for the car app identified by the given [ComponentName] */
+ fun create(
+ context: Context,
+ appName: ComponentName,
+ displayId: Int,
+ backPressedHandler: BackPressedHandler,
+ surfaceCallbackHandler: SurfaceCallbackHandler,
+ statusBarManager: StatusBarManager,
+ debugOverlayHandler: DebugOverlayHandler,
+ inputManager: InputManager,
+ inputConfig: InputConfig,
+ carAppManager: CarAppManager,
+ isNavigationApp: Boolean,
+ hostResourceIds: HostResourceIds,
+ uxreConfig: UxreConfig,
+ hostApiLevelConfig: HostApiLevelConfig,
+ themeManager: ThemeManager,
+ telemetryHandler: TelemetryHandler,
+ featuresConfig: FeaturesConfig
+ ): TemplateContextImpl {
+ return TemplateContextImpl(
+ context,
+ appName,
+ backPressedHandler,
+ surfaceCallbackHandler,
+ statusBarManager,
+ ErrorHandlerImpl.create(context, appName, carAppManager, hostResourceIds),
+ ToastControllerImpl(context),
+ displayId,
+ inputManager,
+ inputConfig,
+ carAppManager,
+ telemetryHandler,
+ debugOverlayHandler,
+ RoutingInfoStateImpl(),
+ ColorContrastCheckStateImpl(),
+ CarHostConfigImpl(context, appName, hostApiLevelConfig, featuresConfig),
+ SystemClockWrapper(),
+ isNavigationApp,
+ hostResourceIds,
+ uxreConfig,
+ themeManager
+ )
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt
new file mode 100644
index 0000000..57f0328
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/ToastControllerImpl.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal
+
+import android.content.Context
+import android.widget.Toast
+import com.android.car.libraries.apphost.common.ToastController
+
+/** Manages the toasts on car screen. */
+class ToastControllerImpl(private val context: Context) : ToastController {
+ override fun showToast(text: CharSequence?, duration: Int) {
+ Toast.makeText(context, text, duration).show()
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml
new file mode 100644
index 0000000..16dafa5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.libraries.templates.host.internal.debug">
+
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="31" />
+
+ <application>
+ <activity
+ android:name=".ClusterActivity"
+ android:allowEmbedded="true"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:launchMode="singleInstance"
+ android:process=":renderer_service"
+ android:resizeableActivity="true"
+ android:screenOrientation="user"
+ android:theme="@style/Theme.Template">
+ <!-- In car_embedded builds, indicate that we are distraction optimized to prevent maps
+ from being killed when the car is moving. -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.car.cluster.NAVIGATION" />
+ </intent-filter>
+
+ <meta-data
+ android:name="distractionOptimized"
+ android:value="true" />
+ </activity>
+ </application>
+</manifest>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt
new file mode 100644
index 0000000..c5a082c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/ClusterActivity.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.internal.debug
+
+import android.annotation.SuppressLint
+import android.car.Car
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import androidx.appcompat.app.AppCompatActivity
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.StyleableRes
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.view.common.CarTextParams
+import com.android.car.libraries.templates.host.internal.HostNavState
+import com.android.car.libraries.templates.host.internal.NavigationCoordinator
+import com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView
+import com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView
+import com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView
+import com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import com.android.car.libraries.templates.host.R
+
+/**
+ * This activity will be launched by the system to show the user navigation updates in the
+ * instrument cluster.
+ */
+class ClusterActivity : AppCompatActivity() {
+ private lateinit var root: ViewGroup
+
+ // travel estimate card will only be enabled if there's enough room on screen
+ private var travelEstimateEnabled = false
+ private var travelEstimateView: TravelEstimateView? = null
+ private var travelEstimateContainer: ViewGroup? = null
+ private lateinit var detailedStepView: DetailedStepView
+ private lateinit var compactStepView: CompactStepView
+ private lateinit var progressView: ProgressView
+
+ private var carTextParams: CarTextParams? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.cluster_activity)
+ root = findViewById(R.id.root)
+ travelEstimateContainer = findViewById(R.id.travel_estimate_card_container)
+ travelEstimateView = findViewById(R.id.travel_estimate_view)
+ detailedStepView = findViewById(R.id.detailed_step_view)
+ compactStepView = findViewById(R.id.compact_step_view)
+ progressView = findViewById(R.id.progress_view)
+
+ initColors()
+ // `root` hasn't finished measuring yet, and will report width=0, so we need to throw this work
+ // to end of the MainLooper's queue.
+ Handler(Looper.getMainLooper()).post {
+ adjustViewport(intent)
+ calcTravelEstimateEnabled()
+ }
+
+ observeNavigationState()
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ adjustViewport(intent)
+ }
+
+ private fun initColors() {
+ // Read the fallback color to use with the app-defined card background color.
+ @StyleableRes
+ val themeAttrs =
+ intArrayOf(
+ com.android.car.libraries.templates.host.R.attr.templateNavCardFallbackContentColor
+ )
+ val ta = obtainStyledAttributes(themeAttrs)
+ val contentColor = ta.getColor(0, Color.WHITE)
+ ta.recycle()
+ detailedStepView.setTextColor(contentColor)
+ compactStepView.setTextColor(contentColor)
+ progressView.setColor(contentColor)
+ }
+
+ private fun observeNavigationState() {
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ NavigationCoordinator.getInstance(applicationContext).navigationState.collect { state ->
+ when (state) {
+ HostNavState.NotNavigating -> {
+ detailedStepView.setStepAndDistance(null, null, null, null, Color.TRANSPARENT, false)
+ compactStepView.setStep(null, null, null, Color.TRANSPARENT)
+ travelEstimateContainer?.visibility = View.GONE
+ }
+ is HostNavState.Navigating -> {
+ renderTrip(state)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun renderTrip(state: HostNavState.Navigating) {
+ val trip = state.trip
+ val templateContext = state.templateContext
+ val step = trip.steps.firstOrNull()
+ val travelEstimate = trip.stepTravelEstimates.firstOrNull()
+ val nextStep = trip.steps.elementAtOrNull(1)
+ val carTextParams =
+ carTextParams ?: createStepTextParams(templateContext).also { carTextParams = it }
+ detailedStepView.setStepAndDistance(
+ templateContext,
+ step,
+ travelEstimate?.remainingDistance,
+ carTextParams,
+ Color.TRANSPARENT,
+ false
+ )
+ compactStepView.setStep(templateContext, nextStep, carTextParams, Color.TRANSPARENT)
+
+ progressView.visibility = if (trip.isLoading) View.VISIBLE else View.GONE
+ if (travelEstimateEnabled && travelEstimate != null) {
+ travelEstimateContainer?.visibility = View.VISIBLE
+ travelEstimateView?.setTravelEstimate(templateContext, travelEstimate)
+ } else {
+ travelEstimateContainer?.visibility = View.GONE
+ }
+ }
+
+ /**
+ * Some of the display might be obscured by either the shape of the physical screen, or other
+ * elements in the cluster display. We need to respect this constraint and only display our UI
+ * within those bounds.
+ */
+ private fun adjustViewport(intent: Intent?) {
+ intent ?: return
+ val bundle = intent.getBundleExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE) ?: return
+ val viewport = bundle.getParcelable<Rect>("android.car:activityState.unobscured") ?: return
+
+ L.d(LogTags.CLUSTER) { "cluster un-obscured area: $viewport" }
+ root.setPadding(
+ viewport.left,
+ viewport.top,
+ root.width - viewport.right,
+ root.height - viewport.bottom
+ )
+ }
+
+ private fun calcTravelEstimateEnabled() {
+ val top = root.top + root.paddingTop
+ val bottom = root.bottom - root.paddingBottom
+ val safeAreaHeight = bottom - top
+ val threshold =
+ resources.getDimensionPixelSize(R.dimen.travel_estimate_card_min_height_threshold)
+ travelEstimateEnabled = safeAreaHeight > threshold
+ }
+
+ /**
+ * Returns a [CarTextParams] instance to use for the text of a step.
+ *
+ * Unlike other text elsewhere, image spans are allowed in these strings.
+ */
+ @SuppressLint("ResourceType")
+ private fun createStepTextParams(templateContext: TemplateContext): CarTextParams? {
+ @StyleableRes
+ val themeAttrs =
+ intArrayOf(
+ R.attr.templateRoutingImageSpanRatio,
+ R.attr.templateRoutingImageSpanBody2MaxHeight,
+ R.attr.templateRoutingImageSpanBody3MaxHeight
+ )
+ val ta = templateContext.obtainStyledAttributes(themeAttrs)
+ val imageRatio = ta.getFloat(0, 0f)
+ val body2MaxHeight = ta.getDimensionPixelSize(1, 0)
+ ta.recycle()
+ val maxWidth = (body2MaxHeight * imageRatio).toInt()
+ return CarTextParams.builder()
+ .setImageBoundingBox(Rect(0, 0, maxWidth, body2MaxHeight))
+ .setMaxImages(2)
+ .setColorSpanConstraints(CarColorConstraints.NO_COLOR)
+ .build()
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml
new file mode 100644
index 0000000..46b2c01
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/layout/cluster_activity.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ android:id="@+id/steps_card_container"
+ layout="@layout/steps_card_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="?templateCardContentContainerBottomMargin"
+ app:layout_constraintBottom_toTopOf="@id/travel_estimate_card_container"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed" />
+
+ <include
+ android:id="@+id/travel_estimate_card_container"
+ layout="@layout/travel_estimate_card_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/steps_card_container" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml
new file mode 100644
index 0000000..234d34f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/styles/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Travel estimate card should only be shown if the total safe area available for cluster is larger than this threshold -->
+<!-- <dimen name="travel_estimate_card_min_height_threshold">400dp</dimen>-->
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml
new file mode 100644
index 0000000..bdee464
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="car_app_ui_cluster_nav_icon_size">48dp</dimen>
+ <dimen name="car_app_ui_cluster_nav_text_size">40sp</dimen>
+ <!-- Travel estimate card should only be shown if the total safe area available for cluster is larger than this threshold -->
+ <dimen name="travel_estimate_card_min_height_threshold">400dp</dimen>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml
new file mode 100644
index 0000000..248e56b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Cluster serves drivers. They don't need contentDescription. -->
+ <string name="dummy_content_description" translatable="false" />
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml
new file mode 100644
index 0000000..a90d718
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/debug/res/values/styles.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <style name="Theme.AppCompat.NoActionBar.Fullscreen" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml
new file mode 100644
index 0000000..8d949aa
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/bools.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Whether or not NavState should be sent to NavigationManager -->
+ <bool name="send_navstates_to_system">true</bool>
+</resources> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml
new file mode 100644
index 0000000..93f023f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/internal/res/values/integers.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- How long to wait before unbinding from the service after the user leaves an app. -->
+ <integer name="app_unbind_delay_seconds">180</integer>
+
+ <!-- The max length of a car app list for showing routes. -->
+ <integer name="route_list_max_length">3</integer>
+
+ <!-- The max length of a car app list for showing pane information. -->
+ <integer name="pane_max_length">4</integer>
+
+ <!-- The max size for the template stack for the car app. -->
+ <integer name="template_stack_max_size">5</integer>
+
+ <!-- Default max string length -->
+ <integer name="car_app_default_max_string_length">120</integer>
+
+ <!-- How long to keep Cluster Icons in memory -->
+ <integer name="cluster_icon_cache_duration_millis">10000</integer>
+
+ <!-- How long to wait for Trip conversion before giving up on this update -->
+ <integer name="cluster_trip_to_navstate_conversion_timeout_millis">1000</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml
new file mode 100644
index 0000000..9707746
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_action_button_background_color_selector.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ https://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <!-- Primary color -->
+ <item app:type_primary="true"
+ android:color="@color/car_app_ui_action_button_primary_background_color"/>
+ <!-- Default-->
+ <item android:color="@color/car_app_ui_action_button_default_background_color"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml
new file mode 100644
index 0000000..ee4c7a0
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_background_color_selector.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Disabled state-->
+ <item android:state_enabled="false" android:color="@color/default_gray_928"/>
+
+ <!-- Default-->
+ <item android:color="@color/default_gray_868"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml
new file mode 100644
index 0000000..e2e0aad
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_color_selector.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Disabled state-->
+ <item android:state_enabled="false" android:color="@color/default_gradient_white_56"/>
+
+ <!-- Default-->
+ <item android:color="@color/default_white"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml
new file mode 100644
index 0000000..fc38e92
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_foreground_color_selector.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <!-- Note: order of the following lines is important -->
+ <item app:state_error="true" android:color="@color/car_app_ui_edit_text_error_color"/>
+ <item android:state_focused="true" android:color="@color/car_app_ui_edit_text_active_color"/>
+ <item android:state_enabled="true" android:color="@color/car_app_ui_edit_text_enabled_color"/>
+ <item android:color="@color/car_app_ui_edit_text_disabled_color"/>
+</selector> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml
new file mode 100644
index 0000000..d2e2056
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_edit_text_hint_color_selector.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Disabled state-->
+ <item android:state_enabled="false" android:color="@color/default_gradient_white_56"/>
+
+ <!-- Default-->
+ <item android:color="@color/default_gradient_white_72"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml
new file mode 100644
index 0000000..c6c3432
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/color/default_ripple_color_selector.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ https://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show
+for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed.
+Doing so also avoids "ghost" effect when rapidly moving focus across Views. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/default_controller_ripple_selector_color"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml
new file mode 100644
index 0000000..4aaee56
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- The background fill -->
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/default_action_button_background_color_selector"/>
+ <corners
+ android:radius="@dimen/car_app_ui_button_corner_radius"/>
+ </shape>
+ </item>
+
+ <!-- Masked ripple layer -->
+ <item android:drawable="@drawable/default_action_button_ripple"/>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml
new file mode 100644
index 0000000..ba31afc
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_action_button_ripple.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/default_ripple_color_selector">
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/default_ripple_color_selector"/>
+ <corners android:radius="@dimen/car_app_ui_button_corner_radius"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml
new file mode 100644
index 0000000..e6b3670
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/default_edit_text_background_color_selector"/>
+ <corners android:radius="@dimen/car_app_ui_corner_radius"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml
new file mode 100644
index 0000000..4451f8d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_edit_text_foreground.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:gravity="bottom">
+ <shape>
+ <size android:height="@dimen/car_app_ui_edit_text_border_width" />
+ <solid android:color="@color/default_edit_text_foreground_color_selector" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml
new file mode 100644
index 0000000..171d5c2
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_pan_button.xml
@@ -0,0 +1,28 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M15.54,5.54L13.77,7.3 12,5.54 10.23,7.3 8.46,5.54 12,2zM18.46,15.54l-1.76,-1.77L18.46,12l-1.76,-1.77 1.76,-1.77L22,12zM8.46,18.46l1.77,-1.76L12,18.46l1.77,-1.76 1.77,1.76L12,22zM5.54,8.46l1.76,1.77L5.54,12l1.76,1.77 -1.76,1.77L2,12z"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml
new file mode 100644
index 0000000..6384396
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/drawable/default_ic_refresh_button.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,20q-3.35,0 -5.675,-2.325Q4,15.35 4,12q0,-3.35 2.325,-5.675Q8.65,4 12,4q1.725,0 3.3,0.713 1.575,0.712 2.7,2.037V4h2v7h-7V9h4.2q-0.8,-1.4 -2.188,-2.2Q13.625,6 12,6 9.5,6 7.75,7.75T6,12q0,2.5 1.75,4.25T12,18q1.925,0 3.475,-1.1T17.65,14h2.1q-0.7,2.65 -2.85,4.325Q14.75,20 12,20z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml
new file mode 100644
index 0000000..24bad14
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <color name="default_hun_text_color">#E0FFFFFF</color>
+ <color name="default_hun_text_color2">#80FFFFFF</color>
+
+ <color name="default_card_text_color">#CCFFFFFF</color>
+ <color name="default_card_background_color">@color/default_gray_868</color>
+ <color name="default_focus_blue">#2371CD</color>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml
new file mode 100644
index 0000000..f7a1c51
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values-night/colors_overlayable.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Buttons. -->
+ <color name="car_app_ui_floating_button_default_background_color">@color/default_white</color>
+ <color name="car_app_ui_floating_button_default_text_color">@color/default_black</color>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml
new file mode 100644
index 0000000..07b3c19
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/attrs.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Custom error state to be used in edit boxes or other components that support this state -->
+<!-- <declare-styleable name="ErrorState">-->
+<!-- <attr name="state_error" format="boolean"/>-->
+<!-- </declare-styleable>-->
+
+ <!-- Custom button type to be used in action buttons or other component that support this
+ classification -->
+ <declare-styleable name="ButtonType">
+ <!-- Indicates this a "primary" button, out of a set of other buttons -->
+ <attr name="type_primary" format="boolean"/>
+ <!-- Indicates this an app button, background color controlled by the app -->
+ <attr name="type_custom" format="boolean"/>
+ </declare-styleable>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml
new file mode 100644
index 0000000..22e64fe
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/bools_overlayable.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Boolean definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these booleans directly from views. Booleans must be
+ referred to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them.
+
+ !!! IMPORTANT !!!
+ These resources must be added to the overlayable.xml file. DO NOT
+ add documentation here. Concentrate all documentation in public-ready
+ comments in the overlayable.xml file. -->
+
+ <bool name="car_app_ui_customized">false</bool>
+ <bool name="car_app_ui_is_action_color_overridden">false</bool>
+ <bool name="car_app_ui_action_button_list_button_stretch_horizontal">false</bool>
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml
new file mode 100644
index 0000000..0ca7b10
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Colors used as the default value for overlayable resources.
+ These resources are not directly overlayable. -->
+ <color name="default_white">#FFFFFF</color>
+ <color name="default_gradient_white_12">#1FFFFFFF</color>
+ <color name="default_gradient_white_16">#29FFFFFF</color>
+ <color name="default_gradient_white_24">#3DFFFFFF</color>
+ <color name="default_gradient_white_40">#66FFFFFF</color>
+ <color name="default_gradient_white_46">#75FFFFFF</color>
+ <color name="default_gradient_white_56">#8FFFFFFF</color>
+ <color name="default_gradient_white_72">#B8FFFFFF</color>
+ <color name="default_gray_50">#F8F9FA</color>
+ <color name="default_gray_100">#F1F3F4</color>
+ <color name="default_gray_200">#E8EAED</color>
+ <color name="default_gray_300">#DADCE0</color>
+ <color name="default_gray_400">#BDC1C6</color>
+ <color name="default_gray_500">#9AA0A6</color>
+ <color name="default_gray_600">#80868B</color>
+ <color name="default_gray_700">#5F6368</color>
+ <color name="default_gray_800">#3C4043</color>
+ <color name="default_gray_846">#2E3134</color>
+ <color name="default_gray_868">#282A2D</color>
+ <color name="default_gray_878">#2A2A29</color>
+ <color name="default_gray_900">#202124</color>
+ <color name="default_gray_928">#17181B</color>
+ <color name="default_gray_958">#0E1013</color>
+ <color name="default_black">#000000</color>
+ <color name="default_gradient_black_0">#00000000</color>
+ <color name="default_gradient_black_25">#40000000</color>
+ <color name="default_gradient_black_64">#A3000000</color>
+ <color name="default_gradient_black_72">#B8000000</color>
+ <color name="default_gradient_black_85">#D9000000</color>
+ <color name="default_gradient_black_88">#E0000000</color>
+ <color name="default_gradient_black_100">#FF000000</color>
+
+ <!-- Default colors. -->
+ <color name="default_text_color">@color/default_white</color>
+
+ <!-- Standard colors -->
+ <color name="default_standard_red">#FFEE675C</color>
+ <color name="default_standard_red_dark">#FFC5221F</color>
+ <color name="default_standard_green">#FF61AC70</color>
+ <color name="default_standard_green_dark">#FF448B47</color>
+ <color name="default_standard_blue">#FF669DF6</color>
+ <color name="default_standard_blue_dark">#FF3674E0</color>
+ <color name="default_standard_yellow">#FFE9A240</color>
+ <color name="default_standard_yellow_dark">#FFD5792D</color>
+
+ <!-- Default car app colors is customizable only with Car UI Library. -->
+ <color name="default_primary_color">@color/car_ui_text_color_primary</color>
+ <color name="default_primary_dark_color">@color/car_ui_text_color_primary</color>
+ <color name="default_secondary_color">@color/car_ui_text_color_secondary</color>
+ <color name="default_secondary_dark_color">@color/car_ui_text_color_secondary</color>
+
+ <!-- LINT.IfChange -->
+ <color name="default_hun_text_color">@color/default_white</color>
+ <color name="default_hun_text_color2">#8FFFFFFF</color>
+
+ <color name="default_card_text_color">@color/default_white</color>
+ <color name="default_background_color">@color/default_black</color>
+ <color name="default_card_background_color">@color/default_gray_846</color>
+ <color name="default_focus_blue">#4B9EFF</color>
+ <!-- LINT.ThenChange(../values-night/colors.xml) -->
+
+ <color name="default_message_debug_text_color">#FF57F1B1</color>
+
+ <color name="default_focus_no_content">#48FFFFFF</color>
+ <color name="default_controller_ripple_selector_color">#b27da9c7</color>
+ <color name="default_controller_ripple_color">#66ffffff</color>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml
new file mode 100644
index 0000000..e9c2531
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/colors_overlayable.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Color definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these colors directly from views. Colors must be referred
+ to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them.
+
+ !!! IMPORTANT !!!
+ These resources must be added to the overlayable.xml file. DO NOT
+ add documentation here. Concentrate all documentation in public-ready
+ comments in the overlayable.xml file.
+ -->
+
+ <!-- Standard colores -->
+ <color name="car_app_ui_standard_red">@color/default_standard_red</color>
+ <color name="car_app_ui_standard_red_dark">@color/default_standard_red_dark</color>
+ <color name="car_app_ui_standard_green">@color/default_standard_green</color>
+ <color name="car_app_ui_standard_green_dark">@color/default_standard_green_dark</color>
+ <color name="car_app_ui_standard_blue">@color/default_standard_blue</color>
+ <color name="car_app_ui_standard_blue_dark">@color/default_standard_blue_dark</color>
+ <color name="car_app_ui_standard_yellow">@color/default_standard_yellow</color>
+ <color name="car_app_ui_standard_yellow_dark">@color/default_standard_yellow_dark</color>
+
+ <!-- Button -->
+ <color name="car_app_ui_action_button_default_background_color">@color/default_gray_846</color>
+ <color name="car_app_ui_action_button_primary_background_color">@color/default_standard_blue</color>
+ <color name="car_app_ui_action_button_text_color">@color/default_white</color>
+ <color name="car_app_ui_floating_button_default_background_color">@color/default_black</color>
+ <color name="car_app_ui_floating_button_default_text_color">@color/default_white</color>
+
+ <!-- Read-only Text -->
+ <color name="car_app_ui_read_only_text_color">@color/default_black</color>
+ <color name="car_app_ui_read_only_text_background_color">@color/default_white</color>
+
+ <!-- Edit Text -->
+ <color name="car_app_ui_edit_text_active_color">@color/car_app_ui_standard_blue</color>
+ <color name="car_app_ui_edit_text_enabled_color">@color/default_gradient_white_72</color>
+ <color name="car_app_ui_edit_text_error_color">@color/car_app_ui_standard_red</color>
+ <color name="car_app_ui_edit_text_disabled_color">@color/default_gradient_white_56</color>
+
+ <!-- Hyperlink Text -->
+ <color name="car_app_ui_hyperlink_text_color">@color/default_white</color>
+
+ <!-- Rows -->
+ <color name="car_app_ui_row_background_color">@color/car_ui_activity_background_color</color>
+
+ <!-- Grids -->
+ <color name="car_app_ui_grid_item_background_color">@color/car_ui_activity_background_color</color>
+
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml
new file mode 100644
index 0000000..dd56d1e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/dimens_overlayable.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Dimension definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these dimensions directly from views. Dimensions must be
+ referred to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them.
+
+ !!! IMPORTANT !!!
+ These resources must be added to the overlayable.xml file. DO NOT
+ add documentation here. Concentrate all documentation in public-ready
+ comments in the overlayable.xml file. -->
+
+ <!-- Template element spacing -->
+ <dimen name="car_app_ui_image_to_text_spacing_vertical">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_text_to_control_spacing_vertical">@dimen/car_ui_padding_5</dimen>
+ <dimen name="car_app_ui_text_to_secondary_control_spacing_vertical">@dimen/car_ui_padding_7</dimen>
+ <dimen name="car_app_ui_control_to_text_spacing_vertical">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_control_to_control_spacing_horizontal">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_content_horizontal_margin">24dp</dimen>
+ <dimen name="car_app_ui_touch_target_size">@dimen/car_ui_touch_target_size</dimen>
+
+ <!-- Template element corner radius -->
+ <dimen name="car_app_ui_corner_radius">8dp</dimen>
+
+ <!-- Card spacing -->
+ <dimen name="car_app_ui_card_start_margin">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_card_top_margin">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_card_width">0dp</dimen>
+
+ <!-- Template image sizing -->
+ <dimen name="car_app_ui_large_image_size">@dimen/car_ui_list_item_content_icon_width</dimen>
+
+ <!-- Navigation card spacing -->
+ <dimen name="car_app_ui_nav_card_padding_vertical">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_nav_card_padding_horizontal">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_nav_card_image_to_text_spacing_horizontal">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_nav_card_large_text_size">32sp</dimen>
+ <dimen name="car_app_ui_nav_card_xlarge_text_size">44sp</dimen>
+ <dimen name="car_app_ui_nav_card_small_padding_vertical">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_nav_card_image_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_nav_card_width">0dp</dimen>
+ <dimen name="car_app_ui_nav_card_small_image_size">36dp</dimen>
+ <dimen name="car_app_ui_nav_card_large_image_size">64dp</dimen>
+
+ <!-- Card header spacing/sizing -->
+ <dimen name="car_app_ui_card_header_image_size">44dp</dimen>
+ <dimen name="car_app_ui_card_header_text_padding_horizontal">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_card_header_text_padding_vertical">@dimen/car_ui_padding_2</dimen>
+ <dimen name="car_app_ui_card_header_no_button_text_margin_start">@dimen/car_ui_padding_4</dimen>
+
+ <!-- Grid item spacing/sizing -->
+ <dimen name="car_app_ui_grid_item_vertical_spacing">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_grid_item_image_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_grid_item_text_to_text_spacing_vertical">@dimen/car_ui_padding_1</dimen>
+
+ <!-- Button spacing/sizing -->
+ <dimen name="car_app_ui_button_height">56dp</dimen>
+ <dimen name="car_app_ui_button_image_size">36dp</dimen>
+ <dimen name="car_app_ui_icon_button_start_spacing">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_icon_button_end_spacing">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_icon_button_image_to_text_spacing">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_button_text_horizontal_spacing">@dimen/car_ui_padding_5</dimen>
+ <dimen name="car_app_ui_button_corner_radius">4dp</dimen>
+ <dimen name="car_app_ui_action_button_list_button_max_width">800dp</dimen>
+ <dimen name="car_app_ui_button_side_alignment_spacing">@dimen/car_ui_padding_4</dimen>
+
+ <!-- Edit text spacing/sizing -->
+ <dimen name="car_app_ui_edit_text_top_padding">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_edit_text_bottom_padding">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_edit_text_start_padding">@dimen/car_ui_padding_2</dimen>
+ <dimen name="car_app_ui_edit_text_end_padding">@dimen/car_ui_padding_4</dimen>
+ <dimen name="car_app_ui_edit_text_error_vertical_spacing">@dimen/car_ui_padding_1</dimen>
+ <dimen name="car_app_ui_edit_text_error_horizontal_spacing">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_edit_text_border_width">2dp</dimen>
+
+ <!-- Read-only Text. -->
+ <dimen name="car_app_ui_read_only_text_padding">@dimen/car_ui_padding_4</dimen>
+
+ <!-- Compact row spacing/sizing. These rows are used inside cards. -->
+ <dimen name="car_app_ui_half_row_min_height">0dp</dimen>
+ <dimen name="car_app_ui_half_row_horizontal_padding">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_half_row_vertical_padding">@dimen/car_ui_padding_2</dimen>
+ <dimen name="car_app_ui_half_row_image_to_text_spacing">@dimen/car_ui_padding_3</dimen>
+ <dimen name="car_app_ui_half_row_text_to_text_spacing">@dimen/car_ui_padding_0</dimen>
+ <dimen name="car_app_ui_half_row_image_size">44dp</dimen>
+
+ <!-- Full row spacing/sizing. -->
+ <dimen name="car_app_ui_full_row_start_padding">@dimen/car_ui_list_item_text_start_margin</dimen>
+ <dimen name="car_app_ui_full_row_end_padding">0dp</dimen>
+
+ <!-- Sign-in template spacing/sizing. -->
+ <dimen name="car_app_ui_sign_in_method_max_width">640dp</dimen>
+
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml
new file mode 100644
index 0000000..f363b45
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Drawables used as the default value for overlayable resources.
+ These resources are not directly overlayable. -->
+ <drawable name="default_error_icon">@drawable/car_ui_icon_error</drawable>
+ <drawable name="default_alert_icon">@drawable/car_ui_icon_error</drawable>
+ <drawable name="default_back_icon">@drawable/car_ui_icon_arrow_back</drawable>
+</resources> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml
new file mode 100644
index 0000000..042bbf4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/drawable_overlayable.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Drawable definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these drawables directly from views. Drawables must be referred
+ to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them.
+
+ !!! IMPORTANT !!!
+ These resources must be added to the overlayable.xml file. DO NOT
+ add documentation here. Concentrate all documentation in public-ready
+ comments in the overlayable.xml file. -->
+
+ <drawable name="car_app_ui_action_button_background">@drawable/default_action_button_background</drawable>
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml
new file mode 100644
index 0000000..0a589d5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Gravity integer values (to be used as part of gravity overlayable attributes. -->
+<!-- <integer name="gravity_bottom">80</integer>-->
+<!-- <integer name="gravity_center">17</integer>-->
+<!-- <integer name="gravity_center_horizontal">1</integer>-->
+<!-- <integer name="gravity_center_vertical">16</integer>-->
+<!-- <integer name="gravity_end">8388613</integer>-->
+<!-- <integer name="gravity_left">3</integer>-->
+<!-- <integer name="gravity_no_gravity">0</integer>-->
+<!-- <integer name="gravity_right">5</integer>-->
+<!-- <integer name="gravity_start">8388611</integer>-->
+<!-- <integer name="gravity_top">48</integer>-->
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml
new file mode 100644
index 0000000..0123000
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/integers_overlayable.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Integer definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these integers directly from views. Integers must be
+ referred to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them.
+
+ !!! IMPORTANT !!!
+ These resources must be added to the overlayable.xml file. DO NOT
+ add documentation here. Concentrate all documentation in public-ready
+ comments in the overlayable.xml file. -->
+
+ <integer name="car_app_ui_list_max_length">6</integer>
+ <integer name="car_app_ui_grid_max_length">6</integer>
+ <integer name="car_app_ui_action_button_primary_horizontal_order">0</integer>
+ <integer name="car_app_ui_action_button_list_gravity">0</integer>
+ <integer name="car_app_ui_action_button_list_button_content_alignment">0</integer>
+ <integer name="car_app_ui_content_layout_gravity">@integer/gravity_center</integer>
+ <integer name="car_app_ui_content_gravity">@integer/gravity_center</integer>
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml
new file mode 100644
index 0000000..289f8bc
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/overlayable.xml
@@ -0,0 +1,306 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- List of the resource that can be customized by the OEMs by using
+ Runtime Resource Overlays.
+
+ !!! IMPORTANT !!!
+
+ Comments on this file are used to produce automatically generated
+ documentation available at https://docs.partner.android.com/gas/integrate/template_host.
+
+ Once per AAOS Host release, the following tool should be used to re-generate the publicly
+ documented resource list. This list constitutes an API with the OEMs. DO NOT remove or
+ rename an existing resource without a corresponding deprecation cycle.
+
+ third_party/java_src/android_libs/car/aaos_host/main/com/android/car/libraries/templates/host/overlayable/tools/generateDoc.py
+ -->
+ <overlayable name="OverlayableResources">
+ <policy type="system|product|vendor|signature">
+
+ <!-- Indicates whether OEMs have done any UI customizations. This value should be set to true
+ by the OEMs who wish to provide UI customization. -->
+ <item type="bool" name="car_app_ui_customized" />
+ <!-- Indicates whether OEMs choose to ignore app provided colors on
+ buttons on select templates. This value should be set to true by the
+ OEMs who wish to ignore app provided colors on buttons on select
+ templates. -->
+ <item type="bool" name="car_app_ui_is_action_color_overridden" />
+ <!-- Indicates whether buttons in the action button list (e.g. used in PaneTemplate)
+ stretch to fill the horizontal space. -->
+ <item type="bool" name="car_app_ui_action_button_list_button_stretch_horizontal" />
+
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_red" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_red_dark" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_green" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_green_dark" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_blue" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_blue_dark" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_yellow" />
+ <!-- Car App Library standard color. -->
+ <item type="color" name="car_app_ui_standard_yellow_dark" />
+ <!-- Default background color used on 'Action' buttons when one is not provided by the
+ application. -->
+ <item type="color" name="car_app_ui_action_button_default_background_color" />
+ <!-- Background color used on 'Action' buttons marked as 'Primary', when one is not provided
+ by the application. -->
+ <item type="color" name="car_app_ui_action_button_primary_background_color" />
+ <!-- Text color used on 'Action' buttons when one is not provided by the application. -->
+ <item type="color" name="car_app_ui_action_button_text_color" />
+ <!-- Background color used on FABs (floating action buttons) when one is not provided by the
+ application. -->
+ <item type="color" name="car_app_ui_floating_button_default_background_color" />
+ <!-- Text color used on FABs (floating action buttons) when one is not provided by the
+ application. -->
+ <item type="color" name="car_app_ui_floating_button_default_text_color" />
+ <!-- Text color used on read-only text boxes (such as the PIN code in Sign-In template). -->
+ <item type="color" name="car_app_ui_read_only_text_color" />
+ <!-- Background color used on read-only text boxes (such as the PIN code in Sign-In
+ template). -->
+ <item type="color" name="car_app_ui_read_only_text_background_color" />
+ <!-- Edit box 'active' text color (such as the username and password in Sign-In template). -->
+ <item type="color" name="car_app_ui_edit_text_active_color" />
+ <!-- Edit box 'enabled' text color (such as the username and password in Sign-In template). -->
+ <item type="color" name="car_app_ui_edit_text_enabled_color" />
+ <!-- Edit box 'error' text color (such as the username and password in Sign-In template). -->
+ <item type="color" name="car_app_ui_edit_text_error_color" />
+ <!-- Edit box 'disabled' text color (such as the username and password in Sign-In template). -->
+ <item type="color" name="car_app_ui_edit_text_disabled_color"/>
+ <!-- Text color used in 'clickable spans' (such as the ones allowed in Sign-In template). -->
+ <item type="color" name="car_app_ui_hyperlink_text_color" />
+ <!-- The background color of a row container view to check color contrast against its contents.
+ This color is used only for color contrast checks, and not for actual background coloring.
+ Set an appropriate value if the row background color is customized. -->
+ <item type="color" name="car_app_ui_row_background_color" />
+ <!-- The background color of a grid item view to check color contrast against its contents.
+ This color is used only for color contrast checks, and not for actual background coloring.
+ Set an appropriate value if the grid background color is customized. -->
+ <item type="color" name="car_app_ui_grid_item_background_color" />
+
+ <!-- Vertical space between an image and a text -->
+ <item type="dimen" name="car_app_ui_image_to_text_spacing_vertical" />
+ <!-- Vertical space between a text and a control (such as an edit box to instruction text). -->
+ <item type="dimen" name="car_app_ui_text_to_control_spacing_vertical" />
+ <!-- Vertical space between a text and a secondary control (such as an action button list view to additional text). -->
+ <item type="dimen" name="car_app_ui_text_to_secondary_control_spacing_vertical" />
+ <!-- Vertical space between a control (such as an edit box) and a text. -->
+ <item type="dimen" name="car_app_ui_control_to_text_spacing_vertical" />
+ <!-- Horizontal space between two controls (such two buttons in an Action Strip). -->
+ <item type="dimen" name="car_app_ui_control_to_control_spacing_horizontal" />
+ <!-- Horizontal space around content areas such as full screen lists and grids. -->
+ <item type="dimen" name="car_app_ui_content_horizontal_margin" />
+ <!-- Touch target size, used to define the size of header buttons, for example. -->
+ <item type="dimen" name="car_app_ui_touch_target_size" />
+ <!-- Corner radius used across the UI except for the buttons. -->
+ <item type="dimen" name="car_app_ui_corner_radius" />
+ <!-- Card width (expect for navigation card). If not set, the card width will be defined by
+ the host in proportion to the screen size. This value must be within the template host
+ defined range. -->
+ <item type="dimen" name="car_app_ui_card_width" />
+ <!-- Width and height of large images (such as list and grid items, and message and
+ sign-in images. -->
+ <item type="dimen" name="car_app_ui_large_image_size" />
+ <!-- Vertical space between the nav card content and its container. -->
+ <item type="dimen" name="car_app_ui_nav_card_padding_vertical" />
+ <!-- Horizontal space between the navigation card content and its container. -->
+ <item type="dimen" name="car_app_ui_nav_card_padding_horizontal" />
+ <!-- Horizontal space between an image and a text inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_image_to_text_spacing_horizontal" />
+ <!-- Vertical space between an image and a text inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_image_to_text_spacing_vertical" />
+ <!-- Size of xlarge text inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_xlarge_text_size" />
+ <!-- Size of large text inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_large_text_size" />
+ <!-- Vertical space applied in navigation card when lane images are present, for example. -->
+ <item type="dimen" name="car_app_ui_nav_card_small_padding_vertical" />
+ <!-- Navigation card width. If not set, the card width will be defined by the host in
+ proportion to the screen size. This value must be within the host defined maximum
+ range. -->
+ <item type="dimen" name="car_app_ui_nav_card_width" />
+ <!-- Size of small images inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_small_image_size" />
+ <!-- Size of large images inside a navigation card. -->
+ <item type="dimen" name="car_app_ui_nav_card_large_image_size" />
+ <!-- Size of an image inside a card header. -->
+ <item type="dimen" name="car_app_ui_card_header_image_size" />
+ <!-- Horizontal space between a text (e.g. a title) and the border of a card. -->
+ <item type="dimen" name="car_app_ui_card_header_text_padding_horizontal" />
+ <!-- Vertical space between a text (e.g. a title) and the border of a card. -->
+ <item type="dimen" name="car_app_ui_card_header_text_padding_vertical" />
+ <!-- Horizontal space between a text (e.g. a title) and the border of a card when no header
+ button is included. -->
+ <item type="dimen" name="car_app_ui_card_header_no_button_text_margin_start" />
+ <!-- Vertical space between grid items -->
+ <item type="dimen" name="car_app_ui_grid_item_vertical_spacing" />
+ <!-- Vertical space between an image and a text inside a grid item. -->
+ <item type="dimen" name="car_app_ui_grid_item_image_to_text_spacing_vertical" />
+ <!-- Vertical space between an two texts inside a grid item. -->
+ <item type="dimen" name="car_app_ui_grid_item_text_to_text_spacing_vertical" />
+ <!-- Buttons height. -->
+ <item type="dimen" name="car_app_ui_button_height" />
+ <!-- Image size inside a button. -->
+ <item type="dimen" name="car_app_ui_button_image_size" />
+ <!-- Horizontal space between the start and end sides of a FAB or button and the action
+ text. The spacing is applied only when the button only has the text.
+ If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. -->
+ <item type="dimen" name="car_app_ui_button_text_horizontal_spacing" />
+ <!-- Horizontal space between the icon and the text in a FAB or button. -->
+ <item type="dimen" name="car_app_ui_icon_button_image_to_text_spacing" />
+ <!-- Horizontal space between the start side of a FAB or button and the action icon. The
+ spacing is applied only when the button has both icon and text.
+ If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. -->
+ <item type="dimen" name="car_app_ui_icon_button_start_spacing" />
+ <!-- Horizontal space between the end side of a FAB or button and the action icon. The
+ spacing is applied only when the button has both icon and text.
+ If `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right), this value will be ignored. -->
+ <item type="dimen" name="car_app_ui_icon_button_end_spacing" />
+ <!-- Corner radius applied to buttons. -->
+ <item type="dimen" name="car_app_ui_button_corner_radius" />
+ <!-- The maximum width of a button in the action button list, e.g. used in PaneTemplate.
+ This value will be used only when `car_app_ui_action_button_list_button_stretch_horizontal` is set to `true`. -->
+ <item type="dimen" name="car_app_ui_action_button_list_button_max_width" />
+ <!-- The horizontal spacing around the content in a button in the action button list, e.g. used in PaneTemplate.
+ This value will be used only when `car_app_ui_action_button_list_button_content_alignment` is set to 1 (left) or 2 (right).
+ When this value is used, `car_app_ui_icon_button_start_spacing`, `car_app_ui_icon_button_end_spacing`, and `car_app_ui_button_text_horizontal_spacing` will be ignored. -->
+ <item type="dimen" name="car_app_ui_button_side_alignment_spacing" />
+ <!-- Edit box top vertical space -->
+ <item type="dimen" name="car_app_ui_edit_text_top_padding" />
+ <!-- Edit box bottom vertical space -->
+ <item type="dimen" name="car_app_ui_edit_text_bottom_padding" />
+ <!-- Edit box start side horizontal space -->
+ <item type="dimen" name="car_app_ui_edit_text_start_padding" />
+ <!-- Edit box end side horizontal space -->
+ <item type="dimen" name="car_app_ui_edit_text_end_padding" />
+ <!-- Vertical space between the edit box and the associated error message. -->
+ <item type="dimen" name="car_app_ui_edit_text_error_vertical_spacing" />
+ <!-- Horizontal space between the edit box error message and its container. -->
+ <item type="dimen" name="car_app_ui_edit_text_error_horizontal_spacing" />
+ <!-- Horizontal space around the text in read-only boxes (such as the PIN code in Sign-In
+ template). -->
+ <item type="dimen" name="car_app_ui_read_only_text_padding" />
+ <!-- Width of a border around or under the edit box, showing the different states of the box. -->
+ <item type="dimen" name="car_app_ui_edit_text_border_width"/>
+ <!-- Start padding to list items in full lists (such as ListTemplate) -->
+ <item type="dimen" name="car_app_ui_full_row_start_padding" />
+ <!-- End padding to list items in full lists (such as ListTemplate) -->
+ <item type="dimen" name="car_app_ui_full_row_end_padding" />
+ <!-- Minimum height of a list item in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_min_height" />
+ <!-- Horizontal space around list items in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_horizontal_padding" />
+ <!-- Vertical space around list items in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_vertical_padding" />
+ <!-- Horizontal space between image and text in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_image_to_text_spacing" />
+ <!-- Horizontal space between two texts in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_text_to_text_spacing" />
+ <!-- Image sizes in half lists (such as PlaceListMapTemplate) -->
+ <item type="dimen" name="car_app_ui_half_row_image_size" />
+ <!-- Sign-in template authentication methods max width. -->
+ <item type="dimen" name="car_app_ui_sign_in_method_max_width" />
+
+ <!-- Drawable used for action buttons background. The default value will render these
+ actions as solid rectangles with rounded corners (corner radius defined by
+ 'car_app_ui_button_corner_radius'). Background color will be
+ 'car_app_ui_action_button_default_background_color' or
+ 'car_app_ui_action_button_primary_background_color', depending on whether the button
+ is primary or not.
+ Buttons have the following custom selectors:
+ <ul>
+ <li>type_primary: Indicates the button is a primary one.
+ <li>type_custom: Indicate the colors of this button depend on app provided colors.
+ </ul>
+ When a button is marked as 'custom', the app provided background color is applied as a
+ tint over this drawable. -->
+ <item type="drawable" name="car_app_ui_action_button_background" />
+
+ <!-- Maximum number of items to show in a list. This can't be lower than 6 -->
+ <item type="integer" name="car_app_ui_list_max_length" />
+ <!-- Maximum number of items to show in a grid. This can't be lower than 6 -->
+ <item type="integer" name="car_app_ui_grid_max_length" />
+ <!-- Indicates the horizontal order that OEMs pick for the primary action
+ on selected templates.
+ <ul>
+ <li>0 means no re-order
+ <li>1 indicates primary action should be on the left
+ <li>2 indicates primary action should be on the right
+ </ul>
+ On horizontal buttons,
+ -->
+ <item type="integer" name="car_app_ui_action_button_primary_horizontal_order" />
+
+ <!-- The gravity of action button list (e.g. used in MessageTemplate and PaneTemplate).
+ The possible values are:
+ <ul>
+ <li>0: center (default)
+ <li>1: bottom
+ </ul> -->
+ <item type="integer" name="car_app_ui_action_button_list_gravity" />
+ <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate).
+ The possible values are:
+ <ul>
+ <li>0: center (default)
+ <li>1: left
+ <li>2: right
+ </ul> -->
+ <item type="integer" name="car_app_ui_action_button_list_button_content_alignment" />
+ <!-- Layout gravity for content areas (e.g content vertical alignment in Sign In Template
+ content).-->
+ <item type="integer" name="car_app_ui_content_layout_gravity"/>
+ <!-- Content gravity for content areas (e.g. content horizontal alignment in Sign In
+ Template content). -->
+ <item type="integer" name="car_app_ui_content_gravity"/>
+
+ <!-- General paragraph text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.TextBlock" />
+ <!-- Sign-in header text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.SignInHeader" />
+ <!-- Sign-in legal notice text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.SignInLegal" />
+ <!-- Card header appareance (e.g. Place List Template) -->
+ <item type="style" name="TextAppearance.CarAppUi.CardHeader" />
+ <!-- Grid item title text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.GridItemTitle" />
+ <!-- Grid item description text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.GridItemText" />
+ <!-- Buttons text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.ButtonText" />
+ <!-- Read-only text appareance -->
+ <item type="style" name="TextAppearance.CarAppUi.ReadOnlyText"/>
+ <!-- Style applied to input views (e.g. Sign-In username box) -->
+ <item type="style" name="Widget.CarAppUi.InputView" />
+ <!-- Style applied to edit boxes -->
+ <item type="style" name="Widget.CarAppUi.EditText" />
+ <!-- Style applied to row sections headers (such as in ListTemplate) -->
+ <item type="style" name="Widget.CarAppUi.RowSectionHeader" />
+ <!-- Style applied to row title (such as in ListTemplate) -->
+ <item type="style" name="Widget.CarAppUi.RowTitle" />
+ <!-- Style applied to row secondary text (such as in ListTemplate) -->
+ <item type="style" name="Widget.CarAppUi.RowSecondary" />
+ <!-- Style applied to list empty text (such as in ListTemplate) -->
+ <item type="style" name="Widget.CarAppUi.RowListEmpty" />
+ </policy>
+ </overlayable>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml
new file mode 100644
index 0000000..2162364
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/overlayable/res/values/styles_overlayable.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- LINT.IfChange -->
+<resources>
+ <!-- Style definitions that can be overridden by the OEMs.
+
+ !!! IMPORTANT !!!
+ Do not refer to these styles directly from views. Styles must be referred
+ to through theme attributes (in attrs.xml).
+
+ Any new resource added to this file should also be added to the
+ overlayable.xml before OEMs can customize them. -->
+
+ <!-- Template textAppearance -->
+ <style name="TextAppearance.CarAppUi.TextBlock" parent="TextAppearance.CarUi.Body2" />
+ <style name="TextAppearance.CarAppUi.SignInHeader" parent="TextAppearance.CarUi.Body2" />
+ <style name="TextAppearance.CarAppUi.SignInLegal" parent="TextAppearance.CarUi.Body3" />
+ <style name="TextAppearance.CarAppUi.CardHeader" parent="TextAppearance.CarUi.Body2" />
+ <style name="TextAppearance.CarAppUi.GridItemTitle" parent="TextAppearance.CarUi.Body2" />
+ <style name="TextAppearance.CarAppUi.GridItemText" parent="TextAppearance.CarUi.Body3">
+ <item name="android:textColor">@color/car_ui_text_color_secondary</item>
+ </style>
+ <style name="TextAppearance.CarAppUi.ButtonText" parent="TextAppearance.CarUi.Body3" />
+ <style name="TextAppearance.CarAppUi.ReadOnlyText" parent="TextAppearance.CarUi.Body3">
+ <item name="android:textColor">@color/car_app_ui_read_only_text_color</item>
+ </style>
+
+ <!-- Input view styling -->
+ <style name="Widget.CarAppUi.InputView" parent="">
+ <item name="android:gravity">start</item>
+ </style>
+
+ <!-- Edit text styling -->
+ <style name="Widget.CarAppUi.EditText" parent="android:Widget.DeviceDefault.EditText">
+ <item name="android:textColor">@color/default_edit_text_color_selector</item>
+ <item name="android:textColorHint">@color/default_edit_text_hint_color_selector</item>
+ <item name="android:paddingTop">@dimen/car_app_ui_edit_text_top_padding</item>
+ <item name="android:paddingBottom">@dimen/car_app_ui_edit_text_bottom_padding</item>
+ <item name="android:paddingStart">@dimen/car_app_ui_edit_text_start_padding</item>
+ <item name="android:paddingEnd">@dimen/car_app_ui_edit_text_end_padding</item>
+ <item name="android:background">@drawable/default_edit_text_background</item>
+ <item name="android:foreground">@drawable/default_edit_text_foreground</item>
+ </style>
+
+ <!-- The style of the list section header. -->
+ <style name="Widget.CarAppUi.RowSectionHeader" parent="TextAppearance.CarUi.ListItem.Header">
+ <item name="android:textAlignment">textStart</item>
+ <item name="android:layout_marginStart">@dimen/car_ui_padding_4</item>
+ <item name="android:layout_marginVertical">@dimen/car_ui_padding_2</item>
+ </style>
+
+ <!-- The style of the title text in a list row. -->
+ <style name="Widget.CarAppUi.RowTitle" parent="">
+ <item name="android:textAppearance">@style/TextAppearance.CarUi.ListItem</item>
+ <item name="android:textAlignment">viewStart</item>
+ <item name="android:singleLine">@bool/car_ui_list_item_single_line_title</item>
+ </style>
+
+ <!-- The style of the secondary text in a list row. -->
+ <style name="Widget.CarAppUi.RowSecondary" parent="">
+ <item name="android:textAppearance">@style/TextAppearance.CarUi.ListItem.Body</item>
+ <item name="android:textAlignment">viewStart</item>
+ </style>
+
+ <!-- The style of text that indicates a list is empty -->
+ <style name="Widget.CarAppUi.RowListEmpty" parent="Widget.CarAppUi.RowSecondary">
+ <item name="android:maxLines">2</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+</resources>
+<!-- LINT.ThenChange(overlayable.xml) -->
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt
new file mode 100644
index 0000000..bb10468
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/LegacySurfaceController.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.app.Presentation
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.display.VirtualDisplay
+import android.os.Build
+import android.util.Log
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.Surface
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.car.app.activity.renderer.surface.LegacySurfacePackage
+import androidx.car.app.activity.renderer.surface.SurfaceControlCallback
+import androidx.car.app.activity.renderer.surface.SurfaceWrapper
+import androidx.car.app.serialization.Bundleable
+import androidx.car.app.utils.ThreadUtils
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.StatusReporter
+import java.io.PrintWriter
+import java.util.function.Consumer
+
+/**
+ * A presenter similar to [SurfaceControlViewHost] that conforms to [SurfaceController].
+ *
+ * <p>This presenter should only be used if API version is lower than [Build.VERSION_CODES.R].
+ * Otherwise, [SurfaceControlViewHostController] should be used.
+ */
+class LegacySurfaceController(
+ private val context: Context,
+ private val templateContext: TemplateContext,
+ private val errorHandler: Consumer<Throwable>
+) : SurfaceController {
+
+ private var presentation: Presentation? = null
+ private var virtualDisplay: VirtualDisplay? = null
+ set(value) {
+ field?.release()?.also { Log.d(LogTags.APP_HOST, "Released old Display") }
+ field = value
+ }
+ private var width: Int = 0
+ private var height: Int = 0
+ private var densityDpi: Int = 0
+ private var contentView: View? = null
+
+ /** An interface for listening to key events. */
+ // TODO(b/192397819): Remove once SurfaceControlCallback supports the interface.
+ interface OnKeyListener {
+ /** Notifies the key event. */
+ fun onKeyEvent(event: KeyEvent)
+ }
+
+ private val surfaceControl =
+ object : SurfaceControlCallback, OnKeyListener {
+ override fun setSurfaceWrapper(surfaceWrapper: SurfaceWrapper) {
+ ThreadUtils.runOnMain {
+ // Since {@link SurfaceHolder.Callback} gives a guarantee that
+ // {@link SurfaceHolder.Callback#surfaceChanged} "is always called at least once, after"
+ // {@link SurfaceHolder.Callback#surfaceCreated}, we should only call
+ // {@link #updatePresentation} if the library is not adjusting insets. This will prevent
+ // two virtual displays from being created with back-to-back calls of
+ // {@link #setSurfaceWrapper} and {@link #relayout} when library is adjusting insets.
+ if (!libraryAdjustsInsets(templateContext.carHostConfig.appInfo?.libraryDisplayVersion)) {
+ Log.d(
+ LogTags.APP_HOST,
+ "SetSurfaceWrapper: " + "(${surfaceWrapper.width} x ${surfaceWrapper.height})"
+ )
+ updatePresentation(surfaceWrapper)
+ }
+ }
+ }
+
+ override fun onError(msg: String, e: Throwable) {
+ Log.e(LogTags.APP_HOST, msg, e)
+ errorHandler.accept(e)
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean, isInTouchMode: Boolean) {
+ ThreadUtils.runOnMain {
+ if (contentView != null) {
+ presentation?.window?.setLocalFocus(hasFocus, isInTouchMode)
+ }
+ }
+ }
+
+ override fun onTouchEvent(event: MotionEvent) {
+ ThreadUtils.runOnMain { presentation?.window?.injectInputEvent(event) }
+ }
+
+ override fun onKeyEvent(event: KeyEvent) {
+ ThreadUtils.runOnMain { presentation?.window?.superDispatchKeyEvent(event) }
+ }
+ }
+
+ private val surfacePackage = Bundleable.create(LegacySurfacePackage(surfaceControl))
+
+ override fun obtainSurfacePackage(): Bundleable = surfacePackage
+
+ override fun releaseSurfacePackage(value: Bundleable) {
+ // Nothing to do here. LegacySurfacePackage doesn't need to be released.
+ }
+
+ override fun releaseSurface() {
+ virtualDisplay?.surface = null
+ }
+
+ // TODO(b/208313104): Remove once majority of 3p applications migrated to 1.2.0-alpha-02.
+ private fun libraryAdjustsInsets(libraryDisplayVersion: String?): Boolean {
+ if (libraryDisplayVersion == null ||
+ libraryDisplayVersion.startsWith("1.1") ||
+ libraryDisplayVersion == "1.2.0-alpha01"
+ ) {
+ return false
+ }
+ return true
+ }
+
+ override fun relayout(surfaceWrapper: SurfaceWrapper) {
+ Log.i(LogTags.APP_HOST, "Relayout: " + "(${surfaceWrapper.width} x ${surfaceWrapper.height})")
+
+ if (libraryAdjustsInsets(templateContext.carHostConfig.appInfo?.libraryDisplayVersion)) {
+ Log.i(LogTags.APP_HOST, "Library does adjust insets.")
+ // A size change in the surface view requires a change in the dimensions of the virtual
+ // display created on top of such surface. This can only be achieved by recreating the display
+ // and adjusting the presentation on top of it. For this to be efficient, insets changes
+ // should be managed on the host side (see InsetsListener), in order to avoid unnecessary
+ // display recreations.
+ updatePresentation(surfaceWrapper)
+ } else {
+ Log.i(LogTags.APP_HOST, "Library does not adjust insets.")
+ // When library does not adjust the insets, host gets relayout calls even when the keyboard is
+ // displayed. In this case we should not recreate the presentation since that will release the
+ // first responder and dismissed the keyboard. Instead we need to adjust the size of the
+ // containerView.
+ contentView?.layoutParams =
+ FrameLayout.LayoutParams(surfaceWrapper.width, surfaceWrapper.height)
+ }
+ }
+
+ override fun setView(view: View, width: Int, height: Int) {
+ contentView = view
+ }
+
+ override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) {
+ pw.printf(
+ "- virtual display id: %s, width: %d, height: %d, density: %d dpi\n",
+ virtualDisplay?.display?.displayId ?: "-",
+ contentView?.layoutParams?.width ?: 0,
+ contentView?.layoutParams?.height ?: 0,
+ densityDpi
+ )
+ }
+
+ private fun updatePresentation(surfaceWrapper: SurfaceWrapper) {
+ if (!reuseVirtualDisplay(surfaceWrapper)) {
+ setNewDisplayAndPresentation(surfaceWrapper)
+ }
+
+ // Attach contentView to Presentation if it's not already there
+ contentView?.takeIf { !it.isAttachedTo(presentation) }?.let { contentView ->
+ Log.i(LogTags.APP_HOST, "Attaching contentView to Presentation")
+ (contentView.parent as ViewGroup?)?.removeView(contentView)
+ presentation?.setContentView(contentView)
+ contentView.layoutParams = FrameLayout.LayoutParams(width, height)
+ contentView.invalidate()
+ }
+ }
+
+ /**
+ * Attaches the new [Surface] to an existing [VirtualDisplay], if possible.
+ *
+ * @return [false] if there's no existing [VirtualDisplay], or its dimensions don't match. [true]
+ * if reuse was possible.
+ */
+ private fun reuseVirtualDisplay(surfaceWrapper: SurfaceWrapper): Boolean {
+ if (virtualDisplay != null &&
+ width == surfaceWrapper.width &&
+ height == surfaceWrapper.height &&
+ densityDpi == surfaceWrapper.densityDpi
+ ) {
+ Log.i(LogTags.APP_HOST, "Reusing existing VirtualDisplay with new Surface ($width x $height)")
+ virtualDisplay?.surface = surfaceWrapper.surface
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Creates, stores and shows a new [VirtualDisplay] and [Presentation] for the given
+ * [SurfaceWrapper].
+ */
+ private fun setNewDisplayAndPresentation(surfaceWrapper: SurfaceWrapper) {
+ Log.i(
+ LogTags.APP_HOST,
+ "Creating new VirtualDisplay and Presentation " +
+ "(${surfaceWrapper.width} x ${surfaceWrapper.height})"
+ )
+ val displayManager = context.getSystemService(DisplayManager::class.java)
+ virtualDisplay =
+ displayManager.createVirtualDisplay(
+ VIRTUAL_DISPLAY_NAME,
+ surfaceWrapper.width,
+ surfaceWrapper.height,
+ surfaceWrapper.densityDpi,
+ surfaceWrapper.surface,
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
+ )
+ width = surfaceWrapper.width
+ height = surfaceWrapper.height
+ densityDpi = surfaceWrapper.densityDpi
+ presentation = Presentation(PresentationContext(context), virtualDisplay?.display)
+ presentation?.show()
+ }
+
+ protected fun finalize() {
+ virtualDisplay?.release()
+ virtualDisplay = null
+ width = 0
+ height = 0
+ densityDpi = 0
+ }
+
+ companion object {
+ const val VIRTUAL_DISPLAY_NAME = "ScreenRendererVirtualDisplay"
+ }
+}
+
+private fun View.isAttachedTo(presentation: Presentation?): Boolean =
+ parent != null && parent == presentation?.findViewById(android.R.id.content)
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt
new file mode 100644
index 0000000..d65e237
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/PresentationContext.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.Display
+import android.view.inputmethod.InputMethodManager
+
+/**
+ * The context used for the [Presentation] of [LegacySurfaceController].
+ *
+ * This context injects its main [InputMethodManager] to its display contexts to avoid display
+ * mismatch which results in polluted logs.
+ */
+internal class PresentationContext(base: Context) : ContextWrapper(base) {
+
+ private class PresentationDisplayContext(
+ base: Context,
+ private val inputMethodManager: InputMethodManager
+ ) : ContextWrapper(base) {
+
+ override fun getSystemService(name: String): Any {
+ return if (INPUT_METHOD_SERVICE == name) inputMethodManager else super.getSystemService(name)
+ }
+ }
+
+ override fun createDisplayContext(display: Display): Context {
+ val inputMethodManager =
+ baseContext.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ val context = super.createDisplayContext(display)
+ return PresentationDisplayContext(context, inputMethodManager)
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt
new file mode 100644
index 0000000..8f7b4e0
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRenderer.kt
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.os.RemoteException
+import android.os.SystemClock
+import android.util.Log
+import android.view.View
+import androidx.annotation.ChecksSdkIntAtLeast
+import androidx.annotation.VisibleForTesting
+import androidx.car.app.CarAppService
+import androidx.car.app.CarContext
+import androidx.car.app.activity.renderer.ICarAppActivity
+import androidx.car.app.activity.renderer.surface.ISurfaceListener
+import androidx.car.app.activity.renderer.surface.SurfaceWrapper
+import androidx.car.app.model.TemplateWrapper
+import androidx.car.app.serialization.Bundleable
+import androidx.car.app.utils.ThreadUtils
+import com.android.car.libraries.apphost.CarHost
+import com.android.car.libraries.apphost.common.BackPressedHandler
+import com.android.car.libraries.apphost.common.CarAppManager
+import com.android.car.libraries.apphost.common.EventManager
+import com.android.car.libraries.apphost.common.HostResourceIds
+import com.android.car.libraries.apphost.common.IntentUtils
+import com.android.car.libraries.apphost.common.LocationMediator
+import com.android.car.libraries.apphost.common.StatusBarManager
+import com.android.car.libraries.apphost.common.SurfaceCallbackHandler
+import com.android.car.libraries.apphost.common.TemplateContext
+import com.android.car.libraries.apphost.internal.LocationMediatorImpl
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.StatusReporter
+import com.android.car.libraries.apphost.logging.TelemetryHandler
+import com.android.car.libraries.apphost.nav.NavigationHost
+import com.android.car.libraries.apphost.template.AppHost
+import com.android.car.libraries.apphost.template.ConstraintHost
+import com.android.car.libraries.apphost.template.UIController
+import com.android.car.libraries.apphost.view.SurfaceProvider
+import com.android.car.libraries.templates.host.di.FeaturesConfig
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig
+import com.android.car.libraries.templates.host.di.ThemeManager
+import com.android.car.libraries.templates.host.di.UxreConfig
+import com.android.car.libraries.templates.host.internal.CarActivityDispatcher
+import com.android.car.libraries.templates.host.internal.CarAppServiceInfo
+import com.android.car.libraries.templates.host.internal.CarHostRepository
+import com.android.car.libraries.templates.host.internal.DebugOverlayHandlerImpl
+import com.android.car.libraries.templates.host.internal.InputConfigImpl
+import com.android.car.libraries.templates.host.internal.InputManagerImpl
+import com.android.car.libraries.templates.host.internal.InsetsListener
+import com.android.car.libraries.templates.host.internal.NavigationStateCallbackImpl
+import com.android.car.libraries.templates.host.internal.RendererCallback
+import com.android.car.libraries.templates.host.internal.StartCarAppUtil
+import com.android.car.libraries.templates.host.internal.TemplateContextImpl
+import com.android.car.libraries.templates.host.view.TemplateView
+import java.io.PrintWriter
+
+/**
+ * A class used to handle rendering of a single car app screen.
+ *
+ * <p>Once the activity is ready the [onCreateActivity] should be called to start the rendering.
+ *
+ * @property appName Points to the car app service which provides the data for the screen.
+ * @param display The display on which the content should be displayed.
+ */
+class ScreenRenderer(
+ private val context: Context,
+ private val appName: ComponentName,
+ displayId: Int,
+ private val callback: CarActivityDispatcher.Callback,
+ hostResourceIds: HostResourceIds,
+ uxreConfig: UxreConfig,
+ hostApiLevelConfig: HostApiLevelConfig,
+ themeManager: ThemeManager,
+ telemetryHandler: TelemetryHandler,
+ featuresConfig: FeaturesConfig,
+ isDebugOverlayActive: Boolean
+) : BackPressedHandler, SurfaceCallbackHandler, StatusBarManager {
+ private var surfaceController: SurfaceController? = null
+ private lateinit var carActivity: CarActivityDispatcher
+ private val carAppManager = CarAppManagerImpl()
+ private val carAppServiceInfo = CarAppServiceInfo(context, appName)
+ private val isNavigationApp = carAppServiceInfo.isNavigationService
+ @VisibleForTesting var lastTemplate: TemplateWrapper? = null
+ private val mainHandler = Handler(Looper.getMainLooper(), HandlerCallback())
+ private val inputManagerListener =
+ object : InputManagerImpl.InputManagerListener {
+ override fun onStartInput() {
+ carActivity.dispatch(ICarAppActivity::onStartInput)
+ }
+
+ override fun onStopInput() {
+ carActivity.dispatch(ICarAppActivity::onStopInput)
+ }
+
+ override fun onUpdateSelection(
+ oldSelStart: Int,
+ oldSelEnd: Int,
+ newSelStart: Int,
+ newSelEnd: Int
+ ) {
+ carActivity.dispatchNoFail {
+ it.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)
+ }
+ }
+ }
+
+ private val inputManager = InputManagerImpl(inputManagerListener)
+
+ private val inputConfig = InputConfigImpl()
+
+ val templateContext: TemplateContext =
+ TemplateContextImpl.create(
+ context,
+ appName,
+ displayId,
+ this,
+ this,
+ this,
+ DebugOverlayHandlerImpl(isDebugOverlayActive),
+ inputManager,
+ inputConfig,
+ carAppManager,
+ isNavigationApp,
+ hostResourceIds,
+ uxreConfig,
+ hostApiLevelConfig,
+ themeManager,
+ telemetryHandler,
+ featuresConfig
+ )
+
+ private var templateView = TemplateView.create(templateContext)
+
+ @VisibleForTesting
+ val uiController =
+ object : UIController {
+ override fun getSurfaceProvider(appName: ComponentName?): SurfaceProvider {
+ return templateView.surfaceProvider
+ }
+
+ override fun setTemplate(appName: ComponentName?, template: TemplateWrapper?) {
+ mainHandler.removeMessages(MSG_SET_TEMPLATE)
+ val msg = mainHandler.obtainMessage(MSG_SET_TEMPLATE)
+ msg.obj = template
+
+ mainHandler.sendMessage(msg)
+ }
+ }
+
+ init {
+ val locationMediator =
+ LocationMediatorImpl.create(templateContext.eventManager) { enable: Boolean ->
+ trySetEnableAppLocationUpdates(enable)
+ }
+ templateContext.registerAppHostService(LocationMediator::class.java, locationMediator)
+ }
+
+ override fun onBackPressed() {
+ val carHost = CarHostRepository.get(appName)
+ val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.onBackPressed()
+ }
+
+ override fun onScroll(distanceX: Float, distanceY: Float) {
+ val carHost = CarHostRepository.get(appName)
+ val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.onSurfaceScroll(distanceX, distanceY)
+ }
+
+ override fun onFling(velocityX: Float, velocityY: Float) {
+ val carHost = CarHostRepository.get(appName)
+ val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.onSurfaceFling(velocityX, velocityY)
+ }
+
+ override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
+ val carHost = CarHostRepository.get(appName)
+ val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.onSurfaceScale(focusX, focusY, scaleFactor)
+ }
+
+ override fun setStatusBarState(
+ statusBarState: StatusBarManager.StatusBarState?,
+ rootView: View?
+ ) {
+ // TODO: Not yet implemented
+ Log.v(
+ LogTags.APP_HOST,
+ "StatusBar state updated to $statusBarState. " + "RootView is $rootView."
+ )
+ }
+
+ /** Requests to enable or disable location updates from the app. */
+ private fun trySetEnableAppLocationUpdates(enabled: Boolean) {
+ val carHost = CarHostRepository.get(appName)
+ val appHost = carHost?.getHostOrThrow(CarContext.APP_SERVICE) as? AppHost
+ appHost?.trySetEnableLocationUpdates(enabled)
+ }
+
+ private fun createBinderIntent(intent: Intent) =
+ Intent().apply {
+ action = CarAppService.SERVICE_INTERFACE
+ component = appName
+ IntentUtils.embedOriginalIntent(this, intent)
+ }
+
+ fun onCreateActivity(carActivity: ICarAppActivity) {
+ this.carActivity = CarActivityDispatcher(appName, carActivity, callback)
+ val carHost = CarHostRepository.computeIfAbsent(appName) { CarHost.create(templateContext) }
+ carHost.registerHostService(CarContext.APP_SERVICE) { appBinding ->
+ AppHost.create(uiController, appBinding, templateContext)
+ }
+ carHost.registerHostService(CarContext.CONSTRAINT_SERVICE) {
+ ConstraintHost.create(templateContext)
+ }
+
+ // Register the navigation host service only if the app is a navigation app. An
+ // exception will be thrown if non-nav apps try to request access to the
+ // navigation host service.
+ if (templateContext.carAppPackageInfo.isNavigationApp) {
+ L.d(LogTags.NAVIGATION, "Registering navigation service")
+ carHost.registerHostService(CarContext.NAVIGATION_SERVICE) { appBinding: Any? ->
+ NavigationHost.create(
+ appBinding,
+ templateContext,
+ NavigationStateCallbackImpl.create(templateContext)
+ )
+ }
+ }
+
+ // Before returning the CarHost instance, check that the AppHost service still has a
+ // reference to the UiController instance of this activity, and if not, update it.
+ // This could happen if the activity is destroyed and re-created after, while the
+ // CarAppService binding remains alive through those changes.
+ val appHost: AppHost = carHost.getHostOrThrow(CarContext.APP_SERVICE) as AppHost
+ if (uiController != appHost.getUIController()) {
+ L.d(
+ LogTags.APP_HOST,
+ "Activity has been re-created, updating UI controller and " +
+ "template context in the host services"
+ )
+ appHost.setUIController(uiController)
+ carHost.setTemplateContext(templateContext)
+ }
+
+ templateView.setParentLifecycle(carHost.lifecycle)
+ templateView.setTemplateContext(templateContext)
+
+ val surfaceListener = surfaceListener(carActivity, carHost)
+ carActivity.setSurfaceListener(surfaceListener)
+
+ templateContext.eventManager.subscribeEvent(this, EventManager.EventType.CONSTRAINTS) {
+ reloadTemplate()
+ }
+ }
+
+ fun onNewIntent(intent: Intent) {
+ val binderIntent = createBinderIntent(intent)
+ CarHostRepository.get(appName)?.bindToApp(binderIntent)
+ }
+
+ /** Updates the context with given configuration. */
+ fun onConfigurationChanged(config: Configuration) {
+ templateContext.updateConfiguration(config)
+ }
+
+ /**
+ * Called when the activity has disconnected from the renderer service. This instance shouldn't be
+ * used again after this point.
+ */
+ fun onDestroy() {
+ L.d(
+ LogTags.APP_HOST,
+ "Activity disconnected from the renderer service. " +
+ "Destroying its associated screen renderer."
+ )
+ }
+
+ fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) {
+ surfaceController?.reportStatus(pw, piiHandling)
+ pw.printf("- last template: %s\n", lastTemplate)
+ }
+
+ /** Shows/hides debug overlay if isVisible is {@code true}/{@code false} respectively. */
+ fun showDebugOverlay(isVisible: Boolean) {
+ templateContext.debugOverlayHandler.isActive = isVisible
+ }
+
+ private fun surfaceListener(carActivity: ICarAppActivity, carHost: CarHost): ISurfaceListener {
+ return object : ISurfaceListener.Stub() {
+ override fun onSurfaceAvailable(surfaceWrapperBundleable: Bundleable) {
+ val surfaceWrapper = surfaceWrapperBundleable.get()
+ if (surfaceWrapper !is SurfaceWrapper) {
+ Log.e(
+ LogTags.APP_HOST,
+ "onSurfaceAvailable event invoked with unexpected type: $surfaceWrapper"
+ )
+ // TODO(b/181775931): Better handle error case
+ return
+ }
+
+ val width = surfaceWrapper.width
+ val height = surfaceWrapper.height
+ ThreadUtils.runOnMain {
+ val surfaceController = getOrCreateSurfaceController(surfaceWrapper)
+ surfaceController.setView(templateView, width, height)
+ this@ScreenRenderer.surfaceController = surfaceController
+ val surfacePackage = surfaceController.obtainSurfacePackage()
+ val rendererCallback = RendererCallback(carHost, inputManager)
+ val insetsListener = InsetsListener(templateView)
+
+ try {
+ carActivity.setInsetsListener(insetsListener)
+ carActivity.setSurfacePackage(surfacePackage)
+ carActivity.registerRendererCallback(rendererCallback)
+ } catch (e: RemoteException) {
+ Log.e(LogTags.APP_HOST, "Binder invocation failed", e)
+ // TODO(b/181775931): Better handle error case
+ }
+
+ surfaceController.releaseSurfacePackage(surfacePackage)
+ }
+ }
+
+ override fun onSurfaceChanged(surfaceWrapperBundleable: Bundleable) {
+ val surfaceWrapper = surfaceWrapperBundleable.get()
+ if (surfaceWrapper !is SurfaceWrapper) {
+ Log.e(
+ LogTags.APP_HOST,
+ "onSurfaceChanged event invoked with unexpected type: $surfaceWrapper"
+ )
+ return
+ }
+
+ ThreadUtils.runOnMain { surfaceController?.relayout(surfaceWrapper) }
+ }
+
+ override fun onSurfaceDestroyed(surfaceWrapperBundleable: Bundleable) {
+ val surfaceWrapper = surfaceWrapperBundleable.get()
+ if (surfaceWrapper !is SurfaceWrapper) {
+ Log.e(
+ LogTags.APP_HOST,
+ "onSurfaceDestroyed event invoked with unexpected type: $surfaceWrapper"
+ )
+ return
+ }
+
+ ThreadUtils.runOnMain { surfaceController?.releaseSurface() }
+ }
+ }
+ }
+
+ private fun getOrCreateSurfaceController(surfaceWrapper: SurfaceWrapper): SurfaceController {
+ return if (SUPPORTS_SURFACE_VIEW_HOST_WRAPPER && surfaceWrapper.hostToken != null) {
+ SurfaceControlViewHostController(context, surfaceWrapper)
+ } else {
+ // Reuse old instance for SDK < 30 to avoid flicker (b/187841390)
+ surfaceController
+ ?: LegacySurfaceController(context, templateContext) { e ->
+ Log.e(LogTags.APP_HOST, "LegacySurfaceController error", e)
+ carActivity.disconnect()
+ }
+ }
+ }
+
+ private fun reloadTemplate() {
+ ThreadUtils.runOnMain { lastTemplate?.let { templateView.setTemplate(it) } }
+ }
+
+ private inner class CarAppManagerImpl : CarAppManager {
+ override fun startCarApp(intent: Intent) {
+ StartCarAppUtil.validateStartCarAppIntent(
+ context,
+ appName.packageName,
+ intent,
+ isNavigationApp
+ )
+ carActivity.dispatch { it.startCarApp(intent) }
+ }
+
+ override fun finishCarApp() {
+ carActivity.dispatch { it.finishCarApp() }
+ ThreadUtils.runOnMain { CarHostRepository.remove(appName) }
+ }
+ }
+
+ companion object {
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
+ private val SUPPORTS_SURFACE_VIEW_HOST_WRAPPER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+ private const val MSG_SET_TEMPLATE = 1
+ }
+
+ /** A [Handler.Callback] used to process the message queue for the ui controller. */
+ private inner class HandlerCallback : Handler.Callback {
+ private var lastUpdateUptimeMillis = -Long.MAX_VALUE
+
+ override fun handleMessage(msg: Message): Boolean {
+ if (msg.what == MSG_SET_TEMPLATE) {
+ // Use SystemClock.uptimeMillis since that is what Handler uses for time.
+ val currentUptimeMillis: Long = SystemClock.uptimeMillis()
+ val updateUptimeMillis: Long = lastUpdateUptimeMillis + 1000
+ if (updateUptimeMillis > currentUptimeMillis) {
+ val message: Message = mainHandler.obtainMessage(MSG_SET_TEMPLATE)
+ message.obj = msg.obj
+ mainHandler.removeMessages(MSG_SET_TEMPLATE)
+ mainHandler.sendMessageAtTime(message, updateUptimeMillis)
+ return true
+ }
+ lastUpdateUptimeMillis = currentUptimeMillis
+ val template: TemplateWrapper = msg.obj as TemplateWrapper
+
+ lastTemplate = template
+ templateView.setTemplate(template)
+ return true
+ } else {
+ L.w(LogTags.APP_HOST, "Unknown message: %s", msg)
+ }
+ return false
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt
new file mode 100644
index 0000000..5d5f559
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/ScreenRendererRepository.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.content.ComponentName
+import com.android.car.libraries.apphost.logging.L
+import com.android.car.libraries.apphost.logging.LogTags
+import com.android.car.libraries.apphost.logging.StatusReporter
+import com.android.car.libraries.templates.host.internal.StatusManager
+import com.google.common.collect.ImmutableList
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+import java.util.function.Supplier
+
+/** A cache to store instances of active [ScreenRenderer] that is safe for concurrent accesses. */
+object ScreenRendererRepository : StatusReporter {
+
+ // TODO(b/169643103): Change to Guava LRU cache to avoid having potential memory leaks.
+ private val cache: ConcurrentHashMap<ComponentName, ScreenRenderer> = ConcurrentHashMap()
+
+ init {
+ StatusManager.addStatusReporter(StatusManager.ReportSection.SCREEN_RENDERES, this)
+ }
+
+ /**
+ * Returns the value for the given [key]. If the key is not found in the cache, creates a
+ * [ScreenRenderer] using the provided [screenRendererProvider], puts its result into the map
+ * under the given key and returns it.
+ *
+ * This method guarantees not to put the value into the map if the key is already there, but the
+ * [screenRendererProvider] may be invoked even if the key is already in the map.
+ */
+ fun computeIfAbsent(
+ key: ComponentName,
+ screenRendererProvider: Supplier<ScreenRenderer>
+ ): ScreenRenderer {
+ return cache.getOrPut(key) { screenRendererProvider.get() }
+ }
+
+ /** Returns the [ScreenRenderer] for the given [key] if available. */
+ fun get(key: ComponentName): ScreenRenderer? {
+ return cache[key]
+ }
+
+ /** Returns a copy of all the available [ScreenRenderer]s. */
+ fun getAll(): ImmutableList<ScreenRenderer> {
+ return ImmutableList.copyOf(cache.values)
+ }
+
+ /** Removes the [ScreenRenderer] associated with the given [key] if available. */
+ fun remove(key: ComponentName): ScreenRenderer? {
+ return cache.remove(key)
+ }
+
+ /** Clears the cache content. */
+ fun clear() {
+ cache.clear()
+ }
+
+ override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) {
+ try {
+ pw.println("ScreenRenderer cache")
+ pw.printf("- size: %d\n", cache.size)
+ pw.printf("- screenRenderers: %d\n", cache.size)
+ for ((name, value) in cache.toSortedMap()) {
+ pw.println("\n-------------------------------")
+ pw.printf("App: %s\n", name.flattenToShortString())
+ value.reportStatus(pw, piiHandling)
+ }
+ } catch (t: Throwable) {
+ L.e(LogTags.APP_HOST, t, "Failed to produce status report for screen renderer cache")
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt
new file mode 100644
index 0000000..c9a47e8
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceControlViewHostController.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.annotation.RequiresApi
+import androidx.car.app.activity.renderer.surface.SurfaceWrapper
+import androidx.car.app.serialization.Bundleable
+import com.android.car.libraries.apphost.logging.StatusReporter
+import java.io.PrintWriter
+import java.lang.IllegalStateException
+
+/** A simple wrapper around [SurfaceControlViewHost] that conforms to [SurfaceController]. */
+@RequiresApi(Build.VERSION_CODES.R)
+class SurfaceControlViewHostController(val context: Context, val surfaceWrapper: SurfaceWrapper) :
+ SurfaceController {
+
+ private var surfaceControlViewHost: SurfaceControlViewHost
+ private var width: Int? = null
+ private var height: Int? = null
+
+ init {
+ val displayManager = context.getSystemService(DisplayManager::class.java)
+ val display = displayManager.getDisplay(surfaceWrapper.displayId)
+ val hostToken = surfaceWrapper.hostToken
+ surfaceControlViewHost = SurfaceControlViewHost(context, display, hostToken)
+ }
+
+ /**
+ * Because we are wrapping the [SurfacePackage] inside a [Bundleable], automatic releasing is not
+ * happening. Instead it must be released manually using [releaseSurfacePackage] once this value
+ * has been sent to the remote process.
+ *
+ * @see [SurfacePackage] Javadoc on recommendations around releasing this value.
+ */
+ override fun obtainSurfacePackage(): Bundleable {
+ val surfacePackage =
+ surfaceControlViewHost.surfacePackage
+ ?: throw IllegalStateException(
+ "SurfaceControlViewHost returned a null " + "SurfacePackage, which should never happen"
+ )
+ return Bundleable.create(surfacePackage)
+ }
+
+ override fun releaseSurfacePackage(value: Bundleable) {
+ (value.get() as SurfaceControlViewHost.SurfacePackage).release()
+ }
+
+ override fun relayout(surfaceWrapper: SurfaceWrapper) {
+ width = surfaceWrapper.width
+ height = surfaceWrapper.height
+ surfaceControlViewHost.relayout(surfaceWrapper.width, surfaceWrapper.height)
+ }
+
+ override fun setView(view: View, width: Int, height: Int) {
+ this.width = width
+ this.height = height
+
+ // SurfaceControlViewHost doesn't provide a way to detach the view hierarchy once attached.
+ // We add an intermediate ViewGroup here so we can detach TemplateView and reuse it in a
+ // different surface if needed.
+ val contentView = FrameLayout(context)
+ contentView.layoutParams =
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ (view.parent as ViewGroup?)?.removeView(view)
+ contentView.addView(
+ view,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+
+ surfaceControlViewHost.setView(contentView, width, height)
+ }
+
+ override fun releaseSurface() {
+ // No-op. Releasing surface is handled by the surface package.
+ }
+
+ override fun reportStatus(pw: PrintWriter, piiHandling: StatusReporter.Pii) {
+ pw.printf("- display id: %d, width: %d, height: %d\n", surfaceWrapper.displayId, width, height)
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt
new file mode 100644
index 0000000..d831f84
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/renderer/SurfaceController.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.renderer
+
+import android.view.View
+import androidx.car.app.activity.renderer.surface.SurfaceWrapper
+import androidx.car.app.serialization.Bundleable
+import com.android.car.libraries.apphost.logging.StatusReporter
+
+/** An interface used for presenters who want to present a surface control host. */
+interface SurfaceController : StatusReporter {
+ /**
+ * Returns a surface package object in form of a [Bundleable]. This surface package must be
+ * released calling [releaseSurfacePackage]
+ */
+ fun obtainSurfacePackage(): Bundleable
+
+ /** Releases a surface package previously obtained with [obtainSurfacePackage] */
+ fun releaseSurfacePackage(value: Bundleable)
+
+ /** Relayout the surface using the given [width] and [height]. */
+ fun relayout(surfaceWrapper: SurfaceWrapper)
+
+ /** Updates the top level content view with given [view]. */
+ fun setView(view: View, width: Int, height: Int)
+
+ /** Releases the surface. Should be called once the surface is destroyed. */
+ fun releaseSurface()
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java
new file mode 100644
index 0000000..34b4203
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestHostApiLevelConfig.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.testing;
+
+import android.content.ComponentName;
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig;
+
+/** A test implementation of {@link HostApiLevelConfig} */
+public class TestHostApiLevelConfig implements HostApiLevelConfig {
+
+ private static final TestHostApiLevelConfig INSTANCE = new TestHostApiLevelConfig();
+
+ /** Returns a {@link TestHostApiLevelConfig} implementation */
+ public static TestHostApiLevelConfig getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public int getHostMinApiLevel(int defaultValue, ComponentName componentName) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getHostMaxApiLevel(int defaultValue, ComponentName componentName) {
+ return defaultValue;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java
new file mode 100644
index 0000000..9472b75
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/testing/TestUxreConfig.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.testing;
+
+import com.android.car.libraries.templates.host.di.UxreConfig;
+
+/** A test implementation of {@link UxreConfig} */
+public class TestUxreConfig implements UxreConfig {
+
+ private static final TestUxreConfig INSTANCE = new TestUxreConfig();
+
+ /** Returns a {@link TestUxreConfig} implementation */
+ public static TestUxreConfig getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public int getTemplateStackMaxSize(int defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getRouteListMaxLength(int defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getPaneMaxLength(int defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getGridMaxLength(int defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getListMaxLength(int defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public int getCarAppDefaultMaxStringLength(int defaultValue) {
+ return defaultValue;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java
new file mode 100644
index 0000000..2d76c90
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/TemplateView.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view;
+
+import static java.lang.Math.max;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.DebugOverlayHandler.Observer;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.view.AbstractTemplateView;
+import com.android.car.libraries.apphost.view.SurfaceProvider;
+import com.android.car.libraries.apphost.view.SurfaceViewContainer;
+import com.android.car.libraries.apphost.view.TemplateTransitionManager;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.common.TemplateTransitionManagerImpl;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A view that displays {@link Template}s.
+ *
+ * <p>The current template can be set with {@link #setTemplate} method.
+ */
+public class TemplateView extends AbstractTemplateView implements Observer {
+ /**
+ * The {@link SurfaceViewContainer} which holds the surface that 3p apps can use to render custom
+ * content.
+ */
+ private SurfaceViewContainer mSurfaceViewContainer;
+
+ /** The {@link FrameLayout} container which holds the currently set template. */
+ private FrameLayout mTemplateContainer;
+
+ /** The {@link TextView} container which holds debug overlay info. */
+ private TextView mDebugOverlayText;
+
+ /** See {@link AbstractTemplateView#getMinimumTopPadding()} */
+ private final int mMinimumTopPadding;
+
+ /** {@link TemplateTransitionManager} used by this {@link AbstractTemplateView} implementation */
+ private final TemplateTransitionManager mTransitionManager = new TemplateTransitionManagerImpl();
+
+ /** Creates a new instance of {@link TemplateView}. */
+ @SuppressLint("InflateParams")
+ public static TemplateView create(TemplateContext context) {
+ TemplateView templateView =
+ (TemplateView) LayoutInflater.from(context).inflate(R.layout.template_view, null);
+ context.getDebugOverlayHandler().setObserver(templateView);
+ return templateView;
+ }
+
+ public TemplateView(Context context) {
+ this(context, null);
+ }
+
+ public TemplateView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TemplateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final int[] themeAttrs = {R.attr.templateStatusBarMinimumTopPadding};
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mMinimumTopPadding = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
+ }
+
+ /**
+ * Returns a {@link SurfaceProvider} which can be used to retrieve the {@link
+ * android.view.Surface} that 3p apps can use to draw custom content.
+ */
+ @Override
+ public SurfaceProvider getSurfaceProvider() {
+ return mSurfaceViewContainer;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSurfaceViewContainer = findViewById(R.id.surface_container);
+ mTemplateContainer = findViewById(R.id.template_container);
+ mDebugOverlayText = findViewById(R.id.debug_overlay);
+ }
+
+ @Override
+ protected SurfaceViewContainer getSurfaceViewContainer() {
+ return mSurfaceViewContainer;
+ }
+
+ @Override
+ protected ViewGroup getTemplateContainer() {
+ return mTemplateContainer;
+ }
+
+ @Override
+ protected int getMinimumTopPadding() {
+ return mMinimumTopPadding;
+ }
+
+ @Override
+ protected TemplateTransitionManager getTransitionManager() {
+ return mTransitionManager;
+ }
+
+ @Override
+ public void setWindowInsets(WindowInsets windowInsets) {
+ super.setWindowInsets(windowInsets);
+
+ if (mDebugOverlayText == null) {
+ return;
+ }
+
+ int leftInset;
+ int topInset;
+ int rightInset;
+ int bottomInset;
+
+ if (VERSION.SDK_INT >= VERSION_CODES.R) {
+ Insets insets =
+ windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime());
+ leftInset = insets.left;
+ topInset = insets.top;
+ rightInset = insets.right;
+ bottomInset = insets.bottom;
+
+ } else {
+ leftInset = windowInsets.getSystemWindowInsetLeft();
+ topInset = windowInsets.getSystemWindowInsetTop();
+ rightInset = windowInsets.getSystemWindowInsetRight();
+ bottomInset = windowInsets.getSystemWindowInsetBottom();
+ }
+
+ FrameLayout.LayoutParams lp = (LayoutParams) mDebugOverlayText.getLayoutParams();
+ lp.setMargins(leftInset, max(topInset, getMinimumTopPadding()), rightInset, bottomInset);
+ }
+
+ @Override
+ public void setTemplate(TemplateWrapper templateWrapper) {
+ super.setTemplate(templateWrapper);
+
+ TemplateContext templateContext = getTemplateContext();
+ if (templateContext != null) {
+ templateContext.getDebugOverlayHandler().resetTemplateDebugOverlay(templateWrapper);
+ }
+ }
+
+ @Override
+ public void entriesUpdated() {
+ TemplateContext templateContext = getTemplateContext();
+ if (templateContext != null) {
+ setDebugOverlayText(templateContext.getDebugOverlayHandler().getDebugOverlayText());
+ setDebugOverlayVisibility(templateContext.getDebugOverlayHandler().isActive());
+ }
+ }
+
+ private void setDebugOverlayText(CharSequence text) {
+ if (mDebugOverlayText == null) {
+ return;
+ }
+
+ mDebugOverlayText.setText(text);
+ }
+
+ private void setDebugOverlayVisibility(boolean isVisible) {
+ if (mDebugOverlayText == null) {
+ return;
+ }
+
+ mDebugOverlayText.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java
new file mode 100644
index 0000000..6034f7b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/animation/AnimationListenerAdapter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.animation;
+
+import android.view.animation.Animation;
+
+/**
+ * Provides empty implementations of the methods in {@link Animation.AnimationListener} for
+ * convenience reasons.
+ */
+public class AnimationListenerAdapter implements Animation.AnimationListener {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java
new file mode 100644
index 0000000..d144770
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/TemplateTransitionManagerImpl.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.common;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.SuppressLint;
+import android.content.res.TypedArray;
+import android.transition.Scene;
+import android.transition.Transition;
+import android.transition.TransitionInflater;
+import android.transition.TransitionManager;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplateTransitionManager;
+import com.android.car.libraries.templates.host.R;
+
+/** Controls transitions between different templates. */
+public class TemplateTransitionManagerImpl implements TemplateTransitionManager {
+ private static final float TRANSITION_ALPHA_GONE = 0f;
+ private static final float TRANSITION_ALPHA_VISIBLE = 1f;
+
+ @Override
+ public void transition(
+ ViewGroup root, View surface, TemplatePresenter to, @Nullable TemplatePresenter from) {
+ boolean toFullScreen = to.isFullScreen();
+ boolean fromFullScreen = from == null || from.isFullScreen();
+
+ if (toFullScreen || fromFullScreen) {
+ transitionDefault(root, surface, to, from);
+ } else {
+ transitionBetweenHalfScreenTemplates(root, to);
+ }
+ }
+
+ private static void transitionBetweenHalfScreenTemplates(ViewGroup root, TemplatePresenter to) {
+ Scene endingScene = new Scene(root, to.getView());
+ Transition transition =
+ TransitionInflater.from(root.getContext())
+ .inflateTransition(R.transition.half_screen_to_half_screen_transition);
+
+ TransitionManager.go(endingScene, transition);
+ }
+
+ @SuppressLint("Recycle")
+ private static void transitionDefault(
+ ViewGroup root, View surface, TemplatePresenter to, @Nullable TemplatePresenter from) {
+ @StyleableRes final int[] themeAttrs = {R.attr.templateUpdateAnimationDurationMilliseconds};
+ TypedArray ta = root.getContext().obtainStyledAttributes(themeAttrs);
+ long animationDurationMillis = ta.getInteger(0, 0);
+ ta.recycle();
+
+ if (to.usesSurface()) {
+ surface.setVisibility(View.VISIBLE);
+ }
+
+ View toView = to.getView();
+ View fromView = from == null ? null : from.getView();
+
+ toView.setAlpha(TRANSITION_ALPHA_GONE);
+ root.addView(toView);
+
+ toView.animate().alpha(TRANSITION_ALPHA_VISIBLE).setDuration(animationDurationMillis);
+
+ if (fromView != null) {
+ fromView
+ .animate()
+ .alpha(TRANSITION_ALPHA_GONE)
+ .setDuration(animationDurationMillis)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!to.usesSurface()) {
+ surface.setVisibility(View.GONE);
+ }
+ root.removeView(fromView);
+ }
+ });
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml
new file mode 100644
index 0000000..ca204ef
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/transition/half_screen_to_half_screen_transition.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together">
+ <fade
+ android:fadingMode="fade_in_out"
+ android:duration="?templateUpdateAnimationDurationMilliseconds">
+ <targets>
+ <target android:excludeId="@id/map_container" />
+ </targets>
+ </fade>
+ <changeBounds
+ android:duration="?templateUpdateAnimationDurationMilliseconds"
+ android:interpolator="@interpolator/fast_out_slow_in"/>
+</transitionSet>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml
new file mode 100644
index 0000000..f957662
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/common/res/values/ids.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <item name="map_container" type="id"/>
+ <item name="content_container" type="id"/>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java
new file mode 100644
index 0000000..d2b3997
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/AudioRecordThread.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.VisibleForTesting;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A class that is used to write bytes to an {@link OutputStream} from an [@link AudioRecord} */
+final class AudioRecordThread extends Thread {
+
+ private static final int AUDIO_RECORD_BUFFER_SIZE_BYTES = 512;
+
+ private final ParcelFileDescriptor.AutoCloseOutputStream mOutputStream;
+ private final InputStream mInputStream;
+ private boolean mIsRecording;
+ private final MicrophoneClosedListener mMicrophoneClosedListener;
+
+ AudioRecordThread(
+ ParcelFileDescriptor inputDescriptor,
+ ParcelFileDescriptor outputDescriptor,
+ MicrophoneClosedListener microphoneClosedListener) {
+ mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputDescriptor);
+ mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputDescriptor);
+ mMicrophoneClosedListener = microphoneClosedListener;
+ }
+
+ @Override
+ public void run() {
+ mIsRecording = true;
+ L.i(LogTags.TEMPLATE, "Recording START");
+ while (mIsRecording) {
+
+ // TODO(b/159207187): Consider using read blocking
+ byte[] bData = new byte[AUDIO_RECORD_BUFFER_SIZE_BYTES];
+ try {
+ mInputStream.read(bData);
+ } catch (IOException e) {
+ L.w(LogTags.TEMPLATE, e, "Recording STOPPED");
+ break;
+ }
+ if (bData == null) {
+ L.w(LogTags.TEMPLATE, "Recording STOPPED");
+ break;
+ }
+ // The task may have been cancelled:
+ if (isInterrupted()) {
+ L.d(LogTags.TEMPLATE, "Recording CANCELLED");
+ break;
+ }
+
+ if (bData != null) {
+ try {
+ mOutputStream.write(bData, 0, AUDIO_RECORD_BUFFER_SIZE_BYTES);
+ } catch (IOException e) {
+ // If we are unable to write bytes to the outputstream
+ // we close the outputstream and finish recording
+ L.i(LogTags.TEMPLATE, "Recording DONE");
+ break;
+ }
+ }
+ }
+
+ L.d(LogTags.TEMPLATE, "Recording CLEANUP");
+
+ // TODO(b/159208600): rewrite AudioRecordThread to use a monitor instead of errors to
+ // communicate
+ closeRecordingResourcesSafe();
+ }
+
+ /** Closes all resources associated with an ongoing recording. */
+ public void closeRecordingResourcesSafe() {
+ if (!mIsRecording) {
+ return;
+ }
+ try {
+ mOutputStream.close();
+ } catch (IOException e) {
+ L.e(LogTags.TEMPLATE, e, "IOException closing outputstream");
+ } finally {
+ mIsRecording = false;
+ if (mMicrophoneClosedListener != null) {
+
+ mMicrophoneClosedListener.onMicrophoneClosed();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public MicrophoneClosedListener getMicrophoneClosedListener() {
+ return mMicrophoneClosedListener;
+ }
+
+ @VisibleForTesting
+ public void setRecording(boolean isRecording) {
+ mIsRecording = isRecording;
+ }
+
+ public boolean isRecording() {
+ return mIsRecording;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java
new file mode 100644
index 0000000..bc91551
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplateConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import android.content.Context;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate;
+import com.android.car.libraries.apphost.view.TemplateConverter;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+
+/** A {@link TemplateConverter} for common templates. */
+public class CommonTemplateConverter implements TemplateConverter {
+ private static final CommonTemplateConverter INSTANCE = new CommonTemplateConverter();
+ private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES =
+ ImmutableSet.of(PaneTemplate.class, ListTemplate.class);
+
+ /** Returns an instance of CommonTemplateConverter */
+ public static CommonTemplateConverter get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public TemplateWrapper maybeConvertTemplate(Context context, TemplateWrapper templateWrapper) {
+ Template template = templateWrapper.getTemplate();
+ if (template instanceof ListTemplate || template instanceof PaneTemplate) {
+ Template newTemplate =
+ RowListWrapperTemplate.wrap(context, template, templateWrapper.isRefresh());
+
+ TemplateWrapper newWrapper = TemplateWrapper.wrap(newTemplate, templateWrapper.getId());
+ newWrapper.setRefresh(templateWrapper.isRefresh());
+ newWrapper.setCurrentTaskStep(templateWrapper.getCurrentTaskStep());
+ return newWrapper;
+ }
+ return templateWrapper;
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return SUPPORTED_TEMPLATES;
+ }
+
+ private CommonTemplateConverter() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java
new file mode 100644
index 0000000..2f30d3d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/CommonTemplatePresenterFactory.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.SearchTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.signin.SignInTemplate;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenterFactory;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+
+/**
+ * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for
+ * providing {@link TemplatePresenter} instances for the set of templates the host supports.
+ */
+public class CommonTemplatePresenterFactory implements TemplatePresenterFactory {
+ private static final CommonTemplatePresenterFactory INSTANCE =
+ new CommonTemplatePresenterFactory();
+ private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES =
+ ImmutableSet.of(
+ GridTemplate.class,
+ LongMessageTemplate.class,
+ MessageTemplate.class,
+ RowListWrapperTemplate.class,
+ SearchTemplate.class,
+ SignInTemplate.class);
+
+ /** Returns an instance of CommonTemplatePresenterFactory */
+ public static CommonTemplatePresenterFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ @Nullable
+ public TemplatePresenter createPresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ Class<? extends Template> clazz = templateWrapper.getTemplate().getClass();
+ if (GridTemplate.class == clazz) {
+ return GridTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (MessageTemplate.class == clazz) {
+ return MessageTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (LongMessageTemplate.class == clazz) {
+ return LongMessageTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (RowListWrapperTemplate.class == clazz) {
+ return RowListWrapperTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (SearchTemplate.class == clazz) {
+ return SearchTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (SignInTemplate.class == clazz) {
+ return SignInTemplatePresenter.create(templateContext, templateWrapper);
+ } else {
+ L.w(
+ LogTags.TEMPLATE,
+ "Don't know how to create a presenter for template: %s",
+ clazz.getSimpleName());
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return SUPPORTED_TEMPLATES;
+ }
+
+ private CommonTemplatePresenterFactory() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java
new file mode 100644
index 0000000..c79feb9
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/GridTemplatePresenter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.view.View.VISIBLE;
+
+import android.annotation.SuppressLint;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.GridWrapper;
+import com.android.car.libraries.templates.host.view.widgets.common.HeaderView;
+
+/** A {@link TemplatePresenter} for {@link GridTemplate} instances. */
+public class GridTemplatePresenter extends AbstractTemplatePresenter {
+ private final ViewGroup mRootView;
+ private final HeaderView mHeaderView;
+ private final ContentView mContentView;
+
+ /** Create a GridTemplatePresenter */
+ public static GridTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ GridTemplatePresenter presenter = new GridTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ private void update() {
+ GridTemplate template = (GridTemplate) getTemplate();
+ ActionStrip actionStrip = template.getActionStrip();
+ GridWrapper gridWrapper;
+ if (template.isLoading()) {
+ gridWrapper =
+ GridWrapper.wrap(null)
+ .setIsLoading(true)
+ .setIsRefresh(getTemplateWrapper().isRefresh())
+ .build();
+ } else {
+ gridWrapper =
+ GridWrapper.wrap(template.getSingleList())
+ .setIsRefresh(getTemplateWrapper().isRefresh())
+ .build();
+ }
+
+ mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction());
+ mContentView.setGridContent(getTemplateContext(), gridWrapper);
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"methodref.receiver.bound.invalid"})
+ private GridTemplatePresenter(TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.LIGHT);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext)
+ .inflate(R.layout.grid_wrapper_template_layout, null);
+ mContentView = mRootView.findViewById(R.id.grid_content_view);
+ View contentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView = HeaderView.install(templateContext, contentContainer);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java
new file mode 100644
index 0000000..afd208f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/LongMessageTemplatePresenter.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static java.util.Objects.requireNonNull;
+
+import android.content.res.TypedArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity;
+import com.android.car.libraries.templates.host.view.widgets.common.HeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.List;
+import org.checkerframework.checker.initialization.qual.UnknownInitialization;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * An {@link AbstractTemplatePresenter} that shows a scrolling long form message and some actions.
+ */
+public class LongMessageTemplatePresenter extends AbstractTemplatePresenter {
+ // TODO(b/183643108): Use a common value for this constant
+ private static final int MAX_ALLOWED_ACTIONS = 2;
+
+ private final ViewGroup mRootView;
+ private final HeaderView mHeaderView;
+ private final CarUiRecyclerView mRecyclerView;
+ private final ActionButtonListView mStickyActionButtonListView;
+ private final ActionButtonListView.Gravity mActionButtonListGravity;
+ private final ActionButtonListParams mActionButtonListParams;
+ private final String mDisabledActionButtonToastMessage;
+
+ private final LongMessageAdapter mAdapter;
+
+ /** Create a LongMessageTemplatePresenter */
+ static LongMessageTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ LongMessageTemplatePresenter presenter =
+ new LongMessageTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ /** Updates the view with current values in the {@link LongMessageTemplate}. */
+ private void update() {
+ LongMessageTemplate template = (LongMessageTemplate) getTemplate();
+ ActionStrip actionStrip = template.getActionStrip();
+
+ // If we have a title or a header action, show the header; hide it otherwise.
+ CarText title = template.getTitle();
+ Action headerAction = template.getHeaderAction();
+ if (!CarText.isNullOrEmpty(title) || headerAction != null) {
+ mHeaderView.setContent(getTemplateContext(), title, headerAction);
+ } else {
+ mHeaderView.setContent(getTemplateContext(), null, null);
+ }
+
+ mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+
+ mAdapter.setMessage(template.getMessage());
+ if (mActionButtonListGravity == Gravity.CENTER) {
+ // In the case of Gravity.CENTER, put the buttons in a row along with the rest of the content.
+ mAdapter.setActions(template.getActions());
+ mStickyActionButtonListView.setVisibility(GONE);
+ } else {
+ // If action button list gravity is not Gravity.CENTER, put the buttons in the sticky action
+ // button list view so they stay on screen at all times.
+ mAdapter.setActions(null);
+ mStickyActionButtonListView.setVisibility(VISIBLE);
+ mStickyActionButtonListView.setActionList(
+ getTemplateContext(), template.getActions(), mActionButtonListParams);
+ }
+
+ // If this update is not due to a refresh, scroll back to the top. Template presenters can
+ // be reused for templates of the same type, so a scroll reset would be needed for the case
+ // where an app pushes two long message templates in the same flow, for example, or if we at
+ // some point implement a pool of presenters.
+ // TODO(b/186244619): Add unit test to cover this path.
+ if (!getTemplateWrapper().isRefresh()) {
+ mRecyclerView.scrollToPosition(0);
+ }
+
+ setActionButtonEnabledState();
+ }
+
+ private LongMessageTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.LIGHT);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext)
+ .inflate(R.layout.long_message_template_layout, null);
+
+ mRecyclerView = mRootView.findViewById(R.id.list_view);
+
+ ParkedOnlyFrameLayout contentContainer = mRootView.findViewById(R.id.park_only_container);
+ mHeaderView = HeaderView.install(templateContext, contentContainer);
+ contentContainer.setTemplateContext(templateContext);
+
+ mStickyActionButtonListView = mRootView.requireViewById(R.id.sticky_action_button_list_view);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionButtonListGravity, R.attr.templatePlainContentBackgroundColor
+ };
+
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ mActionButtonListGravity = ActionButtonListView.Gravity.values()[ta.getInt(0, 0)];
+ @ColorInt int surroundingColor = ta.getColor(1, 0);
+ ta.recycle();
+
+ mDisabledActionButtonToastMessage =
+ templateContext
+ .getResources()
+ .getString(
+ templateContext.getHostResourceIds().getLongMessageTemplateDisabledActionText());
+
+ mActionButtonListParams =
+ ActionButtonListParams.builder()
+ .setMaxActions(MAX_ALLOWED_ACTIONS)
+ .setOemReorderingAllowed(true)
+ .setOemColorOverrideAllowed(true)
+ .setSurroundingColor(surroundingColor)
+ .build();
+
+ mAdapter = new LongMessageAdapter();
+ mRecyclerView.setAdapter(mAdapter);
+
+ mRecyclerView.addOnScrollListener(
+ new OnScrollListener() {
+ @Override
+ public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) {
+ // no-op
+ }
+
+ @Override
+ public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) {
+ if (newState != RecyclerView.SCROLL_STATE_IDLE) {
+ return;
+ }
+
+ setActionButtonEnabledState();
+ }
+ });
+ // {@link View#OnLayoutChangeListener} is required to disable sticky action buttons on first
+ // load.
+ mRecyclerView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+ setActionButtonEnabledState());
+ }
+
+ @RequiresNonNull({
+ "mRecyclerView",
+ "mStickyActionButtonListView",
+ "mDisabledActionButtonToastMessage"
+ })
+ private void setActionButtonEnabledState(
+ @UnknownInitialization LongMessageTemplatePresenter this) {
+ if (!mRecyclerView.getView().isAttachedToWindow()) {
+ return;
+ }
+ // Only need to set active state for sticky action buttons since they stay on screen at all
+ // times.
+ if (mActionButtonListGravity == Gravity.CENTER) {
+ return;
+ }
+
+ boolean enabled = !mRecyclerView.getView().canScrollVertically(/* direction= */ 1);
+
+ if (enabled) {
+ mStickyActionButtonListView.enableActionButtons();
+ } else {
+ mStickyActionButtonListView.disableActionButtons(mDisabledActionButtonToastMessage);
+ }
+ }
+
+ /** Adapter used for rendering the long text and buttons in this template. */
+ private class LongMessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ static final int ITEM_TYPE_MESSAGE = 1;
+ static final int ITEM_TYPE_ACTION = 2;
+
+ private String mMessage;
+ @Nullable private List<Action> mActions;
+
+ public void setMessage(CarText message) {
+ mMessage = CarTextUtils.toCharSequenceOrEmpty(getTemplateContext(), message).toString();
+ notifyDataSetChanged();
+ }
+
+ public void setActions(@Nullable List<Action> actions) {
+ mActions = actions;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position == 0) {
+ return ITEM_TYPE_MESSAGE;
+ } else {
+ return ITEM_TYPE_ACTION;
+ }
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ switch (viewType) {
+ case ITEM_TYPE_ACTION:
+ return new ActionsViewHolder(
+ LayoutInflater.from(getTemplateContext())
+ .inflate(R.layout.long_message_action_layout, viewGroup, false));
+
+ case ITEM_TYPE_MESSAGE:
+ default:
+ return new MessageViewHolder(
+ LayoutInflater.from(getTemplateContext())
+ .inflate(R.layout.long_message_layout, viewGroup, false));
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
+ if (viewHolder instanceof MessageViewHolder) {
+ ((MessageViewHolder) viewHolder).bind(mMessage);
+ } else if (viewHolder instanceof ActionsViewHolder) {
+ ((ActionsViewHolder) viewHolder).bind(getTemplateContext(), requireNonNull(mActions));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mActions == null ? 1 : 2;
+ }
+
+ /** ViewHolder for a message list item */
+ private class MessageViewHolder extends RecyclerView.ViewHolder {
+
+ private final CarUiTextView mMessage;
+
+ private MessageViewHolder(@NonNull View view) {
+ super(view);
+ mMessage = view.requireViewById(R.id.message_text);
+ }
+
+ private void bind(String message) {
+ mMessage.setText(message);
+ }
+ }
+
+ /** ViewHolder for actions list item */
+ private class ActionsViewHolder extends RecyclerView.ViewHolder {
+
+ private final ActionButtonListView mActionButtonListView;
+
+ private ActionsViewHolder(@NonNull View view) {
+ super(view);
+ mActionButtonListView = view.requireViewById(R.id.action_button_list_view);
+ }
+
+ private void bind(TemplateContext templateContext, List<Action> actions) {
+ if (!actions.isEmpty()) {
+ mActionButtonListView.setActionList(templateContext, actions, mActionButtonListParams);
+ mActionButtonListView.setVisibility(VISIBLE);
+ } else {
+ mActionButtonListView.setVisibility(GONE);
+ }
+ }
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java
new file mode 100644
index 0000000..0fd0c08
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MessageTemplatePresenter.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import androidx.annotation.ColorInt;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.internal.CommonUtils;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity;
+import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils;
+import com.android.car.libraries.templates.host.view.widgets.common.HeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.ViewUtils;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.List;
+
+/** An {@link AbstractTemplatePresenter} that shows an alert message, some actions, and an icon. */
+public class MessageTemplatePresenter extends AbstractTemplatePresenter {
+ // TODO(b/183643108): Use a common MAX_ALLOWED_ACTIONS between AAOS and AAP
+ private static final int MAX_ALLOWED_ACTIONS = 2;
+
+ private final ViewGroup mRootView;
+ private final HeaderView mHeaderView;
+ private final ViewGroup mProgressContainer;
+ private final CarUiTextView mMessageTextView;
+ private final ViewGroup mStackTraceContainer;
+ private final CarUiTextView mStackTraceView;
+ private final ImageView mIconView;
+ private final ActionButtonListView mActionListView;
+ private final ImageViewParams mImageViewParams;
+ private final ActionButtonListParams mActionButtonListParams;
+ private final boolean mIsDebugEnabled;
+ private final ViewGroup mMessageContainer;
+
+ /** Create a MessageTemplatePresenter */
+ public static MessageTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ MessageTemplatePresenter presenter =
+ new MessageTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mActionListView.getVisibility() == VISIBLE) {
+ return mActionListView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ /** Updates the view with current values in the {@link MessageTemplate}. */
+ private void update() {
+ TemplateContext templateContext = getTemplateContext();
+ MessageTemplate template = (MessageTemplate) getTemplate();
+
+ // If we have a title or a header action, show the header; hide it otherwise.
+ CarText title = template.getTitle();
+ Action headerAction = template.getHeaderAction();
+ if (!CarText.isNullOrEmpty(title) || headerAction != null) {
+ mHeaderView.setContent(getTemplateContext(), title, headerAction);
+ } else {
+ mHeaderView.setContent(getTemplateContext(), null, null);
+ }
+
+ mHeaderView.setActionStrip(
+ template.getActionStrip(), ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+
+ // Show a message if we have it, hide it otherwise.
+ CarText message = template.getMessage();
+ if (!CarText.isNullOrEmpty(message)) {
+ mMessageTextView.setText(
+ CarUiTextUtils.fromCarText(templateContext, message, mMessageTextView.getMaxLines()));
+ mMessageTextView.setVisibility(VISIBLE);
+
+ // Allow focus on the message view if there are no actions available.
+ mMessageTextView.setFocusable(template.getActions().isEmpty());
+ } else {
+ mMessageTextView.setVisibility(GONE);
+ }
+
+ // The icon and progress indicator are mutually exclusive, next we choose which one to
+ // display.
+ boolean isLoading = template.isLoading();
+ if (isLoading) {
+ // If in loading state, show the progress container and hide the icon.
+ mProgressContainer.setVisibility(VISIBLE);
+ mIconView.setVisibility(GONE);
+ } else {
+ // Not in loading state: hide the progress container and show the icon, if we have one.
+ mProgressContainer.setVisibility(GONE);
+
+ CarIcon icon = template.getIcon();
+ boolean showIcon = icon != null;
+ if (showIcon) {
+ showIcon = ImageUtils.setImageSrc(templateContext, icon, mIconView, mImageViewParams);
+ }
+ mIconView.setVisibility(showIcon ? VISIBLE : GONE);
+ }
+
+ // Show the action list if we have it, hide it otherwise.
+ List<Action> actionList = template.getActions();
+ if (!actionList.isEmpty()) {
+ mActionListView.setActionList(getTemplateContext(), actionList, mActionButtonListParams);
+ mActionListView.setVisibility(VISIBLE);
+ } else {
+ mActionListView.setVisibility(GONE);
+ }
+
+ // If we can show the debug information, add a button to the action strip that toggles it
+ // on and off when tapped.
+ CarText debugMessage = template.getDebugMessage();
+ if (mIsDebugEnabled && !CarText.isNullOrEmpty(debugMessage)) {
+ mStackTraceView.setText(CarTextUtils.toCharSequenceOrEmpty(templateContext, debugMessage));
+ mStackTraceContainer.setVisibility(VISIBLE);
+ addDebugToggle(templateContext);
+ } else {
+ mStackTraceContainer.setVisibility(GONE);
+ }
+ }
+
+ private void addDebugToggle(TemplateContext templateContext) {
+ Drawable icon = templateContext.getDrawable(R.drawable.ic_bug_report_grey600_24dp);
+ mHeaderView.addToggle(icon, this::showTraceView);
+ }
+
+ private void showTraceView(boolean show) {
+ mStackTraceContainer.setVisibility(show ? VISIBLE : GONE);
+ mMessageContainer.setVisibility(show ? GONE : VISIBLE);
+ }
+
+ @SuppressWarnings("method.invocation.invalid")
+ private MessageTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.LIGHT);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext).inflate(R.layout.message_template_layout, null);
+ mMessageTextView = mRootView.findViewById(R.id.message_text);
+ mStackTraceContainer = mRootView.findViewById(R.id.stack_trace_container);
+ mStackTraceView = mRootView.findViewById(R.id.stack_trace);
+ mProgressContainer = mRootView.findViewById(R.id.progress_container);
+ mIconView = mRootView.findViewById(R.id.message_icon);
+ ViewGroup contentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView = HeaderView.install(templateContext, contentContainer);
+ mMessageContainer = mRootView.findViewById(R.id.message_container);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateMessageDefaultIconTint,
+ R.attr.templateLargeImageSizeMin,
+ R.attr.templateLargeImageSizeMax,
+ R.attr.templateActionButtonListGravity,
+ R.attr.templatePlainContentBackgroundColor
+ };
+
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ @ColorInt int defaultIconTint = ta.getColor(0, 0);
+ int largeImageSizeMin = ta.getDimensionPixelSize(1, 0);
+ int largeImageSizeMax = ta.getDimensionPixelSize(2, Integer.MAX_VALUE);
+ ActionButtonListView.Gravity actionButtonListGravity =
+ ActionButtonListView.Gravity.values()[ta.getInt(3, 0)];
+ @ColorInt int backgroundColor = ta.getColor(4, 0);
+ ta.recycle();
+
+ mActionListView =
+ actionButtonListGravity == Gravity.CENTER
+ ? mRootView.findViewById(R.id.action_button_list_view)
+ : mRootView.findViewById(R.id.sticky_action_button_list_view);
+
+ // Progress container size is OEM-customizable. Enforce the size limit here.
+ ViewUtils.enforceViewSizeLimit(mProgressContainer, largeImageSizeMin, largeImageSizeMax);
+
+ mImageViewParams =
+ ImageViewParams.builder()
+ .setDefaultTint(defaultIconTint)
+ .setBackgroundColor(backgroundColor)
+ .build();
+ mActionButtonListParams =
+ ActionButtonListParams.builder()
+ .setMaxActions(MAX_ALLOWED_ACTIONS)
+ .setOemReorderingAllowed(true)
+ .setOemColorOverrideAllowed(true)
+ .setSurroundingColor(backgroundColor)
+ .build();
+ mIsDebugEnabled = CommonUtils.INSTANCE.isDebugEnabled(templateContext);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java
new file mode 100644
index 0000000..9294e3c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/MicrophoneClosedListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+/**
+ * A listener which will be notified whenever the microphone is no longer being recorded. Will allow
+ * for UI to be updated from the watevra host.
+ */
+public interface MicrophoneClosedListener {
+ /** Callback for when the microphone is closed. */
+ void onMicrophoneClosed();
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java
new file mode 100644
index 0000000..4f07f3c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/PresenterUtils.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+
+/** Assorted presenter utilities. */
+public abstract class PresenterUtils {
+ /**
+ * Applies the top window insets of the root view of a template to the {@code viewContainer}.
+ *
+ * <p>This is needed for templates that use a overlaid view on a background surface, so that the
+ * status bar is rendered above the surface, and the view container is moved down so that it is
+ * not drawn under the status bar text.
+ */
+ public static void applyTopWindowInsetsToContainer(int topInset, ViewGroup viewContainer) {
+ ViewGroup.LayoutParams layoutParams = viewContainer.getLayoutParams();
+ if (layoutParams instanceof MarginLayoutParams) {
+ ((MarginLayoutParams) layoutParams).topMargin = topInset;
+ viewContainer.setLayoutParams(layoutParams);
+ }
+ }
+
+ /**
+ * Returns the margin value to be applied on the left and right side to set a view's width to be a
+ * fraction of the screen width.
+ */
+ public static int getAdaptiveMargin(Context context, float containerWidthFraction) {
+ Resources resources = context.getResources();
+ if (resources == null) {
+ return 0;
+ }
+ int screenWidth = resources.getDisplayMetrics().widthPixels;
+
+ float marginFraction = (1.f - containerWidthFraction) / 2;
+ return (int) (screenWidth * marginFraction);
+ }
+
+ private PresenterUtils() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java
new file mode 100644
index 0000000..ea049fd
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/RowListWrapperTemplatePresenter.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.annotation.SuppressLint;
+import android.content.res.TypedArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapperTemplate;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.HeaderView;
+import java.util.List;
+
+/** A {@link TemplatePresenter} for {@link RowListWrapperTemplate} instances. */
+public class RowListWrapperTemplatePresenter extends AbstractTemplatePresenter {
+ // TODO(b/183643108): Use a common value for this constant
+ private static final int MAX_ALLOWED_ACTIONS = 2;
+
+ private final ViewGroup mRootView;
+ private final HeaderView mHeaderView;
+ private final ContentView mContentView;
+ private final ActionButtonListView mStickyActionButtonListView;
+ private final ActionButtonListParams mActionButtonListParams;
+
+ /** Create a RowListWrapperTemplatePresenter */
+ public static RowListWrapperTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ RowListWrapperTemplatePresenter presenter =
+ new RowListWrapperTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ private void update() {
+ RowListWrapperTemplate template = (RowListWrapperTemplate) getTemplate();
+ ActionStrip actionStrip = template.getActionStrip();
+ RowListWrapper list = template.getList();
+
+ List<Action> actionList = template.getActionList();
+ if (actionList != null && !actionList.isEmpty()) {
+ mStickyActionButtonListView.setVisibility(VISIBLE);
+ mStickyActionButtonListView.setActionList(
+ getTemplateContext(), actionList, mActionButtonListParams);
+ } else {
+ mStickyActionButtonListView.setVisibility(GONE);
+ }
+
+ mHeaderView.setActionStrip(actionStrip, template.getActionsConstraints());
+ mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction());
+
+ mContentView.setRowListContent(getTemplateContext(), list);
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"methodref.receiver.bound.invalid"})
+ private RowListWrapperTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.LIGHT);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext)
+ .inflate(R.layout.row_list_wrapper_template_layout, null);
+ mContentView = mRootView.findViewById(R.id.content_view);
+ View contentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView = HeaderView.install(templateContext, contentContainer);
+
+ @StyleableRes final int[] themeAttrs = {R.attr.templatePlainContentBackgroundColor};
+
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ @ColorInt int surroundingColor = ta.getColor(0, 0);
+ ta.recycle();
+
+ mActionButtonListParams =
+ ActionButtonListParams.builder()
+ .setMaxActions(MAX_ALLOWED_ACTIONS)
+ .setOemReorderingAllowed(true)
+ .setOemColorOverrideAllowed(true)
+ .setSurroundingColor(surroundingColor)
+ .build();
+
+ mStickyActionButtonListView = mRootView.requireViewById(R.id.sticky_action_button_list_view);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java
new file mode 100644
index 0000000..fbb5f84
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SearchTemplatePresenter.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.text.InputType.TYPE_CLASS_TEXT;
+import static android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+import static android.view.View.VISIBLE;
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_RENDER_TITLE_AS_SECONDARY;
+
+import android.annotation.SuppressLint;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.SearchTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.android.car.libraries.apphost.input.CarEditable;
+import com.android.car.libraries.apphost.input.CarEditableListener;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.SearchHeaderView;
+
+/**
+ * A {@link TemplatePresenter} presenter which controls the {@link InputManager} based on values in
+ * the {@link SearchTemplate} model provided via {@link #update}.
+ */
+public class SearchTemplatePresenter extends AbstractTemplatePresenter implements CarEditable {
+
+ private final InputManager mInputManager;
+ private final ViewGroup mRootView;
+ private final SearchHeaderView mHeaderView;
+ private final ContentView mContentView;
+
+ private String mSearchHint;
+ private final String mDisabledSearchHint;
+
+ private boolean mInputWasActiveOnLastWindowFocus;
+
+ /** Creates a SearchTemplatePresenter */
+ public static SearchTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ SearchTemplatePresenter presenter =
+ new SearchTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+
+ return presenter;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventManager eventManager = getTemplateContext().getEventManager();
+ eventManager.subscribeEvent(
+ this,
+ EventType.WINDOW_FOCUS_CHANGED,
+ () -> {
+ if (hasWindowFocus()) {
+ // If the input was active the last time the window was focused, it means
+ // that the user just dismissed the car screen keyboard. In this case, focus
+ // on the search result list.
+ if (mInputWasActiveOnLastWindowFocus) {
+ mContentView.requestFocus();
+ }
+ }
+ mInputWasActiveOnLastWindowFocus = mInputManager.isInputActive();
+ });
+ }
+
+ @Override
+ public void onStop() {
+ // TODO(b/182232738): Reenable keyboard listener
+ // LocationManager locationManager = LocationManager.getInstance();
+ // locationManager.removeKeyboardEnabledListener(driveStatusEventListener);
+ getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED);
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ mInputManager.stopInput();
+ super.onPause();
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ // Hide the cursor by clearing the edit text focus if input is not active
+ if (!mInputManager.isInputActive()) {
+ mHeaderView.getSearchBar().clearFocus();
+ }
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+ return mHeaderView.onCreateInputConnection(editorInfo);
+ }
+
+ @Override
+ public void setCarEditableListener(CarEditableListener listener) {}
+
+ @Override
+ public void setInputEnabled(boolean enabled) {}
+
+ private void update() {
+ SearchTemplate searchTemplate = (SearchTemplate) getTemplate();
+ ActionStrip actionStrip = searchTemplate.getActionStrip();
+ TemplateContext templateContext = getTemplateContext();
+
+ // Store the hint so we can set it again when the keyboard is enabled. Use a local variable
+ // so only one call to getSearchHint and null checker doesn't complain.
+ String tempSearchHint = searchTemplate.getSearchHint();
+ mSearchHint =
+ tempSearchHint == null
+ ? templateContext.getString(templateContext.getHostResourceIds().getSearchHintText())
+ : tempSearchHint;
+ updateSearchHint(mHeaderView.getSearchBar().isEnabled());
+
+ mHeaderView.setActionStrip(actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+
+ mHeaderView.setAction(searchTemplate.getHeaderAction());
+
+ ItemList itemList = searchTemplate.getItemList();
+ boolean isEmptyList = false;
+ if (itemList != null && itemList.getItems().isEmpty()) {
+ // If the list is empty, use the first row to display the no-items message.
+ itemList = getItemListWithEmptyTextRow(itemList.getNoItemsMessage());
+ isEmptyList = true;
+ }
+
+ RowListWrapper.Builder builder =
+ RowListWrapper.wrap(templateContext, itemList)
+ .setIsLoading(searchTemplate.isLoading())
+ .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+ .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE)
+ .setIsRefresh(getTemplateWrapper().isRefresh());
+ if (isEmptyList) {
+ builder.setListFlags(LIST_FLAGS_RENDER_TITLE_AS_SECONDARY);
+ }
+
+ mContentView.setRowListContent(templateContext, builder.build());
+ }
+
+ private void updateSearchHint(boolean searchEnabled) {
+ mHeaderView.setHint(searchEnabled ? mSearchHint : mDisabledSearchHint);
+ }
+
+ /**
+ * Returns an {@link ItemList} that has the no-item message sent as the text on the first row.
+ *
+ * <p>If the input no-item message is {@code null}, a default message will be added instead.
+ */
+ private ItemList getItemListWithEmptyTextRow(@Nullable CarText customNoItemMessage) {
+ // Set the title to be empty because the no-item message should be rendered as secondary
+ // text.
+ String message =
+ getTemplateContext()
+ .getString(getTemplateContext().getHostResourceIds().getTemplateListNoItemsText());
+ return new ItemList.Builder()
+ .addItem(
+ new Row.Builder()
+ .setTitle(
+ customNoItemMessage == null ? message : customNoItemMessage.toCharSequence())
+ .build())
+ .build();
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"argument.type.incompatible", "method.invocation.invalid"})
+ private SearchTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.GONE);
+
+ mInputManager = templateContext.getInputManager();
+
+ mRootView =
+ (ViewGroup) LayoutInflater.from(templateContext).inflate(R.layout.search_layout, null);
+ mContentView = mRootView.findViewById(R.id.content_view);
+ mDisabledSearchHint =
+ templateContext.getString(templateContext.getHostResourceIds().getSearchHintDisabledText());
+
+ SearchTemplate searchTemplate = (SearchTemplate) templateWrapper.getTemplate();
+ View contentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView =
+ SearchHeaderView.install(
+ templateContext,
+ contentContainer,
+ mRootView,
+ searchTemplate.getInitialSearchText(),
+ searchTemplate.getSearchCallbackDelegate(),
+ searchTemplate.isShowKeyboardByDefault());
+ int inputType =
+ mHeaderView.getSearchBar().getInputType() | TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+ mHeaderView.getSearchBar().setInputType(inputType);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java
new file mode 100644
index 0000000..4a09650
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/SignInTemplatePresenter.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.common;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.res.TypedArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import androidx.annotation.ColorInt;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.signin.InputSignInMethod;
+import androidx.car.app.model.signin.PinSignInMethod;
+import androidx.car.app.model.signin.ProviderSignInMethod;
+import androidx.car.app.model.signin.QRCodeSignInMethod;
+import androidx.car.app.model.signin.SignInTemplate;
+import androidx.car.app.model.signin.SignInTemplate.SignInMethod;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView.Gravity;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView;
+import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils;
+import com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer;
+import com.android.car.libraries.templates.host.view.widgets.common.HeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.InputSignInView;
+import com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout;
+import com.android.car.libraries.templates.host.view.widgets.common.PinSignInView;
+import com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.List;
+
+/** A {@link TemplatePresenter} for {@link SignInTemplate} instances. */
+public class SignInTemplatePresenter extends AbstractTemplatePresenter {
+ // TODO(b/183643108): Use a common MAX_ALLOWED_ACTIONS between AAOS and AAP
+ private static final int MAX_ALLOWED_ACTIONS = 2;
+ private static final CarTextParams ADDITIONAL_TEXT_PARAMS =
+ CarTextParams.builder().setAllowClickableSpans(true).build();
+
+ private final InputManager mInputManager;
+ private final ViewGroup mRootView;
+ private final HeaderView mHeaderView;
+ private final LinearLayout mSignInContainer;
+ private final CarUiTextView mInstructionTextView;
+ private final ActionButtonView mProviderSignInButton;
+ private final InputSignInView mInputSignInView;
+ private final PinSignInView mPinSignInView;
+ private final QRCodeSignInView mQRCodeSignInView;
+ private final ClickableSpanTextContainer mAdditionalTextView;
+ private final ActionButtonListView mActionListView;
+ private final ParkedOnlyFrameLayout mContentContainer;
+ private final ProgressBar mProgressBar;
+ private final String mDisabledInputHint;
+ private final ActionButtonListParams mActionButtonListParams;
+ private final CarTextParams mInstructionTextParams;
+ private boolean mInputWasActiveOnLastWindowFocus;
+
+ /** Create a {@link SignInTemplatePresenter} instance. */
+ static SignInTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ SignInTemplatePresenter presenter =
+ new SignInTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mProviderSignInButton.getVisibility() == VISIBLE) {
+ return mProviderSignInButton;
+ }
+ if (mInputSignInView.getVisibility() == VISIBLE) {
+ // Hide the cursor by clearing the edit text focus if input is not active
+ if (!mInputManager.isInputActive()) {
+ mInputSignInView.clearEditTextFocus();
+ }
+ }
+ if (mPinSignInView.getVisibility() == VISIBLE) {
+ return mPinSignInView;
+ }
+ if (mActionListView.getVisibility() == VISIBLE) {
+ return mActionListView;
+ }
+
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventManager eventManager = getTemplateContext().getEventManager();
+ eventManager.subscribeEvent(
+ this,
+ EventType.WINDOW_FOCUS_CHANGED,
+ () -> {
+ if (hasWindowFocus()) {
+ // If the input was active the last time the window was focused, it means
+ // that the user just dismissed the car screen keyboard. In this case, focus
+ // on the action button list.
+ if (mInputWasActiveOnLastWindowFocus) {
+ mActionListView.requestFocus();
+ }
+ }
+ mInputWasActiveOnLastWindowFocus = mInputManager.isInputActive();
+ });
+ }
+
+ @Override
+ public void onStop() {
+ // TODO(b/182232738): Reenable keyboard listener
+ // LocationManager locationManager = LocationManager.getInstance();
+ // locationManager.removeKeyboardEnabledListener(driveStatusEventListener);
+ getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED);
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ mInputManager.stopInput();
+ super.onPause();
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ /** Updates the view with current values in the {@link SignInTemplate}. */
+ private void update() {
+ TemplateContext templateContext = getTemplateContext();
+ SignInTemplate template = (SignInTemplate) getTemplate();
+
+ setHeaderView(templateContext, template);
+ setProgressBar(template);
+ setInstructionText(templateContext, template);
+ setSignInView(templateContext, template);
+ setAdditionalText(templateContext, template);
+ setActionListButtons(templateContext, template);
+ }
+
+ private void setHeaderView(TemplateContext templateContext, SignInTemplate template) {
+ // If we have a title or a header action, show the header; hide it otherwise.
+ CarText title = template.getTitle();
+ Action headerAction = template.getHeaderAction();
+ if (!CarText.isNullOrEmpty(title) || headerAction != null) {
+ mHeaderView.setContent(templateContext, title, headerAction);
+ } else {
+ mHeaderView.setContent(templateContext, null, null);
+ }
+ mHeaderView.setActionStrip(
+ template.getActionStrip(), ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ }
+
+ private void setProgressBar(SignInTemplate template) {
+ if (template.isLoading()) {
+ mProgressBar.setVisibility(VISIBLE);
+ mSignInContainer.setVisibility(GONE);
+ } else {
+ mProgressBar.setVisibility(GONE);
+ mSignInContainer.setVisibility(VISIBLE);
+ }
+ }
+
+ private void setInstructionText(TemplateContext templateContext, SignInTemplate template) {
+ CarText instructions = template.getInstructions();
+ if (!CarText.isNullOrEmpty(instructions)) {
+ mInstructionTextView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext,
+ instructions,
+ mInstructionTextParams,
+ mInstructionTextView.getMaxLines()));
+ mInstructionTextView.setVisibility(VISIBLE);
+ } else {
+ mInstructionTextView.setVisibility(GONE);
+ }
+ }
+
+ private void setSignInView(TemplateContext templateContext, SignInTemplate template) {
+ // Reset the sign-in view
+ mProviderSignInButton.setVisibility(GONE);
+ mInputSignInView.setVisibility(GONE);
+ mPinSignInView.setVisibility(GONE);
+ mQRCodeSignInView.setVisibility(GONE);
+ SignInMethod signInMethod = template.getSignInMethod();
+ if (signInMethod instanceof ProviderSignInMethod) {
+ ProviderSignInMethod providerSignInMethod = (ProviderSignInMethod) signInMethod;
+ Action providerSignInAction = providerSignInMethod.getAction();
+ // OEMs cannot overwrite provider method button in Sign in template
+ mProviderSignInButton.setAction(
+ templateContext,
+ providerSignInAction,
+ ActionButtonListParams.builder().setAllowAppColor(true).build());
+ mProviderSignInButton.setVisibility(VISIBLE);
+ } else if (signInMethod instanceof InputSignInMethod) {
+ InputSignInMethod inputSignInMethod = (InputSignInMethod) signInMethod;
+ mInputSignInView.setSignInMethod(
+ templateContext,
+ inputSignInMethod,
+ mInputManager,
+ mDisabledInputHint,
+ getTemplateWrapper().isRefresh());
+ mInputSignInView.setVisibility(VISIBLE);
+ } else if (signInMethod instanceof PinSignInMethod) {
+ PinSignInMethod pinSignInMethod = (PinSignInMethod) signInMethod;
+ mPinSignInView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, pinSignInMethod.getPinCode(), mPinSignInView.getMaxLines()));
+ mPinSignInView.setVisibility(VISIBLE);
+ } else if (signInMethod instanceof QRCodeSignInMethod) {
+ QRCodeSignInMethod qrCodeSignInMethod = (QRCodeSignInMethod) signInMethod;
+ mQRCodeSignInView.setQRCodeSignInMethod(templateContext, qrCodeSignInMethod);
+ mQRCodeSignInView.setVisibility(VISIBLE);
+ } else {
+ L.w(LogTags.TEMPLATE, "Unknown sign in method: %s", signInMethod);
+ }
+ }
+
+ private void setAdditionalText(TemplateContext templateContext, SignInTemplate template) {
+ CarText additionalText = template.getAdditionalText();
+
+ if (!CarText.isNullOrEmpty(additionalText)) {
+ mAdditionalTextView.setText(
+ CarTextUtils.toCharSequenceOrEmpty(
+ templateContext, additionalText, ADDITIONAL_TEXT_PARAMS));
+ mAdditionalTextView.setVisibility(VISIBLE);
+ } else {
+ mAdditionalTextView.setVisibility(GONE);
+ }
+ }
+
+ private void setActionListButtons(TemplateContext templateContext, SignInTemplate template) {
+ List<Action> actionList = template.getActions();
+ if (!actionList.isEmpty()) {
+ mActionListView.setActionList(templateContext, actionList, mActionButtonListParams);
+ mActionListView.setVisibility(VISIBLE);
+ } else {
+ mActionListView.setVisibility(GONE);
+ }
+ }
+
+ @SuppressWarnings("nullness:method.invocation")
+ private SignInTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.LIGHT);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionButtonListGravity, R.attr.templatePlainContentBackgroundColor
+ };
+
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ ActionButtonListView.Gravity actionButtonListGravity =
+ ActionButtonListView.Gravity.values()[ta.getInt(0, 0)];
+ @ColorInt int backgroundColor = ta.getColor(1, 0);
+ ta.recycle();
+
+ mInputManager = templateContext.getInputManager();
+ mDisabledInputHint =
+ templateContext.getString(templateContext.getHostResourceIds().getSearchHintDisabledText());
+ mActionButtonListParams =
+ ActionButtonListParams.builder()
+ .setMaxActions(MAX_ALLOWED_ACTIONS)
+ .setOemReorderingAllowed(false)
+ .setOemColorOverrideAllowed(false)
+ .setSurroundingColor(backgroundColor)
+ .build();
+ mInstructionTextParams =
+ CarTextParams.builder()
+ .setColorSpanConstraints(CarColorConstraints.STANDARD_ONLY)
+ .setBackgroundColor(backgroundColor)
+ .build();
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext).inflate(R.layout.sign_in_template_layout, null);
+ mSignInContainer = mRootView.findViewById(R.id.sign_in_container);
+ mInstructionTextView = mRootView.findViewById(R.id.instruction_text);
+ mProviderSignInButton = mRootView.findViewById(R.id.provider_sign_in_button);
+ mInputSignInView = mRootView.findViewById(R.id.input_sign_in_view);
+ mPinSignInView = mRootView.findViewById(R.id.pin_sign_in_view);
+ mQRCodeSignInView = mRootView.findViewById(R.id.qr_code_sign_in_view);
+ mAdditionalTextView = mRootView.findViewById(R.id.additional_text);
+ mContentContainer = mRootView.findViewById(R.id.park_only_container);
+ mHeaderView = HeaderView.install(templateContext, mContentContainer);
+ mContentContainer.setTemplateContext(templateContext);
+ mProgressBar = mRootView.findViewById(R.id.sign_in_progress_bar);
+ mActionListView =
+ actionButtonListGravity == Gravity.CENTER
+ ? mRootView.findViewById(R.id.action_button_list_view)
+ : mRootView.findViewById(R.id.sticky_action_button_list_view);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml
new file mode 100644
index 0000000..b471d0d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_bug_report_grey600_24dp.xml
@@ -0,0 +1,10 @@
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24"
+ android:width="24dp">
+ <path
+ android:fillColor="@color/default_gray_600"
+ android:pathData="M20,10L20,8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01 -0.16,-0.04 -0.32,-0.06 -0.49,-0.09l-0.17,-0.03C12.46,5.02 12.23,5 12,5c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3 7,4.41l1.62,1.63h0.01c-0.75,0.5 -1.37,1.18 -1.82,1.96L4,8v2h2.09c-0.06,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10zM16,15c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4s4,1.79 4,4v4zM10,14h4v2h-4zM10,10h4v2h-4z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml
new file mode 100644
index 0000000..e1ff4ac
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_mic_black_48dp.xml
@@ -0,0 +1,4 @@
+<vector android:height="48dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#000000" android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml
new file mode 100644
index 0000000..ee5bc0e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_report_problem_black_48dp.xml
@@ -0,0 +1,4 @@
+<vector android:height="48dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#000000" android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M12,2L1,21h22L12,2zM13,16h-2v2h2v-2zM13,10h-2v4h2v-4z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml
new file mode 100644
index 0000000..c58cae9
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/drawable/ic_volume_up_black_48dp.xml
@@ -0,0 +1,4 @@
+<vector android:height="48dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#000000" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM10,8.83v6.34L7.83,13L5,13v-2h2.83L10,8.83zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77 0,-4.28 -2.99,-7.86 -7,-8.77z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml
new file mode 100644
index 0000000..3795876
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/grid_wrapper_template_layout.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ style="?attr/templatePlainContentContainerStyle"
+ android:orientation="vertical">
+
+ <!-- A container for the contents of the screen, used as the container for
+ the header view. -->
+ <LinearLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/grid_content_view"
+ layout="@layout/content_view"
+ android:layout_marginTop="@dimen/car_ui_padding_3"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml
new file mode 100644
index 0000000..8641f81
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_action_layout.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+ <include
+ android:id="@+id/action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical"
+ android:layout_marginBottom="@dimen/template_padding_2" />
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml
new file mode 100644
index 0000000..f087180
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_layout.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CarUiTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/message_text"
+ style="?templateMessageLongTextStyle"
+ android:layout_gravity="start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:foreground="@drawable/no_content_view_focus_ring" />
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml
new file mode 100644
index 0000000..68c5af4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/long_message_template_layout.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="?attr/templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- A container for the contents of the screen, used as the container for
+ the header view. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout
+ android:id="@+id/park_only_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.android.car.ui.FocusArea
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clickable="true"
+ android:paddingTop="@dimen/template_padding_3"
+ android:clipToPadding="true"
+ app:layoutStyle="linear"/>
+
+ <include
+ android:id="@+id/sticky_action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateStickyButtonsVerticalSpacing"
+ android:layout_marginBottom="?templateStickyButtonsVerticalSpacing"
+ android:visibility="gone" />
+ </com.android.car.ui.FocusArea>
+ </com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout>
+</FrameLayout>
+
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml
new file mode 100644
index 0000000..caca61d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/message_template_layout.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- A container for the contents of the screen, used so we can implement
+ margins that adapt to different screen sizes. Used as a container for
+ the header view.-->
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- A container for the template elements that is centered in the screen. -->
+ <com.android.car.ui.FocusArea
+ android:id="@+id/message_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="?templatePlainContentHorizontalPadding"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/sticky_action_button_focus_area"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:ignore="UselessParent">
+
+ <!-- A progress indicator shown in place of the icon if the message
+ template is in loading state-->
+ <FrameLayout
+ android:id="@+id/progress_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:focusable="true"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinSize="?templateLargeImageSizeMin"
+ app:imageMaxSize="?templateLargeImageSizeMax"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <!-- An icon shown on top of the contents. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/message_icon"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinWidth="?templateLargeImageSizeMin"
+ app:imageMaxWidth="?templateLargeImageSizeMax"
+ app:imageMinHeight="?templateLargeImageSizeMin"
+ app:imageMaxHeight="?templateLargeImageSizeMax"
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+ <!-- The title displayed below the icon. -->
+ <CarUiTextView
+ android:id="@+id/message_text"
+ style="?templateMessageTitleTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="?templateMessageTitleTopSpacing"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <include
+ android:id="@+id/action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateMessageButtonsTopSpacing"
+ android:visibility="gone" />
+ </com.android.car.ui.FocusArea>
+
+ <!-- Stack trace section, shown only in debug when clicking on the
+ action to view it. -->
+ <ScrollView
+ android:id="@+id/stack_trace_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/template_padding_4"
+ android:layout_marginBottom="@dimen/template_padding_1"
+ android:layout_marginStart="@dimen/template_width_keyline_2"
+ android:layout_marginEnd="@dimen/template_width_keyline_2"
+ android:padding="@dimen/template_padding_1"
+ android:focusable="false"
+ android:layout_gravity="top"
+ android:visibility="gone"
+ android:background="?templateDebugMessageBackgroundColor"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:ignore="RtlHardcoded">
+
+ <HorizontalScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <CarUiTextView
+ android:id="@+id/stack_trace"
+ style="?templateMessageDebugTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </HorizontalScrollView>
+ </ScrollView>
+ <com.android.car.ui.FocusArea
+ android:id="@+id/sticky_action_button_focus_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+ <include
+ android:id="@+id/sticky_action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateStickyButtonsVerticalSpacing"
+ android:layout_marginBottom="?templateStickyButtonsVerticalSpacing"
+ android:visibility="gone" />
+ </com.android.car.ui.FocusArea>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
+
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml
new file mode 100644
index 0000000..84669a8
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/row_list_wrapper_template_layout.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="?attr/templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- A container for the contents of the screen, used as the container for
+ the header view. -->
+ <LinearLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/content_view"
+ layout="@layout/content_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <com.android.car.ui.FocusArea
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <include
+ android:id="@+id/sticky_action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateStickyButtonsVerticalSpacing"
+ android:layout_marginBottom="?templateStickyButtonsVerticalSpacing"
+ android:visibility="gone" />
+ </com.android.car.ui.FocusArea>
+ </LinearLayout>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml
new file mode 100644
index 0000000..3bfd482
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/search_layout.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="?templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <include
+ android:id="@+id/content_view"
+ layout="@layout/content_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml
new file mode 100644
index 0000000..a8c56b7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/sign_in_template_layout.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- A container for the contents of the screen, used so we can implement
+ margins that adapt to different screen sizes. Used as a container for
+ the header view. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout
+ android:id="@+id/park_only_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- This FrameLayout's visibility is modified only by
+ ParkedOnlyFrameLayout -->
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- The loading spinner. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/sign_in_progress_bar"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinSize="?templateLargeImageSizeMin"
+ app:imageMaxSize="?templateLargeImageSizeMax"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone"/>
+
+ <!-- A container for the template elements that is centered in the
+ screen. -->
+ <LinearLayout
+ android:id="@+id/sign_in_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="?templateSignInContainerStyle"
+ android:orientation="vertical"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/sticky_action_button_list_view"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:ignore="UselessParent">
+
+ <!-- The instruction text. -->
+ <CarUiTextView
+ android:id="@+id/instruction_text"
+ style="?templateSignInInstructionTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:layout_marginTop="@dimen/template_padding_3"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <!-- Provider Sign In Method -->
+ <include
+ android:id="@+id/provider_sign_in_button"
+ layout="@layout/sign_in_button_view"
+ android:layout_width="wrap_content"
+ android:layout_height="?templateActionButtonHeight"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical"
+ android:visibility="gone" />
+
+ <!-- Input Sign In Method -->
+ <include
+ android:id="@+id/input_sign_in_view"
+ layout="@layout/input_sign_in_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical"
+ android:visibility="gone" />
+
+ <!-- PIN Sign In Method -->
+ <include
+ android:id="@+id/pin_sign_in_view"
+ layout="@layout/pin_sign_in_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical"
+ android:visibility="gone" />
+
+ <!-- QR Code Sign In Method -->
+ <include
+ android:id="@+id/qr_code_sign_in_view"
+ layout="@layout/qr_code_sign_in_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_control_spacing_vertical"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/additional_text"
+ layout="@layout/clickable_span_text_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_control_to_text_spacing_vertical" />
+
+ <include
+ android:id="@+id/action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_app_ui_text_to_secondary_control_spacing_vertical"
+ android:visibility="gone" />
+ </LinearLayout>
+ <include
+ android:id="@+id/sticky_action_button_list_view"
+ layout="@layout/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateStickyButtonsVerticalSpacing"
+ android:layout_marginBottom="?templateStickyButtonsVerticalSpacing"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.android.car.libraries.templates.host.view.widgets.common.ParkedOnlyFrameLayout>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml
new file mode 100644
index 0000000..11b7e8e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res/layout/voice_layout.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/templatePlainContentContainerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/card_header_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<!-- A container for the contents of the screen, used so we can implement
+ margins that adapt to different screen sizes. -->
+ <FrameLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- A container for the template elements that is centered in the screen. We ignore
+ UseCompoundDrawables since are image needs an id to change with the VoiceTemplate state -->
+ <LinearLayout
+ android:id="@+id/button_with_description"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ tools:ignore="UselessParent,UseCompoundDrawables">
+
+ <!-- An icon shown on top of the contents. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/voice_button"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinWidth="?templateLargeImageSizeMin"
+ app:imageMaxWidth="?templateLargeImageSizeMax"
+ app:imageMinHeight="?templateLargeImageSizeMin"
+ app:imageMaxHeight="?templateLargeImageSizeMax"
+ tools:ignore="ContentDescription"/>
+
+ <!-- The title displayed below the icon. -->
+ <CarUiTextView
+ android:id="@+id/state_description"
+ style="?templateMessageTitleTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="?templateMessageTitleTopSpacing"/>
+ </LinearLayout>
+ </FrameLayout>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java
new file mode 100644
index 0000000..7ab0e12
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/MapsTemplatePresenterFactory.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.maps;
+
+import android.content.Context;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.PlaceListMapTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.Lifecycle.State;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenterFactory;
+import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.di.MapViewContainerFactory;
+import com.google.common.collect.ImmutableSet;
+import dagger.hilt.android.scopes.ServiceScoped;
+import java.util.Collection;
+import javax.inject.Inject;
+
+/**
+ * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for
+ * providing {@link TemplatePresenter} instances for the set of templates the host supports.
+ */
+@ServiceScoped
+public class MapsTemplatePresenterFactory implements TemplatePresenterFactory {
+
+ private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES =
+ ImmutableSet.of(PlaceListMapTemplate.class);
+
+ /** Boolean to trigger an one-time preload of MapView rendering code. */
+ private static boolean sMapViewPreloaded;
+
+ private final MapViewContainerFactory mMapViewContainerFactory;
+
+ @Inject
+ MapsTemplatePresenterFactory(MapViewContainerFactory mapViewContainerFactory) {
+ mMapViewContainerFactory = mapViewContainerFactory;
+ }
+
+ @VisibleForTesting
+ public static boolean isMapViewPreloaded() {
+ return sMapViewPreloaded;
+ }
+
+ /**
+ * Performance optimization: preload some of the MapView rendering code that requires one-time
+ * static initialization.
+ *
+ * <p>This helps to speed up MapView being loaded when the {@link PlaceListMapTemplate} is
+ * actually used.
+ */
+ @MainThread
+ public void preloadMapView(Context context) {
+ if (sMapViewPreloaded) {
+ L.d(LogTags.TEMPLATE, "MapView previously preloaded. Skipping.");
+ return;
+ }
+
+ AbstractMapViewContainer mapViewContainer =
+ mMapViewContainerFactory.create(context, R.style.Theme_Template);
+ // onCreate triggers the mapView.getMapAsync call to initialize the mapView.
+ mapViewContainer.getLifecycleRegistry().setCurrentState(State.CREATED);
+ // make sure the mapView is properly cleaned up to avoid any possible leaks.
+ mapViewContainer.getLifecycleRegistry().setCurrentState(State.DESTROYED);
+ sMapViewPreloaded = true;
+ }
+
+ @Override
+ @Nullable
+ public TemplatePresenter createPresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ Class<? extends Template> clazz = templateWrapper.getTemplate().getClass();
+
+ if (PlaceListMapTemplate.class == clazz) {
+ return PlaceListMapTemplatePresenter.create(
+ templateContext, templateWrapper, mMapViewContainerFactory);
+ } else {
+ L.w(
+ LogTags.TEMPLATE,
+ "Don't know how to create a presenter for template: %s",
+ clazz.getSimpleName());
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return SUPPORTED_TEMPLATES;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java
new file mode 100644
index 0000000..a5205c6
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/PlaceListMapTemplatePresenter.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.maps;
+
+import static android.view.View.VISIBLE;
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.transition.TransitionInflater;
+import android.transition.TransitionManager;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.PlaceListMapTemplate;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.di.MapViewContainerFactory;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView;
+import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.google.common.collect.ImmutableList;
+
+/** A {@link TemplatePresenter} that shows a map view with pins for locations. */
+public class PlaceListMapTemplatePresenter extends AbstractTemplatePresenter {
+ private final ViewGroup mRootView;
+ private final ViewGroup mCardContainer;
+ private final ActionStripView mActionStripView;
+ private final CardHeaderView mHeaderView;
+ private final ContentView mContentView;
+ // This is lazy-initiated during every onStart instead of just in the ctor. For some reason the
+ // map view is not laid out when the user exits the app and comes back to the template,
+ // preventing the map from updating from place changes. Therefore as a workaround we just
+ // re-create the map evertime the user comes back. See b/178606261 for more details.
+ @Nullable private AbstractMapViewContainer mMapContainer;
+ private final OnGlobalLayoutListener mGlobalLayoutListener;
+ private final MapViewContainerFactory mMapViewContainerFactory;
+
+ /** Creates a {@link PlaceListMapTemplatePresenter}. */
+ public static PlaceListMapTemplatePresenter create(
+ TemplateContext templateContext,
+ TemplateWrapper templateWrapper,
+ MapViewContainerFactory mapViewContainerFactory) {
+ PlaceListMapTemplatePresenter presenter =
+ new PlaceListMapTemplatePresenter(
+ templateContext, templateWrapper, mapViewContainerFactory);
+ presenter.update();
+ return presenter;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public AbstractMapViewContainer getMapContainer() {
+ return mMapContainer;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ updateMapContainerLifeCycle(State.CREATED);
+ }
+
+ @Override
+ public void onDestroy() {
+ updateMapContainerLifeCycle(State.DESTROYED);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ updateMapContainerLifeCycle(State.STARTED);
+
+ EventManager eventManager = getTemplateContext().getEventManager();
+ eventManager.subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::refreshViews);
+ eventManager.subscribeEvent(this, EventType.PLACE_LIST, this::updatePlaces);
+
+ // Instantiating the map views during onStart as otherwise the map may not get laid out
+ // properly. See b/178606261 for more details.
+ refreshViews();
+ }
+
+ @Override
+ public void onStop() {
+ updateMapContainerLifeCycle(State.CREATED);
+ TemplateContext templateContext = getTemplateContext();
+
+ // Clear the list of places when transitioning out of this presenter.
+ // This prevents a flow when the app enters this presenter again, it will temporarily show
+ // the previous markers that were set.
+ requireNonNull(templateContext.getAppHostService(LocationMediator.class))
+ .setCurrentPlaces(ImmutableList.of());
+
+ templateContext.getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+ templateContext.getEventManager().unsubscribeEvent(this, EventType.PLACE_LIST);
+
+ super.onStop();
+ }
+
+ @Override
+ public void onPause() {
+ getView().getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
+ updateMapContainerLifeCycle(State.STARTED);
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getView().getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
+ updateMapContainerLifeCycle(State.RESUMED);
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+ // Move between the card view and the action button view on left or right rotary nudge.
+ if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // If the focus is in the card view (back button or row list), request focus in the
+ // action strip.
+ if (moveFocusIfPresent(
+ ImmutableList.of(mCardContainer), ImmutableList.of(mActionStripView))) {
+ return true;
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ // Request focus on the content view so that the first row in the list will take focus.
+ if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) {
+ return true;
+ }
+ }
+ return super.onKeyUp(keyCode, keyEvent);
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public boolean handlesTemplateChangeAnimation() {
+ // PlaceListMapTemplate has special behavior since we don't want to destroy the MapView for
+ // a refresh, and it handles the update motion correctly internally.
+ return true;
+ }
+
+ @Override
+ public boolean isFullScreen() {
+ return false;
+ }
+
+ /** Updates the locations in the map */
+ private void update() {
+ PlaceListMapTemplate mapTemplate = (PlaceListMapTemplate) getTemplate();
+ TemplateContext templateContext = getTemplateContext();
+
+ if (mapTemplate.isLoading()) {
+ // Clear the last list of places if we are in loading state so that we are not showing
+ // stale markers that do not correspond to the current list.
+ requireNonNull(templateContext.getAppHostService(LocationMediator.class))
+ .setCurrentPlaces(ImmutableList.of());
+ }
+
+ TransitionManager.beginDelayedTransition(
+ mRootView,
+ TransitionInflater.from(templateContext)
+ .inflateTransition(R.transition.map_template_transition));
+
+ mHeaderView.setContent(
+ templateContext,
+ mapTemplate.getTitle(),
+ mapTemplate.getHeaderAction(),
+ mapTemplate.getOnContentRefreshDelegate());
+
+ ItemList itemList = mapTemplate.getItemList();
+ RowListWrapper rowListWrapper =
+ RowListWrapper.wrap(templateContext, itemList)
+ .setIsLoading(mapTemplate.isLoading())
+ .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+ .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE)
+ .setIsRefresh(getTemplateWrapper().isRefresh())
+ .setIsHalfList(true)
+ .build();
+ mContentView.setRowListContent(templateContext, rowListWrapper);
+
+ updateMapSettings(mapTemplate);
+ updateActionStrip(mapTemplate.getActionStrip());
+ }
+
+ // TODO(b/159908673): add tests for the lifecycle management logic in here.
+ private void refreshViews() {
+ // Destroy the previous MapView based on this presenter's currently lifecycle events.
+ AbstractMapViewContainer previousMapContainer = mMapContainer;
+ if (previousMapContainer != null) {
+ previousMapContainer.getLifecycleRegistry().setCurrentState(State.DESTROYED);
+ mRootView.removeView(previousMapContainer);
+ }
+
+ Lifecycle lifecycle = getLifecycle();
+ if (lifecycle.getCurrentState() == State.DESTROYED) {
+ // View already destroyed. Don't bother refreshing the views.
+ return;
+ }
+
+ mMapContainer = mMapViewContainerFactory.create(getTemplateContext(), R.style.Theme_Template);
+
+ if (mMapContainer != null) {
+ mMapContainer.setTemplateContext(getTemplateContext());
+ mMapContainer.setId(R.id.map_container);
+ mRootView.addView(mMapContainer, 0);
+
+ // Update the new MapView's lifecycle events to match this presenter's, as that is
+ // required for the map instance to be initiated and shown.
+ mMapContainer.getLifecycleRegistry().setCurrentState(lifecycle.getCurrentState());
+ }
+
+ PlaceListMapTemplate mapTemplate = (PlaceListMapTemplate) getTemplate();
+ updateMapSettings(mapTemplate);
+ updateActionStrip(mapTemplate.getActionStrip());
+ }
+
+ private void updateMapSettings(PlaceListMapTemplate mapTemplate) {
+ AbstractMapViewContainer container = mMapContainer;
+ if (container != null) {
+ container.setCurrentLocationEnabled(mapTemplate.isCurrentLocationEnabled());
+ container.setAnchor(mapTemplate.getAnchor());
+ updatePlaces();
+ }
+ }
+
+ private void updatePlaces() {
+ AbstractMapViewContainer container = mMapContainer;
+ if (container != null) {
+ container.setPlaces(
+ requireNonNull(getTemplateContext().getAppHostService(LocationMediator.class))
+ .getCurrentPlaces());
+ }
+ }
+
+ private void updateActionStrip(@Nullable ActionStrip actionStrip) {
+ mActionStripView.setActionStrip(
+ getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ }
+
+ private void updateMapContainerLifeCycle(State state) {
+ AbstractMapViewContainer container = mMapContainer;
+ // TODO(b/180162594): Use ifNotNull when available.
+ if (container != null) {
+ container.getLifecycleRegistry().setCurrentState(state);
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"methodref.receiver.bound.invalid", "nullness"})
+ private PlaceListMapTemplatePresenter(
+ TemplateContext templateContext,
+ TemplateWrapper templateWrapper,
+ MapViewContainerFactory mapViewContainerFactory) {
+ super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext).inflate(R.layout.map_template_layout, null);
+ mCardContainer = mRootView.findViewById(R.id.card_container);
+ mHeaderView = mRootView.findViewById(R.id.header_view);
+ mContentView = mRootView.findViewById(R.id.content_view);
+ mActionStripView = mRootView.findViewById(R.id.action_strip);
+ mMapViewContainerFactory = mapViewContainerFactory;
+ // Note that the map container is instantiated during onStart.
+
+ // We should always show an ItemList.
+ mCardContainer.setVisibility(View.VISIBLE);
+
+ // Dynamically update the visible area inset. This allows the MapViewContainer to account
+ // for the insets when adjusting zoom levels to show all the place markers.
+ mGlobalLayoutListener =
+ () -> {
+ Rect safeAreaInset = new Rect();
+ // The content container is always visible so just use its right.
+ safeAreaInset.left = mCardContainer.getRight();
+ safeAreaInset.top =
+ mActionStripView.getVisibility() == VISIBLE
+ ? mActionStripView.getBottom()
+ : mRootView.getTop() + mRootView.getPaddingTop();
+ safeAreaInset.bottom = mRootView.getBottom() - mRootView.getPaddingBottom();
+ safeAreaInset.right = mRootView.getRight() - mRootView.getPaddingRight();
+ templateContext.getSurfaceInfoProvider().setVisibleArea(safeAreaInset);
+ };
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml
new file mode 100644
index 0000000..03aac7d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout-w1280dp-h1000dp/map_template_layout.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!--
+ The MapViewContainer is added programmatically here.
+ On configuration changes, the MapView is removed and re-added so that the
+ map can instantiate in the correct day/night mode.
+ TODO(b/159348229): update this once MapView has an explicit API to do this.
+ -->
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <include
+ android:id="@+id/card_container"
+ android:layout_width="?templateCardContentContainerDefaultWidth"
+ android:layout_height="0dp"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ app:layout_goneMarginTop="?templateCardContentContainerTopMargin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/action_strip"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_min="?templateCardContentContainerMinHeight"
+ layout="@layout/card_container"/>
+
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml
new file mode 100644
index 0000000..fb60c0f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/layout/map_template_layout.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!--
+ The MapViewContainer is added programmatically here.
+ On configuration changes, the MapView is removed and re-added so that the
+ map can instantiate in the correct day/night mode.
+ TODO(b/159348229): update this once MapView has an explicit API to do this.
+ -->
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <include
+ android:id="@+id/card_container"
+ layout="@layout/card_container"/>
+
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml
new file mode 100644
index 0000000..59e3103
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res/transition/map_template_transition.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together">
+ <targets>
+ <target android:excludeId="@id/map_container" />
+ </targets>
+ <fade
+ android:fadingMode="fade_in_out"
+ android:duration="?templateUpdateAnimationDurationMilliseconds"/>
+ <changeBounds
+ android:duration="?templateUpdateAnimationDurationMilliseconds"
+ android:interpolator="@interpolator/fast_out_slow_in"/>
+</transitionSet>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java
new file mode 100644
index 0000000..fe6aebc
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenter.java
@@ -0,0 +1,695 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.navigation;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.android.car.libraries.templates.host.view.widgets.common.ActionStripView.ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.SuppressLint;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.transition.TransitionInflater;
+import android.transition.TransitionManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import androidx.annotation.ColorInt;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.navigation.model.MessageInfo;
+import androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.navigation.model.NavigationTemplate.NavigationInfo;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.navigation.model.RoutingInfo;
+import androidx.car.app.navigation.model.TravelEstimate;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.animation.AnimationListenerAdapter;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView;
+import com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView;
+import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView;
+import com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView;
+import com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView;
+import com.android.car.libraries.templates.host.view.widgets.navigation.MessageView;
+import com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView;
+import com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link TemplatePresenter} that shows various navigation cards such as routing cards,
+ * destination cards etc.
+ */
+public class NavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter
+ implements ActionStripView.ActiveStateDelegate {
+ private static final int MAX_IMAGES_PER_TEXT_LINE = 2;
+
+ /** Percentage to lower the brightness of the card's background. */
+ private static final float CARD_BACKGROUND_DARKEN_PERCENTAGE = 0.2f;
+
+ /** Percentage to lower the brightness of the compact step section of the card's background. */
+ private static final float COMPACT_STEP_CARD_BACKGROUND_DARKEN_PERCENTAGE = 0.4f;
+
+ /** The ratio between the junction image max height to the routing card. */
+ private static final float JUNCTION_IMAGE_MAX_HEIGHT_TO_CARD_WIDTH_RATIO = 0.625f;
+
+ /** The ratio between the lanes image container height to the routing card. */
+ private static final float LANES_IMAGE_CONTAINER_HEIGHT_TO_CARD_WIDTH_RATIO = 0.175f;
+
+ /**
+ * {@link #showActionStripViews()} is called in {@link #onStart()}, but a bug in GMS core causes
+ * {@link View#isInTouchMode()} in {@link #onStart()} to return {@code true} even in rotary or
+ * touchpad mode (b/128031459), which prevents the action strip from taking the input focus. We
+ * use this listener to call {@link #showActionStripViews()} after the touch mode changes to the
+ * correct value.
+ */
+ private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+ new OnGlobalFocusChangeListener() {
+ // call to showActionStripView() not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (newFocus != null) {
+ showActionStripViews();
+ }
+ }
+ };
+
+ @ColorInt private final int mNavCardFallbackContentColor;
+
+ private int mStepsCardContainerVisibility = GONE;
+ private int mTravelEstimateContainerVisibility = GONE;
+
+ private final ViewGroup mRootView;
+ private final BleedingCardView mStepsCardContainer;
+ private final ViewGroup mStepsContainer;
+ private final MessageView mMessageView;
+ private final ProgressView mProgressView;
+ private final ViewGroup mTravelEstimateContainer;
+ private final ImageView mJunctionImageView;
+ private final FrameLayout mJunctionImageContainer;
+ private final FrameLayout mLanesImageContainerView;
+ private final DetailedStepView mDetailedStepView;
+ private final CompactStepView mCompactStepView;
+ private final TravelEstimateView mTravelEstimateView;
+ private final ActionStripView mActionStripView;
+ private final ActionStripView mMapActionStripView;
+ private final PanOverlayView mPanOverlay;
+ private final CarTextParams mCurrentStepParams;
+ private final CarTextParams mNextStepParams;
+ @ColorInt private final int mDefaultCardBackgroundColor;
+
+ /** Creates a {@link NavigationTemplatePresenter}. */
+ public static NavigationTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ NavigationTemplatePresenter presenter =
+ new NavigationTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ showActionStripViews();
+ EventManager eventManager = getTemplateContext().getEventManager();
+ eventManager.subscribeEvent(
+ this, EventType.TEMPLATE_TOUCHED_OR_FOCUSED, this::showActionStripViews);
+ eventManager.subscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED, this::showActionStripViews);
+ eventManager.subscribeEvent(
+ this,
+ EventType.CONFIGURATION_CHANGED,
+ () -> {
+ NavigationTemplate template = (NavigationTemplate) getTemplate();
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getMapActionStrip());
+ wrapActionStripsIfNeeded();
+ });
+ getView().getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ getTemplateContext().getRoutingInfoState().setIsRoutingInfoVisible(true);
+ }
+
+ @Override
+ public void onStop() {
+ getTemplateContext().getRoutingInfoState().setIsRoutingInfoVisible(false);
+ EventManager eventManager = getTemplateContext().getEventManager();
+ eventManager.unsubscribeEvent(this, EventType.TEMPLATE_TOUCHED_OR_FOCUSED);
+ eventManager.unsubscribeEvent(this, EventType.WINDOW_FOCUS_CHANGED);
+ eventManager.unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+ getView().getViewTreeObserver().removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+
+ super.onStop();
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public boolean isPanAndZoomEnabled() {
+ return getTemplateContext().getCarHostConfig().isNavPanZoomEnabled();
+ }
+
+ @Override
+ public void onPanModeChanged(boolean isInPanMode) {
+ showActionStripViews();
+ updateVisibility(isInPanMode);
+ dispatchPanModeChange(isInPanMode);
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ // Check the action strip visibility because the action buttons can take focus even when the
+ // action strip is gone.
+ if (mActionStripView.getVisibility() == VISIBLE) {
+ return mActionStripView;
+ }
+ if (mMapActionStripView.getVisibility() == VISIBLE) {
+ return mMapActionStripView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public void calculateAdditionalInset(Rect inset) {
+ // The portrait inset is more favorable to portrait screens it is calculated as the following
+ // bounding box:
+ // * left: inset left
+ // * right: Min(inset right, mapActionStrip left)
+ // * top: Max(inset top, actionStrip bottom, steps card bottom)
+ // * bottom: Min(inset bottom, travelEstimateContainer)
+ Rect portraitScreenInset = new Rect(inset);
+
+ // The landscape inset is more favorable to landscape screens it is calculated as the following
+ // bounding box:
+ // * left: Max(inset left, travelEstimateContainer, stepsContainer)
+ // * right: Min(inset right, mapActionStrip left)
+ // * top: Max(inset top, actionStrip bottom)
+ // * bottom: inset bottom
+ Rect landscapeScreenInset = new Rect(inset);
+
+ if (mMapActionStripView.getVisibility() == View.VISIBLE) {
+ landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft());
+ portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft());
+ }
+ if (mActionStripView.getVisibility() == VISIBLE) {
+ landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom());
+ portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom());
+ }
+ if (mTravelEstimateContainerVisibility == VISIBLE) {
+ portraitScreenInset.bottom =
+ min(portraitScreenInset.bottom, mTravelEstimateContainer.getTop());
+ landscapeScreenInset.left =
+ max(landscapeScreenInset.left, mTravelEstimateContainer.getRight());
+ }
+ if (mStepsCardContainerVisibility == View.VISIBLE) {
+ landscapeScreenInset.left = max(landscapeScreenInset.left, mStepsCardContainer.getRight());
+ portraitScreenInset.top = max(portraitScreenInset.top, mStepsCardContainer.getBottom());
+ }
+ int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width();
+ int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width();
+ inset.set(
+ landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset);
+ }
+
+ @Override
+ public void onActiveStateVisibilityChanged() {
+ requestVisibleAreaUpdate();
+ }
+
+ private void update() {
+ NavigationTemplate template = (NavigationTemplate) getTemplate();
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getMapActionStrip());
+ getPanZoomManager().setEnabled(hasPanButton());
+ setStepsCardBackgroundColor();
+ setStepsCardContentColor();
+
+ TransitionManager.beginDelayedTransition(
+ mRootView,
+ TransitionInflater.from(getTemplateContext())
+ .inflateTransition(R.transition.routing_card_transition));
+
+ @ColorInt int cardBackgroundColor = mStepsCardContainer.getCardBackgroundColor();
+ boolean shouldHideTravelEstimate = false;
+ NavigationInfo navigationInfo = template.getNavigationInfo();
+ if (navigationInfo == null) {
+ mStepsCardContainerVisibility = GONE;
+ mProgressView.setVisibility(GONE);
+ mStepsContainer.setVisibility(GONE);
+ mMessageView.setVisibility(GONE);
+ } else if (navigationInfo instanceof RoutingInfo) {
+ RoutingInfo routingInfo = (RoutingInfo) navigationInfo;
+
+ if (routingInfo.isLoading()) {
+ mStepsCardContainerVisibility = VISIBLE;
+ mProgressView.setVisibility(VISIBLE);
+ mStepsContainer.setVisibility(GONE);
+ mMessageView.setVisibility(GONE);
+ } else {
+
+ boolean shouldShowJunctionImage =
+ ImageUtils.setImageSrc(
+ getTemplateContext(),
+ routingInfo.getJunctionImage(),
+ mJunctionImageView,
+ ImageViewParams.DEFAULT);
+
+ boolean shouldShowNextStep = routingInfo.getNextStep() != null;
+
+ mDetailedStepView.setStepAndDistance(
+ getTemplateContext(),
+ routingInfo.getCurrentStep(),
+ routingInfo.getCurrentDistance(),
+ mCurrentStepParams,
+ cardBackgroundColor,
+ shouldShowJunctionImage);
+ mCompactStepView.setStep(
+ getTemplateContext(), routingInfo.getNextStep(), mNextStepParams, cardBackgroundColor);
+
+ if (shouldShowJunctionImage) {
+ mJunctionImageContainer.setVisibility(VISIBLE);
+ mCompactStepView.setVisibility(GONE);
+ shouldHideTravelEstimate = true;
+ } else {
+ mJunctionImageContainer.setVisibility(GONE);
+ mCompactStepView.setVisibility(shouldShowNextStep ? VISIBLE : GONE);
+ }
+
+ boolean hasNextStepOrJunction = shouldShowJunctionImage || shouldShowNextStep;
+ mStepsCardContainer
+ .findViewById(R.id.divider)
+ .setVisibility(hasNextStepOrJunction ? VISIBLE : GONE);
+
+ mStepsCardContainerVisibility = VISIBLE;
+ mProgressView.setVisibility(GONE);
+ mStepsContainer.setVisibility(VISIBLE);
+ mMessageView.setVisibility(GONE);
+ }
+ } else if (navigationInfo instanceof MessageInfo) {
+ MessageInfo messageInfo = (MessageInfo) navigationInfo;
+ CarText title = messageInfo.getTitle();
+ if (title == null) {
+ L.w(LogTags.TEMPLATE, "Title for the message is expected but not set");
+ title = CarText.create("");
+ }
+ mMessageView.setMessage(
+ getTemplateContext(),
+ messageInfo.getImage(),
+ title,
+ messageInfo.getText(),
+ cardBackgroundColor);
+ mStepsCardContainerVisibility = VISIBLE;
+ mProgressView.setVisibility(GONE);
+ mStepsContainer.setVisibility(GONE);
+ mMessageView.setVisibility(VISIBLE);
+ } else {
+ L.w(LogTags.TEMPLATE, "Unknown navigation info: %s", navigationInfo);
+ }
+
+ TravelEstimate travelEstimate = template.getDestinationTravelEstimate();
+ if (travelEstimate == null || shouldHideTravelEstimate) {
+ mTravelEstimateContainerVisibility = GONE;
+ } else {
+ mTravelEstimateView.setTravelEstimate(getTemplateContext(), travelEstimate);
+ mTravelEstimateContainerVisibility = VISIBLE;
+ }
+
+ updateVisibility(getPanZoomManager().isInPanMode());
+
+ // Wrap action strips after the visibility update, because we need to know if the routing
+ // card is visible in order to decide whether the action strips need to be wrapped.
+ wrapActionStripsIfNeeded();
+
+ getTemplateContext().getSurfaceInfoProvider().invalidateStableArea();
+ requestVisibleAreaUpdate();
+ }
+
+ /**
+ * Navigation template allows up to 4 buttons, which may overlap with the routing card container
+ * in small screens. In this case, draw the buttons in 2 lines to avoid the overlap.
+ */
+ // TODO(b/191828230): Determine the action strip overlaps properly
+ private void wrapActionStripsIfNeeded() {
+ ThreadUtils.runOnMain(
+ () -> {
+ NavigationTemplate template = (NavigationTemplate) getTemplate();
+ int screenWidth = getTemplateContext().getResources().getDisplayMetrics().widthPixels;
+ int screenHeight = getTemplateContext().getResources().getDisplayMetrics().heightPixels;
+
+ // Measure and layout manually to get the correct view widths.
+ mRootView.measure(
+ MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(screenHeight, MeasureSpec.EXACTLY));
+ mRootView.layout(0, 0, screenWidth, screenHeight);
+
+ // We calculate the right side of the card container and the left side of the
+ // action strip because the manual measure and layout calls do not produce the
+ // correct view position in the window.
+ MarginLayoutParams stepsCardContainerLayoutParams =
+ (MarginLayoutParams) mStepsCardContainer.getLayoutParams();
+ int stepsCardContainerRight =
+ stepsCardContainerLayoutParams.getMarginStart()
+ + stepsCardContainerLayoutParams.width;
+ int actionStripViewLeft = screenWidth - mActionStripView.getWidth();
+
+ // If the card container and the action strip view overlap, draw the action
+ // strip in 2 lines to avoid the overlap.
+ if (mStepsCardContainer.getVisibility() == VISIBLE
+ && mActionStripView.getVisibility() == VISIBLE
+ && stepsCardContainerRight > actionStripViewLeft) {
+ updateActionStrip(template.getActionStrip(), /* allowTwoLines= */ true);
+ }
+
+ // We calculate the bottom side of the action strip and the top side of the map
+ // action strip because the manual measure and layout calls do not produce the
+ // correct view position in the window.
+ int actionStripViewBottom = mActionStripView.getBottom();
+ int mapActionStripViewTop = mMapActionStripView.getTop();
+
+ // If the action strip and the map action strip views overlap, draw the map
+ // action strip in 2 lines to avoid the overlap.
+ if (mActionStripView.getVisibility() == VISIBLE
+ && mMapActionStripView.getVisibility() == VISIBLE
+ && actionStripViewBottom > mapActionStripViewTop) {
+ updateMapActionStrip(template.getMapActionStrip(), /* allowTwoLines= */ true);
+ }
+ });
+ }
+
+ private void updateActionStrip(@Nullable ActionStrip actionStrip) {
+ updateActionStrip(actionStrip, /* allowTwoLines= */ false);
+ }
+
+ private void updateActionStrip(@Nullable ActionStrip actionStrip, boolean allowTwoLines) {
+ mActionStripView.setActionStrip(
+ getTemplateContext(),
+ actionStrip,
+ ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION,
+ allowTwoLines);
+ }
+
+ private void updateMapActionStrip(@Nullable ActionStrip actionStrip) {
+ updateMapActionStrip(actionStrip, /* allowTwoLines= */ false);
+ }
+
+ private void updateMapActionStrip(@Nullable ActionStrip actionStrip, boolean allowTwoLines) {
+ ActionStripWrapper actionStripWrapper = null;
+ if (actionStrip != null) {
+ actionStripWrapper =
+ getPanZoomManager()
+ .getMapActionStripWrapper(
+ /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip);
+ }
+
+ mMapActionStripView.setActionStrip(
+ getTemplateContext(),
+ actionStripWrapper,
+ ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP,
+ allowTwoLines);
+ }
+
+ private void showActionStripViews() {
+ boolean isInPanMode = getPanZoomManager().isInPanMode();
+
+ // Show the action strip when not in the pan mode.
+ mActionStripView.setActiveState(!isInPanMode);
+ mMapActionStripView.setActiveState(true);
+
+ // If nothing was focused, set the default focus.
+ if (!mRootView.hasFocus()) {
+ setDefaultFocus();
+ }
+
+ // The action strip view should fade if the action strip or the window is not focused.
+ if (!(mActionStripView.hasFocus() || mMapActionStripView.hasFocus()) || !hasWindowFocus()) {
+ mActionStripView.setActiveStateWithDelay(false, ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS);
+
+ // Fade the map action strip only when not in the pan mode.
+ if (!isInPanMode) {
+ mMapActionStripView.setActiveStateWithDelay(
+ false, ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS);
+ }
+ }
+ }
+
+ private void attachActiveStateDelegate() {
+ mActionStripView.setActiveStateDelegate(this);
+ mMapActionStripView.setActiveStateDelegate(this);
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings("nullness:method.invocation")
+ private NavigationTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE);
+
+ // Read the fallback color to use with the app-defined card background color.
+ @StyleableRes final int[] themeAttrs = {R.attr.templateNavCardFallbackContentColor};
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ mNavCardFallbackContentColor = ta.getColor(0, Color.WHITE);
+ ta.recycle();
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext).inflate(R.layout.navigation_template_layout, null);
+ mStepsCardContainer = mRootView.findViewById(R.id.content_container);
+ mMessageView = mRootView.findViewById(R.id.message_view);
+ mProgressView = mRootView.findViewById(R.id.progress_view);
+ mStepsContainer = mRootView.findViewById(R.id.steps_container);
+
+ mJunctionImageContainer = mRootView.findViewById(R.id.junction_image_container);
+ mJunctionImageView = mRootView.findViewById(R.id.junction_image);
+ mLanesImageContainerView = mRootView.findViewById(R.id.lanes_image_container);
+ mDetailedStepView = mRootView.findViewById(R.id.detailed_step_view);
+ mCompactStepView = mRootView.findViewById(R.id.compact_step_view);
+ mTravelEstimateContainer = mRootView.findViewById(R.id.travel_estimate_card_container);
+ mTravelEstimateView = mRootView.findViewById(R.id.travel_estimate_view);
+ mActionStripView = mRootView.findViewById(R.id.action_strip);
+ mMapActionStripView = mRootView.findViewById(R.id.map_action_strip);
+ mPanOverlay = mRootView.findViewById(R.id.pan_overlay);
+
+ mCurrentStepParams = createStepTextParams(/* isNextStep= */ false);
+ mNextStepParams = createStepTextParams(/* isNextStep= */ true);
+ mDefaultCardBackgroundColor = mStepsCardContainer.getCardBackgroundColor();
+
+ setStepsCardBackgroundColor();
+
+ // Set the junction image max height and lanes image container height.
+ setJunctionImageMaxHeight();
+ setLanesImageContainerHeight();
+
+ attachActiveStateDelegate();
+ }
+
+ /**
+ * Returns a {@link CarTextParams} instance to use for the text of a step.
+ *
+ * <p>Unlike other text elsewhere, image spans are allowed in these strings.
+ */
+ @SuppressLint("ResourceType")
+ private CarTextParams createStepTextParams(boolean isNextStep) {
+ TemplateContext templateContext = getTemplateContext();
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateRoutingImageSpanRatio,
+ R.attr.templateRoutingImageSpanBody2MaxHeight,
+ R.attr.templateRoutingImageSpanBody3MaxHeight,
+ };
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ float imageRatio = ta.getFloat(0, 0.f);
+ int body2MaxHeight = ta.getDimensionPixelSize(1, 0);
+ int body3MaxHeight = ta.getDimensionPixelSize(2, 0);
+ ta.recycle();
+
+ int maxHeight = isNextStep ? body3MaxHeight : body2MaxHeight;
+ int maxWidth = (int) (maxHeight * imageRatio);
+ return CarTextParams.builder()
+ .setImageBoundingBox(new Rect(0, 0, maxWidth, maxHeight))
+ .setMaxImages(MAX_IMAGES_PER_TEXT_LINE)
+ .setColorSpanConstraints(CarColorConstraints.NO_COLOR)
+ .build();
+ }
+
+ private void showTravelEstimateContainer() {
+ if (mTravelEstimateContainer.getVisibility() == VISIBLE) {
+ return;
+ }
+
+ mTravelEstimateContainer.setVisibility(VISIBLE);
+ Animation animation =
+ AnimationUtils.loadAnimation(
+ getTemplateContext(), R.anim.travel_estimate_card_show_animation);
+ mTravelEstimateContainer.setAnimation(animation);
+ }
+
+ private void hideTravelEstimateContainer() {
+ if (mTravelEstimateContainer.getVisibility() == GONE) {
+ return;
+ }
+ Animation animation =
+ AnimationUtils.loadAnimation(
+ getTemplateContext(), R.anim.travel_estimate_card_hide_animation);
+ // TODO(b/180455232): Create default AnimationListenerListener with empty methods.
+ animation.setAnimationListener(
+ new AnimationListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mTravelEstimateContainer.setVisibility(GONE);
+ }
+ });
+ mTravelEstimateContainer.setAnimation(animation);
+ }
+
+ private void setStepsCardBackgroundColor() {
+ // Set the card's background color to the one provided in the template, if any.
+ CarColor backgroundColor = ((NavigationTemplate) getTemplate()).getBackgroundColor();
+ @ColorInt int backgroundColorInt;
+ if (backgroundColor != null) {
+ backgroundColorInt =
+ CarColorUtils.resolveColor(
+ getTemplateContext(),
+ backgroundColor,
+ false,
+ Color.BLACK,
+ CarColorConstraints.UNCONSTRAINED);
+ } else {
+ backgroundColorInt = mDefaultCardBackgroundColor;
+ }
+
+ // Darken the background of the card.
+ mStepsCardContainer.setCardBackgroundColor(
+ CarColorUtils.darkenColor(backgroundColorInt, CARD_BACKGROUND_DARKEN_PERCENTAGE));
+
+ // Darken the background of the compat step view.
+ // We also create a drawable for it that has bottom rounded corners because otherwise the
+ // background of the card won't clip within the parent's outline. It is probably possible to
+ // do the clipping using a convex path (getting the card's background outline and using that
+ // does not work as it returns a rect and not a path), and setting it through an outline
+ // provider but this is cheaper regardless as clipping is an expensive operation.
+ float bottomRadius = mStepsCardContainer.getCardRadius();
+ GradientDrawable drawable = new GradientDrawable();
+ drawable.setCornerRadii(
+ new float[] {0, 0, 0, 0, bottomRadius, bottomRadius, bottomRadius, bottomRadius});
+ drawable.setColor(
+ CarColorUtils.darkenColor(
+ backgroundColorInt, COMPACT_STEP_CARD_BACKGROUND_DARKEN_PERCENTAGE));
+ mCompactStepView.setBackground(drawable);
+ }
+
+ private void setStepsCardContentColor() {
+ if (((NavigationTemplate) getTemplate()).getBackgroundColor() != null) {
+ // Use the fallback content color if the app-defined card background color is used,
+ // because the OEM-defined text color may not have the adequate contrast ratio with the
+ // card background color.
+ mDetailedStepView.setTextColor(mNavCardFallbackContentColor);
+ mCompactStepView.setTextColor(mNavCardFallbackContentColor);
+ mMessageView.setTextColor(mNavCardFallbackContentColor);
+ mProgressView.setColor(mNavCardFallbackContentColor);
+ } else {
+ mDetailedStepView.setDefaultTextColor();
+ mCompactStepView.setDefaultTextColor();
+ mMessageView.setDefaultTextColor();
+ mProgressView.setDefaultColor();
+ }
+ }
+
+ private void setJunctionImageMaxHeight() {
+ int stepsCardContainerWidth = mStepsCardContainer.getLayoutParams().width;
+ int junctionImageMaxHeight =
+ (int) (stepsCardContainerWidth * JUNCTION_IMAGE_MAX_HEIGHT_TO_CARD_WIDTH_RATIO);
+ mJunctionImageView.setMaxHeight(junctionImageMaxHeight);
+ }
+
+ private void setLanesImageContainerHeight() {
+ int stepsCardContainerWidth = mStepsCardContainer.getLayoutParams().width;
+ int lanesImageContainerHeight =
+ (int) (stepsCardContainerWidth * LANES_IMAGE_CONTAINER_HEIGHT_TO_CARD_WIDTH_RATIO);
+ mLanesImageContainerView.getLayoutParams().height = lanesImageContainerHeight;
+ }
+
+ private boolean hasPanButton() {
+ NavigationTemplate template = (NavigationTemplate) getTemplate();
+ ActionStrip mapActionStrip = template.getMapActionStrip();
+ return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null;
+ }
+
+ private void dispatchPanModeChange(boolean isInPanMode) {
+ NavigationTemplate template = (NavigationTemplate) getTemplate();
+ PanModeDelegate panModeDelegate = template.getPanModeDelegate();
+ if (panModeDelegate != null) {
+ getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode);
+ }
+ }
+
+ private void updateVisibility(boolean isInPanMode) {
+ ThreadUtils.runOnMain(
+ () -> {
+ if (isInPanMode) {
+ mPanOverlay.setVisibility(VISIBLE);
+ mStepsCardContainer.setVisibility(GONE);
+ mActionStripView.setActiveState(false);
+ hideTravelEstimateContainer();
+ } else {
+ mPanOverlay.setVisibility(GONE);
+ mStepsCardContainer.setVisibility(mStepsCardContainerVisibility);
+ if (mTravelEstimateContainerVisibility == VISIBLE) {
+ showTravelEstimateContainer();
+ } else {
+ hideTravelEstimateContainer();
+ }
+ }
+ });
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java
new file mode 100644
index 0000000..7fd88e1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/NavigationTemplatePresenterFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.navigation;
+
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenterFactory;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Implementation of a {@link TemplatePresenterFactory} for the production host, responsible for
+ * providing {@link TemplatePresenter} instances for the set of templates the host supports.
+ */
+public class NavigationTemplatePresenterFactory implements TemplatePresenterFactory {
+ private static final NavigationTemplatePresenterFactory sInstance =
+ new NavigationTemplatePresenterFactory();
+ private static final ImmutableSet<Class<? extends Template>> SUPPORTED_TEMPLATES =
+ ImmutableSet.of(
+ NavigationTemplate.class,
+ PlaceListNavigationTemplate.class,
+ RoutePreviewNavigationTemplate.class);
+
+ /** Gets the singleton instance of{@link NavigationTemplatePresenterFactory}. */
+ public static NavigationTemplatePresenterFactory get() {
+ return sInstance;
+ }
+
+ @Override
+ @Nullable
+ public TemplatePresenter createPresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ Template template = templateWrapper.getTemplate();
+
+ Class<? extends Template> clazz = template.getClass();
+ if (NavigationTemplate.class == clazz) {
+ return NavigationTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (PlaceListNavigationTemplate.class == clazz) {
+ return PlaceListNavigationTemplatePresenter.create(templateContext, templateWrapper);
+ } else if (RoutePreviewNavigationTemplate.class == clazz) {
+ return RoutePreviewNavigationTemplatePresenter.create(templateContext, templateWrapper);
+ } else {
+ L.w(
+ LogTags.TEMPLATE,
+ "Don't know how to create a presenter for template: %s",
+ clazz.getSimpleName());
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return SUPPORTED_TEMPLATES;
+ }
+
+ private NavigationTemplatePresenterFactory() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java
new file mode 100644
index 0000000..0f25c8c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/PlaceListNavigationTemplatePresenter.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.navigation;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_SIMPLE;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.transition.TransitionInflater;
+import android.transition.TransitionManager;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView;
+import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A {@link TemplatePresenter} that shows a {@link PlaceListNavigationTemplate}. */
+public class PlaceListNavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter {
+ private final ViewGroup mRootView;
+ private final ViewGroup mContentContainer;
+ private final CardHeaderView mHeaderView;
+ private final ContentView mContentView;
+ private final ActionStripView mActionStripView;
+ private final ActionStripView mMapActionStripView;
+ private final PanOverlayView mPanOverlay;
+
+ /** Creates a {@link PlaceListNavigationTemplatePresenter}. */
+ public static PlaceListNavigationTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ PlaceListNavigationTemplatePresenter presenter =
+ new PlaceListNavigationTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ getTemplateContext()
+ .getEventManager()
+ .subscribeEvent(
+ this,
+ EventType.CONFIGURATION_CHANGED,
+ () -> {
+ PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate();
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getActionStrip());
+ });
+ }
+
+ @Override
+ public void onStop() {
+ getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+
+ super.onStop();
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public boolean isPanAndZoomEnabled() {
+ return getTemplateContext().getCarHostConfig().isPoiRoutePreviewPanZoomEnabled();
+ }
+
+ @Override
+ public void onPanModeChanged(boolean isInPanMode) {
+ updateVisibility(isInPanMode);
+ dispatchPanModeChange(isInPanMode);
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+ if (getPanZoomManager().handlePanEventsIfNeeded(keyCode)) {
+ return true;
+ }
+
+ // Move between the card view and the action button view on left or right rotary nudge.
+ if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // If the focus is in the card view (back button or row list), request focus in the
+ // action
+ // strip.
+ if (moveFocusIfPresent(
+ ImmutableList.of(mContentContainer), ImmutableList.of(mActionStripView))) {
+ return true;
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ // Request focus on the content view so that the first row in the list will take focus.
+ if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) {
+ return true;
+ }
+ }
+ return super.onKeyUp(keyCode, keyEvent);
+ }
+
+ @Override
+ public void calculateAdditionalInset(Rect inset) {
+ // The portrait inset is more favorable to portrait screens and is calculated as the following
+ // bounding box:
+ // * left: inset left
+ // * right: inset right
+ // * top: Max(inset top, actionStrip bottom, content view bottom)
+ // * bottom: inset bottom
+ Rect portraitScreenInset = new Rect(inset);
+
+ // The landscape inset is more favorable to landscape screens it is calculated as the following
+ // bounding box:
+ // * left: Max(inset left, content view right)
+ // * right: inset right
+ // * top: Max(inset top, actionStrip bottom)
+ // * bottom: inset bottom
+ Rect landscapeScreenInset = new Rect(inset);
+
+ if (mMapActionStripView.getVisibility() == VISIBLE) {
+ landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft());
+ portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft());
+ }
+ if (mActionStripView.getVisibility() == VISIBLE) {
+ landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom());
+ portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom());
+ }
+ if (mContentContainer.getVisibility() == View.VISIBLE) {
+ landscapeScreenInset.left = max(landscapeScreenInset.left, mContentContainer.getRight());
+ portraitScreenInset.top = max(portraitScreenInset.top, mContentContainer.getBottom());
+ }
+ int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width();
+ int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width();
+ inset.set(
+ landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset);
+ }
+
+ @Override
+ public boolean handlesTemplateChangeAnimation() {
+ return true;
+ }
+
+ private void update() {
+ TransitionManager.beginDelayedTransition(
+ mRootView,
+ TransitionInflater.from(getTemplateContext())
+ .inflateTransition(R.transition.place_list_nav_template_transition));
+
+ PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate();
+
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getMapActionStrip());
+ getPanZoomManager().setEnabled(hasPanButton());
+ mHeaderView.setContent(
+ getTemplateContext(),
+ template.getTitle(),
+ template.getHeaderAction(),
+ template.getOnContentRefreshDelegate());
+
+ ItemList itemList = template.getItemList();
+ mContentView.setRowListContent(
+ getTemplateContext(),
+ RowListWrapper.wrap(getTemplateContext(), itemList)
+ .setIsLoading(template.isLoading())
+ .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+ .setRowListConstraints(ROW_LIST_CONSTRAINTS_SIMPLE)
+ .setIsRefresh(getTemplateWrapper().isRefresh())
+ .setIsHalfList(true)
+ .build());
+
+ updateVisibility(getPanZoomManager().isInPanMode());
+
+ getTemplateContext().getSurfaceInfoProvider().invalidateStableArea();
+ requestVisibleAreaUpdate();
+ }
+
+ private void updateActionStrip(@Nullable ActionStrip actionStrip) {
+ mActionStripView.setActionStrip(
+ getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ }
+
+ private void updateMapActionStrip(@Nullable ActionStrip actionStrip) {
+ ActionStripWrapper actionStripWrapper = null;
+ if (actionStrip != null) {
+ actionStripWrapper =
+ getPanZoomManager()
+ .getMapActionStripWrapper(
+ /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip);
+ }
+
+ mMapActionStripView.setActionStrip(
+ getTemplateContext(),
+ actionStripWrapper,
+ ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP,
+ /* allowTwoLines= */ false);
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"methodref.receiver.bound.invalid"})
+ private PlaceListNavigationTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE);
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext)
+ .inflate(R.layout.list_navigation_template_layout, null);
+ mContentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView = mRootView.findViewById(R.id.header_view);
+ mContentView = mRootView.findViewById(R.id.content_view);
+ mActionStripView = mRootView.findViewById(R.id.action_strip);
+ mMapActionStripView = mRootView.findViewById(R.id.map_action_strip);
+ mPanOverlay = mRootView.findViewById(R.id.pan_overlay);
+
+ // We should always show an ItemList.
+ mContentContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean hasPanButton() {
+ PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate();
+ ActionStrip mapActionStrip = template.getMapActionStrip();
+ return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null;
+ }
+
+ private void dispatchPanModeChange(boolean isInPanMode) {
+ PlaceListNavigationTemplate template = (PlaceListNavigationTemplate) getTemplate();
+ PanModeDelegate panModeDelegate = template.getPanModeDelegate();
+ if (panModeDelegate != null) {
+ getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode);
+ }
+ }
+
+ private void updateVisibility(boolean isInPanMode) {
+ ThreadUtils.runOnMain(
+ () -> {
+ if (isInPanMode) {
+ mPanOverlay.setVisibility(VISIBLE);
+ mContentContainer.setVisibility(GONE);
+ mActionStripView.setVisibility(GONE);
+ } else {
+ mPanOverlay.setVisibility(GONE);
+ mContentContainer.setVisibility(VISIBLE);
+ mActionStripView.setVisibility(VISIBLE);
+ }
+ });
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java
new file mode 100644
index 0000000..d7cf9d1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/RoutePreviewNavigationTemplatePresenter.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.presenters.navigation;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowListConstraints.ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW;
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW;
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW;
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.view.AbstractSurfaceTemplatePresenter;
+import com.android.car.libraries.apphost.view.TemplatePresenter;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionStripView;
+import com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView;
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A {@link TemplatePresenter} that shows a {@link RoutePreviewNavigationTemplate}. */
+public class RoutePreviewNavigationTemplatePresenter extends AbstractSurfaceTemplatePresenter {
+ private final ViewGroup mRootView;
+ private final ViewGroup mContentContainer;
+ private final CardHeaderView mHeaderView;
+ private final ContentView mContentView;
+ private final ActionStripView mActionStripView;
+ private final ActionStripView mMapActionStripView;
+ private final PanOverlayView mPanOverlay;
+
+ /** Creates a {@link RoutePreviewNavigationTemplatePresenter}. */
+ public static RoutePreviewNavigationTemplatePresenter create(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ RoutePreviewNavigationTemplatePresenter presenter =
+ new RoutePreviewNavigationTemplatePresenter(templateContext, templateWrapper);
+ presenter.update();
+ return presenter;
+ }
+
+ @Override
+ public View getView() {
+ return mRootView;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ getTemplateContext()
+ .getEventManager()
+ .subscribeEvent(
+ this,
+ EventType.CONFIGURATION_CHANGED,
+ () -> {
+ RoutePreviewNavigationTemplate template =
+ (RoutePreviewNavigationTemplate) getTemplate();
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getMapActionStrip());
+ });
+ }
+
+ @Override
+ public void onStop() {
+ getTemplateContext().getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+
+ super.onStop();
+ }
+
+ @Override
+ public void onTemplateChanged() {
+ update();
+ }
+
+ @Override
+ public boolean isPanAndZoomEnabled() {
+ return getTemplateContext().getCarHostConfig().isPoiRoutePreviewPanZoomEnabled();
+ }
+
+ @Override
+ public void onPanModeChanged(boolean isInPanMode) {
+ updateVisibility(isInPanMode);
+ dispatchPanModeChange(isInPanMode);
+ }
+
+ @Override
+ protected View getDefaultFocusedView() {
+ if (mContentView.getVisibility() == VISIBLE) {
+ return mContentView;
+ }
+ return super.getDefaultFocusedView();
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+ if (getPanZoomManager().handlePanEventsIfNeeded(keyCode)) {
+ return true;
+ }
+
+ // Move between the card view and the action button view on left or right rotary nudge.
+ if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // If the focus is in the card view (back button or row list), request focus in the
+ // action
+ // strip.
+ if (moveFocusIfPresent(
+ ImmutableList.of(mContentContainer), ImmutableList.of(mActionStripView))) {
+ return true;
+ }
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ // Request focus on the content view so that the first row in the list will take focus.
+ if (moveFocusIfPresent(ImmutableList.of(mActionStripView), ImmutableList.of(mContentView))) {
+ return true;
+ }
+ }
+ return super.onKeyUp(keyCode, keyEvent);
+ }
+
+ public void calculateAdditionalInset(Rect inset) {
+ // The portrait inset is more favorable to portrait screens and is calculated as the following
+ // bounding box:
+ // * left: inset left
+ // * right: inset right
+ // * top: Max(inset top, actionStrip bottom, content view bottom)
+ // * bottom: inset bottom
+ Rect portraitScreenInset = new Rect(inset);
+
+ // The landscape inset is more favorable to landscape screens it is calculated as the following
+ // bounding box:
+ // * left: Max(inset left, content view right)
+ // * right: inset right
+ // * top: Max(inset top, actionStrip bottom)
+ // * bottom: inset bottom
+ Rect landscapeScreenInset = new Rect(inset);
+
+ if (mMapActionStripView.getVisibility() == VISIBLE) {
+ landscapeScreenInset.right = min(landscapeScreenInset.right, mMapActionStripView.getLeft());
+ portraitScreenInset.right = min(portraitScreenInset.right, mMapActionStripView.getLeft());
+ }
+ if (mActionStripView.getVisibility() == VISIBLE) {
+ landscapeScreenInset.top = max(landscapeScreenInset.top, mActionStripView.getBottom());
+ portraitScreenInset.top = max(portraitScreenInset.top, mActionStripView.getBottom());
+ }
+ if (mContentContainer.getVisibility() == View.VISIBLE) {
+ landscapeScreenInset.left = max(landscapeScreenInset.left, mContentContainer.getRight());
+ portraitScreenInset.top = max(portraitScreenInset.top, mContentContainer.getBottom());
+ }
+ int landscapeScreenArea = landscapeScreenInset.height() * landscapeScreenInset.width();
+ int portraitScreenArea = portraitScreenInset.height() * portraitScreenInset.width();
+ inset.set(
+ landscapeScreenArea > portraitScreenArea ? landscapeScreenInset : portraitScreenInset);
+ }
+
+ private void update() {
+ RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate();
+
+ updateActionStrip(template.getActionStrip());
+ updateMapActionStrip(template.getMapActionStrip());
+ getPanZoomManager().setEnabled(hasPanButton());
+ mHeaderView.setContent(getTemplateContext(), template.getTitle(), template.getHeaderAction());
+
+ ItemList itemList = template.getItemList();
+ Action navigateAction = template.getNavigateAction();
+
+ RowListWrapper.Builder builder =
+ RowListWrapper.wrap(getTemplateContext(), itemList)
+ .setIsLoading(template.isLoading())
+ .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+ .setRowListConstraints(ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW)
+ .setIsRefresh(getTemplateWrapper().isRefresh())
+ .setIsHalfList(true)
+
+ // For the route preview list, don't use radio buttons but rather show the
+ // selection
+ // by changing the row background.
+ .setListFlags(
+ LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW
+ | LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW
+ | LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW);
+ if (!template.isLoading() && navigateAction != null) {
+ builder.setRowSelectedText(navigateAction.getTitle());
+ OnClickDelegate onClickDelegate = navigateAction.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ TemplateContext templateContext = getTemplateContext();
+ builder.setOnRepeatedSelectionCallback(
+ () -> templateContext.getAppDispatcher().dispatchClick(onClickDelegate));
+ }
+ }
+ mContentView.setRowListContent(getTemplateContext(), builder.build());
+
+ updateVisibility(getPanZoomManager().isInPanMode());
+
+ getTemplateContext().getSurfaceInfoProvider().invalidateStableArea();
+ requestVisibleAreaUpdate();
+ }
+
+ private void updateActionStrip(@Nullable ActionStrip actionStrip) {
+ mActionStripView.setActionStrip(
+ getTemplateContext(), actionStrip, ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ }
+
+ private void updateMapActionStrip(@Nullable ActionStrip actionStrip) {
+ ActionStripWrapper actionStripWrapper = null;
+ if (actionStrip != null) {
+ actionStripWrapper =
+ getPanZoomManager()
+ .getMapActionStripWrapper(
+ /* templateContext= */ getTemplateContext(), /* actionStrip= */ actionStrip);
+ }
+
+ mMapActionStripView.setActionStrip(
+ getTemplateContext(),
+ actionStripWrapper,
+ ActionsConstraints.ACTIONS_CONSTRAINTS_NAVIGATION_MAP,
+ /* allowTwoLines= */ false);
+ }
+
+ @SuppressLint("InflateParams")
+ @SuppressWarnings({"methodref.receiver.bound.invalid"})
+ private RoutePreviewNavigationTemplatePresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ super(templateContext, templateWrapper, StatusBarState.OVER_SURFACE);
+
+ mRootView =
+ (ViewGroup)
+ LayoutInflater.from(templateContext)
+ .inflate(R.layout.list_navigation_template_layout, null);
+ mContentContainer = mRootView.findViewById(R.id.content_container);
+ mHeaderView = mRootView.findViewById(R.id.header_view);
+ mContentView = mRootView.findViewById(R.id.content_view);
+ mActionStripView = mRootView.findViewById(R.id.action_strip);
+ mMapActionStripView = mRootView.findViewById(R.id.map_action_strip);
+ mPanOverlay = mRootView.findViewById(R.id.pan_overlay);
+
+ mContentContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean hasPanButton() {
+ RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate();
+ ActionStrip mapActionStrip = template.getMapActionStrip();
+ return mapActionStrip != null && mapActionStrip.getFirstActionOfType(Action.TYPE_PAN) != null;
+ }
+
+ private void dispatchPanModeChange(boolean isInPanMode) {
+ RoutePreviewNavigationTemplate template = (RoutePreviewNavigationTemplate) getTemplate();
+ PanModeDelegate panModeDelegate = template.getPanModeDelegate();
+ if (panModeDelegate != null) {
+ getTemplateContext().getAppDispatcher().dispatchPanModeChanged(panModeDelegate, isInPanMode);
+ }
+ }
+
+ private void updateVisibility(boolean isInPanMode) {
+ ThreadUtils.runOnMain(
+ () -> {
+ if (isInPanMode) {
+ mPanOverlay.setVisibility(VISIBLE);
+ mContentContainer.setVisibility(GONE);
+ mActionStripView.setVisibility(GONE);
+ } else {
+ mPanOverlay.setVisibility(GONE);
+ mContentContainer.setVisibility(VISIBLE);
+ mActionStripView.setVisibility(VISIBLE);
+ }
+ });
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml
new file mode 100644
index 0000000..c4b2288
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_hide_animation.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true">
+ <translate
+ android:fromYDelta="0"
+ android:toYDelta="100%"
+ android:duration="@integer/bottom_card_animation_duration_millis"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ />
+</set>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml
new file mode 100644
index 0000000..24b34da
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/anim/travel_estimate_card_show_animation.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true">
+ <translate
+ android:fromYDelta="100%"
+ android:toYDelta="0"
+ android:duration="@integer/bottom_card_animation_duration_millis"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ />
+</set>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml
new file mode 100644
index 0000000..ff1e95c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/list_navigation_template_layout.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <include
+ android:id="@id/content_container"
+ android:layout_width="?templateCardContentContainerDefaultWidth"
+ android:layout_height="0dp"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ app:layout_goneMarginTop="?templateCardContentContainerTopMargin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/action_strip"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_min="?templateCardContentContainerMinHeight"
+ layout="@layout/card_container" />
+
+ <include
+ android:id="@+id/pan_overlay"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ layout="@layout/pan_overlay" />
+
+ <!-- The action strip with global actions for the template. -->
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating" />
+
+ <include
+ android:id="@+id/map_action_strip"
+ layout="@layout/map_action_strip_view_floating"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml
new file mode 100644
index 0000000..147e2c8
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout-w1280dp-h1000dp/navigation_template_layout.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- This is the card that contains the routing information, arrival view,
+ etc. -->
+ <include
+ android:id="@id/content_container"
+ android:layout_width="?templateRoutingStepsCardContentContainerMinWidth"
+ android:layout_height="0dp"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ android:layout_marginBottom="?templateCardContentContainerBottomMargin"
+ app:layout_goneMarginTop="?templateCardContentContainerTopMargin"
+ app:layout_constraintTop_toBottomOf="@id/action_strip"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_min="?templateRoutingStepsCardContentContainerMinHeight"
+ layout="@layout/steps_card_container"/>
+
+ <!-- The travel estimate (aka "ETA") card at the bottom left of the
+ screen. -->
+ <include
+ layout="@layout/travel_estimate_card_container"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/pan_overlay"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ layout="@layout/pan_overlay" />
+
+ <!-- The action strip with global actions for the template. -->
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating"/>
+
+ <include
+ android:id="@+id/map_action_strip"
+ layout="@layout/map_action_strip_view_floating"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml
new file mode 100644
index 0000000..cba4ed4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/list_navigation_template_layout.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <include
+ android:id="@id/content_container"
+ layout="@layout/card_container" />
+
+ <include
+ android:id="@+id/pan_overlay"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ layout="@layout/pan_overlay" />
+
+ <!-- The action strip with global actions for the template. -->
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating" />
+
+ <include
+ android:id="@+id/map_action_strip"
+ layout="@layout/map_action_strip_view_floating"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml
new file mode 100644
index 0000000..c2dfcef
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/navigation_template_layout.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- This is the card that contains the routing information, arrival view,
+ etc. -->
+ <include
+ android:id="@id/content_container"
+ layout="@layout/steps_card_container"/>
+
+ <!-- The travel estimate (aka "ETA") card at the bottom left of the
+ screen. -->
+ <include
+ layout="@layout/travel_estimate_card_container"
+ android:visibility="gone" />
+
+ <include
+ android:id="@+id/pan_overlay"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ layout="@layout/pan_overlay" />
+
+ <!-- The action strip with global actions for the template. -->
+ <include
+ android:id="@+id/action_strip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ layout="@layout/action_strip_view_floating"/>
+
+ <include
+ android:id="@+id/map_action_strip"
+ layout="@layout/map_action_strip_view_floating"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml
new file mode 100644
index 0000000..1eb797f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/steps_card_container.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+The card has a minimum and maximum heights specific to the routing screen.
+The card is anchored to the top left of the screen which should be consistent
+with the rest of the card-style templates
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?templateCardRoutingContentContainerStyle"
+ android:focusable="false"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_min="?templateRoutingStepsCardContentContainerMinHeight"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ android:layout_marginBottom="?templateCardContentContainerBottomMargin"
+ android:layout_width="?templateRoutingStepsCardContentContainerMinWidth"
+ android:layout_height="0dp"
+ tools:ignore="MissingClass">
+
+ <!-- A view that shows a message. -->
+ <include
+ android:id="@+id/message_view"
+ android:visibility="gone"
+ layout="@layout/message_view" />
+
+ <!-- The container view for progress indicator. -->
+ <include
+ android:id="@+id/progress_view"
+ android:visibility="gone"
+ layout="@layout/progress_view" />
+
+ <!-- A container view for the driving instruction, such as the turn icon and
+ junction image.-->
+ <LinearLayout
+ android:id="@+id/steps_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- A detailed view showing the current step. -->
+ <include
+ android:id="@+id/detailed_step_view"
+ layout="@layout/detailed_step_view" />
+
+ <!-- A divider between the current step and the next step. -->
+ <View
+ android:id="@+id/divider"
+ android:layout_width="match_parent"
+ android:layout_height="?templateDividerThickness"
+ android:background="?templateRoutingDividerColor"/>
+
+ <!-- A compact view showing the next step. -->
+ <include
+ android:id="@+id/compact_step_view"
+ layout="@layout/compact_step_view" />
+
+ <!-- The optional junction image. -->
+ <FrameLayout
+ android:id="@+id/junction_image_container"
+ android:background="?templateRoutingJunctionImageBackgroundColor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:id="@+id/junction_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false"
+ android:scaleType="centerInside"
+ android:adjustViewBounds="true"
+ tools:ignore="ContentDescription" />
+ </FrameLayout>
+ </LinearLayout>
+</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml
new file mode 100644
index 0000000..498304d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/layout/travel_estimate_card_container.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView
+ android:id="@+id/travel_estimate_card_container"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="?templateCardRoutingContentContainerStyle"
+ android:focusable="false"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_width="?templateRoutingStepsCardContentContainerMinWidth"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ android:paddingHorizontal="?templateNavCardPaddingHorizontal"
+ android:paddingVertical="?templateNavCardSmallPaddingVertical"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- The travel estimate (aka ETA) card. -->
+ <include
+ layout="@layout/travel_estimate_view"
+ android:id="@+id/travel_estimate_view"
+ android:layout_gravity="center_vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml
new file mode 100644
index 0000000..bedd592
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/place_list_nav_template_transition.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together">
+ <fade
+ android:fadingMode="fade_in_out"
+ android:duration="?templateUpdateAnimationDurationMilliseconds"/>
+ <changeBounds
+ android:duration="?templateUpdateAnimationDurationMilliseconds"
+ android:interpolator="@interpolator/fast_out_slow_in"/>
+</transitionSet>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml
new file mode 100644
index 0000000..9a18a4e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/transition/routing_card_transition.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together">
+ <targets>
+ <target android:targetId="@id/content_container" />
+ <target android:targetId="@id/travel_estimate_card_container" />
+ <target android:targetId="@id/pan_overlay" />
+ </targets>
+ <fade
+ android:fadingMode="fade_in_out"
+ android:duration="@integer/routing_card_animation_duration_millis">
+ <targets>
+ <target android:excludeId="@id/detailed_step_view" />
+ <target android:excludeId="@id/travel_estimate_card_container" />
+ </targets>
+ </fade>
+ <changeBounds
+ android:duration="@integer/routing_card_animation_duration_millis"
+ android:interpolator="@interpolator/fast_out_slow_in"/>
+</transitionSet>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml
new file mode 100644
index 0000000..6ac3813
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res/values/integers.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <integer name="routing_card_animation_duration_millis">500</integer>
+ <integer name="bottom_card_animation_duration_millis">500</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml
new file mode 100644
index 0000000..d4d7aea
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_material.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+Copied from the Android SDK's internal resources
+Source: //third_party/java/android/android_sdk_linux/platforms/stable/data/res/anim/progress_indeterminate_material.xml
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="1333"
+ android:interpolator="@interpolator/default_trim_start_interpolator"
+ android:propertyName="trimPathStart"
+ android:repeatCount="-1"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType" />
+ <objectAnimator
+ android:duration="1333"
+ android:interpolator="@interpolator/default_trim_end_interpolator"
+ android:propertyName="trimPathEnd"
+ android:repeatCount="-1"
+ android:valueFrom="0"
+ android:valueTo="0.75"
+ android:valueType="floatType" />
+ <objectAnimator
+ android:duration="1333"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:propertyName="trimPathOffset"
+ android:repeatCount="-1"
+ android:valueFrom="0"
+ android:valueTo="0.25"
+ android:valueType="floatType" />
+</set>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml
new file mode 100644
index 0000000..d267c85
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/anim/default_progress_indeterminate_rotation_material.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+Copied from the Android SDK's internal resources
+Source: //third_party/java/android/android_sdk_linux/platforms/stable/data/res/anim/progress_indeterminate_rotation_material.xml
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="6665"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:propertyName="rotation"
+ android:repeatCount="-1"
+ android:valueFrom="0"
+ android:valueTo="720"
+ android:valueType="floatType" />
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml
new file mode 100644
index 0000000..f988a33
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-dpad/template_ripple_color_selector.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show
+for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed.
+Doing so also avoids "ghost" effect when rapidly moving focus across Views. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?templateRippleSelectorColor"
+ android:state_pressed="true"/>
+ <item android:color="@android:color/transparent"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml
new file mode 100644
index 0000000..f988a33
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color-wheel/template_ripple_color_selector.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show
+for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed.
+Doing so also avoids "ghost" effect when rapidly moving focus across Views. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?templateRippleSelectorColor"
+ android:state_pressed="true"/>
+ <item android:color="@android:color/transparent"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml
new file mode 100644
index 0000000..ae18391
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_focus_ring_color_selector.xml
@@ -0,0 +1,9 @@
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <!-- Inactive state -->
+ <item app:templateFocusStateInactive="true" android:color="?templateFocusRingNoAccentColor"/>
+
+ <!-- Default -->
+ <item android:color="?templateFocusRingColor"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml
new file mode 100644
index 0000000..6ad27df
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/color/template_ripple_color_selector.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Due to b/120995067 in order to keep normal ripple effect in touch, the color selector must show
+for all states. In rotary and touchpad, per go/aa-focus-design, don't draw ripple when not pressed.
+Doing so also avoids "ghost" effect when rapidly moving focus across Views. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?templateRippleSelectorColor"/>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml
new file mode 100644
index 0000000..2c5f9aa
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable-night/action_strip_fab_view_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- The background fill -->
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?templateActionStripFabBackgroundColorDark"/>
+ <corners
+ android:radius="?templateButtonCornerRadius"/>
+ </shape>
+ </item>
+
+ <!-- Masked ripple layer -->
+ <item android:drawable="@drawable/action_strip_button_ripple"/>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml
new file mode 100644
index 0000000..3769aab
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_button_focus_ring.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ <corners android:radius="?templateButtonCornerRadius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ <corners android:radius="?templateButtonCornerRadius"/>
+ </shape>
+ </item>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml
new file mode 100644
index 0000000..ba659c5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_ripple.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/template_ripple_color_selector">
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ <corners android:radius="?templateButtonCornerRadius"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml
new file mode 100644
index 0000000..01508d9
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_background.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- The background fill -->
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?templateActionStripButtonBackgroundColor"/>
+ <corners
+ android:radius="?templateButtonCornerRadius"/>
+ <stroke
+ android:width="?templateActionButtonSecondaryBorderWidth"
+ android:color="?templateActionButtonSecondaryBorderColor" />
+ </shape>
+ </item>
+
+ <!-- Masked ripple layer -->
+ <item android:drawable="@drawable/action_strip_button_ripple"/>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml
new file mode 100644
index 0000000..e97b6ab
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_button_view_focus_ring.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor" />
+ <corners android:radius="?templateButtonCornerRadius" />
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor" />
+ <corners android:radius="?templateButtonCornerRadius" />
+ </shape>
+ </item>
+</selector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml
new file mode 100644
index 0000000..37591ff
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/action_strip_fab_view_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- The background fill -->
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?templateActionStripFabBackgroundColorLight"/>
+ <corners
+ android:radius="?templateButtonCornerRadius"/>
+ </shape>
+ </item>
+
+ <!-- Masked ripple layer -->
+ <item android:drawable="@drawable/action_strip_button_ripple"/>
+</layer-list>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml
new file mode 100644
index 0000000..4f5868f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/back_button_focus_ring.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:inset="@dimen/template_back_focus_ring_inset">
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ </selector>
+</inset>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml
new file mode 100644
index 0000000..4f0bf0f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/default_progress_spinner_medium_thin">
+ <target
+ android:name="progressBar"
+ android:animation="@anim/default_progress_indeterminate_material" />
+ <target
+ android:name="root"
+ android:animation="@anim/default_progress_indeterminate_rotation_material" />
+</animated-vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml
new file mode 100644
index 0000000..c5efbed
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/default_progress_spinner_medium_thin.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48">
+ <group
+ android:name="root"
+ android:translateX="24.0"
+ android:translateY="24.0">
+ <path
+ android:name="progressBar"
+ android:fillColor="#00000000"
+ android:pathData="M0, 0 m 0, -19 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38"
+ android:strokeColor="?templateLoadingSpinnerColor"
+ android:strokeLineCap="square"
+ android:strokeLineJoin="miter"
+ android:strokeWidth="3"
+ android:trimPathEnd="0"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ </group>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml
new file mode 100644
index 0000000..d488ba4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/no_content_view_focus_ring.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:inset="@dimen/template_no_content_view_focus_ring_padding">
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusNoContentAccentColor"/>
+ <corners android:radius="?templateNoContentFocusCornerRadius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusNoContentAccentColor"/>
+ <corners android:radius="?templateNoContentFocusCornerRadius"/>
+ </shape>
+ </item>
+ </selector>
+</inset>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml
new file mode 100644
index 0000000..42be196
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/pin_sign_in_view_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?templateSignInPinBackgroundColor" />
+ <corners android:radius="?templateSignInPinCornerRadius" />
+</shape> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml
new file mode 100644
index 0000000..a24754e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_bottom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Background fill -->
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowSelectedBackgroundColor" />
+ <corners
+ android:bottomRightRadius="@dimen/template_row_corner_radius"
+ android:bottomLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowBackgroundColor" />
+ <corners
+ android:bottomRightRadius="@dimen/template_row_corner_radius"
+ android:bottomLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ <corners
+ android:bottomRightRadius="@dimen/template_row_corner_radius"
+ android:bottomLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:bottomLeftRadius="@dimen/template_row_corner_radius"
+ android:bottomRightRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:bottomLeftRadius="@dimen/template_row_corner_radius"
+ android:bottomRightRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml
new file mode 100644
index 0000000..e52e937
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_middle.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Background fill -->
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowSelectedBackgroundColor" />
+ </shape>
+ </item>
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowBackgroundColor" />
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml
new file mode 100644
index 0000000..50b092b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Background fill -->
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowSelectedBackgroundColor" />
+ <corners
+ android:topRightRadius="@dimen/template_row_corner_radius"
+ android:topLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowBackgroundColor" />
+ <corners
+ android:topRightRadius="@dimen/template_row_corner_radius"
+ android:topLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ <corners
+ android:topRightRadius="@dimen/template_row_corner_radius"
+ android:topLeftRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:topLeftRadius="@dimen/template_row_corner_radius"
+ android:topRightRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:topLeftRadius="@@dimen/template_row_corner_radius"
+ android:topRightRadius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml
new file mode 100644
index 0000000..d7e2b4a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_sectional_top_bottom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Background fill -->
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowSelectedBackgroundColor"/>
+ <corners
+ android:radius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <solid android:color="?templateRowBackgroundColor" />
+ <corners
+ android:radius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ <corners
+ android:radius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:radius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:radius="@dimen/template_row_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml
new file mode 100644
index 0000000..58dad27
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_background_simple.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Background fill -->
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape
+ android:shape="rectangle">
+ <solid android:color="?templateRowSelectedBackgroundColor" />
+ </shape>
+ </item>
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml
new file mode 100644
index 0000000..5142deb
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/row_image_placeholder.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="?templateRowImagePlaceholderColor" />
+</shape>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml
new file mode 100644
index 0000000..ade70ba
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/search_bar_icon.xml
@@ -0,0 +1,5 @@
+<vector android:height="36dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/template_edit_text_color_selector" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml
new file mode 100644
index 0000000..1f80d18
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_grid_item_background.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Ripple layer masked so that it is only drawn within the bounds of the view -->
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ <corners
+ android:radius="@dimen/template_grid_item_corner_radius"/>
+ </shape>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:radius="@dimen/template_grid_item_corner_radius"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="rectangle">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ <corners
+ android:radius="@dimen/template_grid_item_corner_radius"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml
new file mode 100644
index 0000000..00a5abb
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_header_button_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:inset="@dimen/template_header_button_focus_inset"
+ android:drawable="@drawable/template_oval_focus_background"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml
new file mode 100644
index 0000000..f8a89ae
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/drawable/template_oval_focus_background.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?templateRippleColor">
+
+ <!-- Ripple layer masked inset the thickness of the ring so the ripple layer
+ is only drawn within the bounds of the ring -->
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <inset android:inset="@dimen/template_focus_oval_ripple_inset">
+ <shape android:shape="oval">
+ <solid android:color="@color/template_ripple_color_selector"/>
+ </shape>
+ </inset>
+ </item>
+
+ <!-- Child layer for drawing foreground ring in hovered and focused states. -->
+ <item>
+ <selector>
+ <item android:state_hovered="true" android:state_window_focused="true">
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_hovered"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" android:state_window_focused="true">
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/template_focus_ring_stroke_width_focused"
+ android:color="?templateFocusAccentColor"/>
+ </shape>
+ </item>
+ </selector>
+ </item>
+</ripple>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml
new file mode 100644
index 0000000..de39d96
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_end_interpolator.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:pathData="C0.2,0 0.1,1 0.5, 1 L 1,1" />
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml
new file mode 100644
index 0000000..34eba93
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/interpolator/default_trim_start_interpolator.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:pathData="L0.5,0 C 0.7,0 0.6,1 1, 1" />
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml
new file mode 100644
index 0000000..67bc95b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/layout/template_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--The template view is marked as focusable in case nothing in the template presenter is focusable.
+It will only take focus if no view under its hierarchy is focusable.
+This is needed for the touchpad mode, which requires at least one focusable view in the screen. -->
+<com.android.car.libraries.templates.host.view.TemplateView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/template_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.ui.FocusParkingView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <com.android.car.libraries.apphost.view.SurfaceViewContainer
+ android:id="@+id/surface_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ <FrameLayout
+ android:id="@+id/template_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <TextView
+ android:id="@+id/debug_overlay"
+ android:layout_gravity="bottom|end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?templateDebugMessageBackgroundColor"
+ android:layout_margin="10dp"
+ android:padding="5dp"
+ android:gravity="end"
+ style="?templateMessageDebugTextStyle"
+ android:visibility="gone"/>
+
+</com.android.car.libraries.templates.host.view.TemplateView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml
new file mode 100644
index 0000000..799ad34
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-night/colors.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Color definitions for the UI in the templates.
+
+ !!! IMPORTANT !!!
+ Do not refer to these colors directly from views. Colors must be referred
+ to through theme attributes (in attrs.xml).
+
+ !!! IMPORTANT !!!
+ Do not define colors with a RGB value in here (e.g. #FF9B4DF8). These
+ colors should all refer to colors defined in @color/default.
+
+ If you need a color that's not there already, make a change to add it and
+ get that approved. -->
+<resources>
+ <!-- Status bar. -->
+ <color name="template_status_bar_end_color">@color/default_black</color>
+
+ <!-- Night mode color scheme for map markers. -->
+ <color name="template_marker_default_background_color">@color/default_white</color>
+ <color name="template_marker_map_default_content_color">@color/default_black</color>
+ <color name="template_marker_list_default_content_color">@color/default_white</color>
+ <color name="template_marker_custom_background_content_color">@color/default_black</color>
+ <color name="template_marker_default_border_color">@color/default_black</color>
+ <color name="template_marker_custom_border_color">@color/default_black</color>
+ <color name="template_anchor_default_background_color">@color/default_white</color>
+ <color name="template_anchor_border_color">@color/default_black</color>
+ <color name="template_anchor_dot_color">@color/default_gradient_black_64</color>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml
new file mode 100644
index 0000000..65eecaa
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w600dp/integers.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- TODO(b/171342295): Update the dp threshold for deciding number of grid items.-->
+<resources>
+ <integer name="template_grid_items_per_row">3</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml
new file mode 100644
index 0000000..99cfd75
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Markers. -->
+ <dimen name="template_marker_icon_size">48dp</dimen>
+ <dimen name="template_marker_image_size">54dp</dimen>
+</resources> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml
new file mode 100644
index 0000000..6fef3f0
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp-h520dp/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- The appearance of the markers in the map view. -->
+ <style name="TextAppearance.Marker" parent="TextAppearance.Template.Body1">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml
new file mode 100644
index 0000000..a59a90d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values-w930dp/integers.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- TODO(b/171342295): Update the dp threshold for deciding number of grid items.-->
+<resources>
+ <integer name="template_grid_items_per_row">4</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml
new file mode 100644
index 0000000..958b65b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/attrs.xml
@@ -0,0 +1,624 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- The theme attributes for the template UI.
+ Template layouts and their widgets should not hardcode any styles for the most part but instead
+ use references to these theme attributes, thus making the look and feel of the template UI
+ completely defined by this theme. -->
+ <declare-styleable name="TemplateTheme">
+ <!-- Map markers. -->
+ <!-- The appearance of markers in the map view. -->
+ <attr name="templateMapMarkerAppearance" format="reference"/>
+
+ <!-- The appearance of markers in the list view. -->
+ <attr name="templateListMarkerAppearance" format="reference"/>
+
+ <!-- Content containers -->
+ <!-- Plain content container attributes. A plain container displays the
+ content on a flat surface with no rounded corners, shadows, etc. -->
+ <attr name="templatePlainContentContainerStyle" format="reference"/>
+ <attr name="templatePlainContentContainerWidth" format="dimension"/>
+ <attr name="templatePlainContentLayoutGravity" format="integer"/>
+ <attr name="templatePlainContentGravity" format="integer"/>
+ <attr name="templatePlainContentHorizontalPadding" format="dimension"/>
+ <attr name="templatePlainContentBackgroundColor" format="color"/>
+
+ <!-- Card content container attributes. -->
+ <attr name="templateCardContentContainerStyle" format="reference"/>
+ <attr name="templateCardRoutingContentContainerStyle" format="reference"/>
+ <attr name="templateCardContentContainerDefaultWidth" format="dimension"/>
+ <attr name="templateCardContentContainerStartMargin" format="dimension"/>
+ <attr name="templateCardContentContainerTopMargin" format="dimension"/>
+ <attr name="templateCardContentContainerBottomMargin" format="dimension"/>
+ <attr name="templateCardContentContainerMinHeight" format="dimension"/>
+
+ <!-- Headers -->
+ <!-- The height of the header, which contains a title and other elements
+ such as the app icon or a back button. -->
+ <attr name="templateHeaderHeight" format="dimension"/>
+
+ <!-- The text style to be used in the header. -->
+ <attr name="templateHeaderTextStyle" format="reference"/>
+
+ <!-- The size of the icon of a button in the header (aspect ratio 1:1). -->
+ <attr name="templateHeaderButtonIconSize" format="dimension"/>
+
+ <!-- The color of the tint for header buttons (e.g. the back button). -->
+ <attr name="templateHeaderButtonIconTint" format="color"/>
+
+ <!-- The size of the button container in the header (aspect ratio 1:1). -->
+ <attr name="templateHeaderButtonContainerSize" format="dimension"/>
+
+ <!-- The start margin of the button container in the header. -->
+ <attr name="templateHeaderButtonStartSpacing" format="dimension"/>
+
+ <!-- Height of the buttons. Both action buttons and FAB. -->
+ <attr name="templateButtonHeight" format="dimension"/>
+
+ <!-- The corner radius used for non-button components. -->
+ <attr name="templateCornerRadius" format="dimension"/>
+
+ <!-- The corner radius use for the buttons. -->
+ <attr name="templateButtonCornerRadius" format="dimension"/>
+
+ <!-- The spacing around the header title. -->
+ <attr name="templateHeaderTextVerticalSpacing" format="dimension"/>
+ <attr name="templateHeaderTextStartSpacing" format="dimension"/>
+ <attr name="templateHeaderTextEndSpacing" format="dimension"/>
+ <attr name="templateHeaderTextNoIconStartSpacing" format="dimension"/>
+
+ <!-- The background of a header button. Used to implement focus selection
+ state, ripple effects, etc. -->
+ <attr name="templateHeaderButtonBackground" format="reference"/>
+
+ <!-- The color of the header background for templates where it is used. -->
+ <attr name="templateHeaderBackgroundColor" format="reference"/>
+
+ <!-- Theme attributes for rows in lists. -->
+ <!-- The background color of a row container view.
+ This color is used only for color contrast checking, and not for actual coloring of the grid item background. -->
+ <attr name="templateRowBackgroundColor" format="color"/>
+
+ <!-- The background color of a row container view in the selected state.-->
+ <attr name="templateRowSelectedBackgroundColor" format="color"/>
+
+ <!-- The color of the placeholder while an image is loading in a row.-->
+ <attr name="templateRowImagePlaceholderColor" format="color"/>
+
+ <!-- Sign in Template -->
+ <attr name="templateSignInContainerStyle" format="reference"/>
+ <attr name="templateSignInMethodViewMaxWidth" format="dimension"/>
+ <attr name="templateSignInInstructionTextStyle" format="reference"/>
+ <attr name="templateSignInProviderSignInButtonStyle" format="reference"/>
+ <attr name="templateSignInPinTextStyle" format="reference"/>
+ <attr name="templateSignInPinBackgroundColor" format="color"/>
+ <attr name="templateSignInPinCornerRadius" format="dimension"/>
+ <attr name="templateSignInPinBackground" format="reference"/>
+ <attr name="templateSignInPinPadding" format="dimension"/>
+ <attr name="templateSignInQRCodeImageWidth" format="dimension"/>
+ <attr name="templateSignInAdditionalTextStyle" format="reference"/>
+ <attr name="templateSignInErrorMessageStyle" format="reference"/>
+ <attr name="templateSignInInputViewStyle" format="reference"/>
+
+ <!-- Hyperlink Text -->
+ <attr name="templateHyperlinkTextColor" format="color"/>
+
+ <!-- The default tint for an icon in a row. -->
+ <attr name="templateRowDefaultIconTint" format="color"/>
+
+ <!-- The padding to the left and right of a row's contents. -->
+ <attr name="templateRowHorizontalPadding" format="dimension"/>
+
+ <!-- The padding to the left and right of a half row's contents. -->
+ <attr name="templateRowHorizontalHalfPadding" format="dimension"/>
+
+ <!-- The padding to the left and right of the text inside of a row. -->
+ <attr name="templateRowTextHorizontalPadding" format="dimension"/>
+
+ <!-- The padding to the left and right of the text inside of a half row. -->
+ <attr name="templateRowTextHorizontalHalfPadding" format="dimension"/>
+
+ <!-- The padding to the bottom of a half list's contents. -->
+ <attr name="templateHalfListBottomPadding" format="dimension"/>
+
+ <!-- The vertical padding inside of a half row. -->
+ <attr name="templateHalfListPaddingVertical" format="dimension"/>
+
+ <!-- The style of the title of a row. -->
+ <attr name="templateRowTitleStyle" format="reference"/>
+
+ <!-- The style of the secondary text of a row. -->
+ <attr name="templateRowSecondaryTextStyle" format="reference"/>
+
+ <!-- The style of the section header. -->
+ <attr name="templateRowSectionHeaderStyle" format="reference"/>
+
+ <!-- The style of the text that indicates a list is empty. -->
+ <attr name="templateRowListEmptyTextStyle" format="reference"/>
+
+ <!-- The image dimensions (for PaneTemplate) in the row list template. -->
+ <attr name="templateRowListToLargeImageRatio" format="dimension"/>
+ <attr name="templateRowListLargeImageContainerMaxWidth" format="dimension"/>
+ <attr name="templateRowListLargeImageAspectRatio" format="dimension"/>
+
+ <!-- Padding between the (PaneTemplate) image and row list -->
+ <attr name="templateRowListAndImagePadding" format="dimension"/>
+
+ <!-- The background for the top, middle, and bottom rows, and for a single
+ row that is both top and bottom, for lists that show rows with
+ a rounded corner background. These backgrounds can be used for
+ providing rounded corners to the list, or they can all simply be set
+ to the same background, if such an effect is not desired. -->
+ <attr name="templateRowBackgroundSectionalTop" format="reference"/>
+ <attr name="templateRowBackgroundSectionalMiddle" format="reference"/>
+ <attr name="templateRowBackgroundSectionalBottom" format="reference"/>
+ <attr name="templateRowBackgroundSectionalTopBottom" format="reference"/>
+
+ <!-- The margin at the bottom of a section in a list. -->
+ <attr name="templateRowBackgroundSectionalBottomMargin" format="dimension"/>
+
+ <!-- The placeholder for asynchronously loaded row images. -->
+ <attr name="templateRowImagePlaceholder" format="reference"/>
+
+ <!-- The background for rows with square corners. -->
+ <attr name="templateRowBackgroundSimple" format="reference"/>
+
+ <!-- The minimum margin between the rows's title and the edge
+ of the row. -->
+ <attr name="templateRowMinHeight" format="dimension"/>
+
+ <!-- Attributes of the icons in a row of a list. -->
+ <attr name="templateRowIconStyle" format="reference"/>
+ <attr name="templateRowIconSize" format="dimension"/>
+ <attr name="templateRowRadioButtonSize" format="dimension"/>
+ <attr name="templateRowImageSizeSmall" format="dimension"/>
+ <attr name="templateRowImageSizeLarge" format="dimension"/>
+
+ <!-- Attributes for the marker label that is displayed in the list rows
+ when the row has a location attached to it. -->
+ <!-- The minimum width of the marker label. The actual width can expand based on the content string.-->
+ <attr name="templateRowMarkerMinSize" format="dimension"/>
+
+ <!-- The margin from the edge of the row to the marker label. -->
+ <attr name="templateRowMarkerLabelMargin" format="dimension"/>
+
+ <!-- The height of a selection element's container in a row (e.g. a toggle
+ or radio button. -->
+ <attr name="templateRowSelectionContainerHeight" format="dimension"/>
+
+ <!-- The half row minimum height. -->
+ <attr name="templateHalfRowMinHeight" format="dimension"/>
+
+ <!-- The paddings used around the half row. -->
+ <attr name="templateHalfRowHorizontalPadding" format="dimension"/>
+ <attr name="templateHalfRowVerticalPadding" format="dimension"/>
+
+ <!-- The paddings used around the full row. -->
+ <attr name="templateFullRowStartPadding" format="dimension"/>
+ <attr name="templateFullRowEndPadding" format="dimension"/>
+
+ <!-- The spacing used between the image and text of the half row. -->
+ <attr name="templateHalfRowImageToTextSpacing" format="dimension"/>
+
+ <!-- The spacing used between the primary and secondary text of the half row. -->
+ <attr name="templateHalfRowTextToTextSpacing" format="dimension"/>
+
+ <!-- Size of the images used within the half list row. -->
+ <attr name="templateHalfRowImageSize" format="dimension"/>
+
+ <!-- The full list row's chevron icon on the right side. -->
+ <attr name="templateFullRowChevronIcon" format="reference"/>
+
+ <!-- The full list row's chevron height. -->
+ <attr name="templateFullRowChevronHeight" format="dimension"/>
+
+ <!-- The full list row's chevron width. -->
+ <attr name="templateFullRowChevronWidth" format="dimension"/>
+
+ <!-- The half list row's chevron icon on the right side. -->
+ <attr name="templateHalfRowChevronIcon" format="reference"/>
+
+ <!-- The background for the grid item and its containers. -->
+ <attr name="templateGridItemBackground" format="reference"/>
+
+ <!-- The background color for the grid item.
+ This color is used only for color contrast checking, and not for actual coloring of the grid item background. -->
+ <attr name="templateGridItemBackgroundColor" format="reference"/>
+
+ <!-- The style of the title of a grid item. -->
+ <attr name="templateGridItemTitleStyle" format="reference"/>
+
+ <!-- The style of the secondary text line below the title of a grid item. -->
+ <attr name="templateGridItemTextStyle" format="reference"/>
+
+ <!-- The tint variations for an icon in a grid item. -->
+ <attr name="templateGridItemDefaultIconTint" format="color"/>
+
+ <!-- The maximum width of a grid item text container. -->
+ <attr name="templateGridItemTextContainerMaxWidth" format="dimension"/>
+
+ <!-- The bottom padding of the text inside of a grid item. -->
+ <attr name="templateGridItemTextBottomPadding" format="dimension"/>
+
+ <!-- The bottom padding of the image inside of a grid item. -->
+ <attr name="templateGridItemImageBottomPadding" format="dimension"/>
+
+ <!-- The padding for the grid items. -->
+ <attr name="templateGridItemHorizontalSpacing" format="dimension"/>
+ <attr name="templateGridItemVerticalSpacing" format="dimension"/>
+
+ <!-- The number of grid items in a grid row. -->
+ <attr name="templateGridItemsPerRow" format="integer"/>
+
+ <!-- Theme attributes for the grid. -->
+ <attr name="templateGridStyle" format="reference"/>
+ <attr name="templateGridRecyclerViewPaddingRight" format="reference"/>
+
+ <!-- The style of the text that indicates a grid is empty. -->
+ <attr name="templateGridEmptyTextStyle" format="reference"/>
+
+ <!-- Action buttons and FABs. -->
+ <!-- The margin between action buttons. -->
+ <attr name="templateActionButtonMargin" format="dimension"/>
+
+ <!-- The style of an action displayed as a button. -->
+ <attr name="templateActionButtonStyle" format="reference"/>
+
+ <!-- The style of the text inside of an action button. -->
+ <attr name="templateActionButtonTextStyle" format="reference"/>
+
+ <!-- The style of the text inside of a primary action button. -->
+ <attr name="templateActionButtonPrimaryTextStyle" format="reference"/>
+
+ <!-- The default background color of an action displayed as a button. -->
+ <attr name="templateActionButtonDefaultBackgroundColor" format="color" />
+
+ <!-- The background color of a primary action displayed as a button. -->
+ <attr name="templateActionButtonPrimaryBackgroundColor" format="color" />
+
+ <!-- The default foreground drawable of an action displayed as a button. -->
+ <attr name="templateActionButtonForeground" format="reference" />
+
+ <!-- The default background drawable of an action displayed as a button. -->
+ <attr name="templateActionButtonBackground" format="reference" />
+
+ <!-- The height of an action button. -->
+ <attr name="templateActionButtonHeight" format="dimension"/>
+
+ <!-- The minimum touch area size for action buttons. -->
+ <attr name="templateActionButtonTouchTargetSize" format="dimension"/>
+
+ <!-- Whether buttons in the action button list (e.g. used in PaneTemplate) stretch to fill the horizontal space. -->
+ <attr name="templateActionButtonListButtonStretchHorizontal" format="boolean"/>
+
+ <!-- Whether OEM colors should override 3P provided colors. -->
+ <attr name="templateActionButtonUseOemColors" format="boolean"/>
+
+ <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate).
+ The possible values are:
+ <ul>
+ <li>0: center (default)
+ <li>1: left
+ <li>2: right
+ </ul> -->
+ <attr name="templateActionButtonPrimaryHorizontalOrder" format="integer"/>
+
+ <!-- The gravity of action button list (e.g. used in MessageTemplate, SigninTemplate and LongMessageTemplate).
+ The possible values are:
+ <ul>
+ <li>0: center (default)
+ <li>1: bottom
+ </ul> -->
+ <attr name="templateActionButtonListGravity" format="integer"/>
+ <!-- The alignment of contents in buttons in the action button list (e.g. used in PaneTemplate).
+ The possible values are:
+ <ul>
+ <li>0: center (default)
+ <li>1: left
+ <li>2: right
+ </ul> -->
+ <attr name="templateActionButtonListButtonContentAlignment" format="integer"/>
+
+ <!-- The maximum width of a button in the action button list, e.g. used in PaneTemplate.
+ This value will be used only when `templateActionButtonListFillHorizontalSpace` is set to `true`. -->
+ <attr name="templateActionButtonListButtonMaxWidth" format="dimension"/>
+
+ <!-- The spacing between the content and the aligned side in a button in the action button list, e.g. used in PaneTemplate.
+ This value will be used only when `templateActionButtonListButtonContentAlignment` is set to 1 (left) or 2 (right).
+ When this value is used, `templateActionIconTextStartSpacing`, `templateActionIconTextEndSpacing`, and `templateActionTextHorizontalSpacing` will be ignored. -->
+ <attr name="templateActionButtonSideAlignmentSpacing" format="dimension"/>
+
+ <!-- The vertical spacing of the action button list row, e.g. used in PaneTemplate. -->
+ <attr name="templateActionButtonListRowVerticalSpacing" format="dimension"/>
+
+ <!-- The size of an icon inside of an action button or FAB. -->
+ <attr name="templateActionIconSize" format="dimension"/>
+ <attr name="templateActionIconSizeMin" format="dimension"/>
+ <attr name="templateActionIconSizeMax" format="dimension"/>
+
+ <!-- The spacing between the start side of a FAB or button and the action icon.
+ The spacing is applied only when the button has both icon and text. -->
+ <attr name="templateActionIconTextStartSpacing" format="dimension"/>
+
+ <!-- The spacing between the end side of a FAB or button and the action icon.
+ The spacing is applied only when the button has both icon and text. -->
+ <attr name="templateActionIconTextEndSpacing" format="dimension"/>
+
+ <!-- The spacing between the icon and the text in a FAB or button. -->
+ <attr name="templateActionIconToTextSpacing" format="dimension"/>
+
+ <!-- The spacing between the start and end sides of a FAB or button and the action text.
+ The spacing is applied only when the button only has the text. -->
+ <attr name="templateActionTextHorizontalSpacing" format="dimension"/>
+
+ <!-- The default tint of an icon inside of an action button or FAB. -->
+ <attr name="templateActionDefaultIconTint" format="color"/>
+
+ <!-- The min width of an action button or FAB with text. -->
+ <attr name="templateActionWithTextMinWidth" format="integer"/>
+
+ <!-- The min width of an action button or FAB with an icon only. -->
+ <attr name="templateActionWithoutTextMinWidth" format="dimension"/>
+
+ <!-- The max ems of the text inside of a button when there is no icon. -->
+ <attr name="templateActionButtonTextMaxEmsNoIcon" format="integer"/>
+
+ <!-- The max ems of the text inside of a button when there is an icon. -->
+ <attr name="templateActionButtonTextMaxEmsWithIcon" format="integer"/>
+
+ <!-- The max ems of the text inside of a FAB when there is no icon. -->
+ <attr name="templateFabTextMaxEmsNoIcon" format="integer"/>
+
+ <!-- The max ems of the text inside of a FAB when there is an icon. -->
+ <attr name="templateFabTextMaxEmsWithIcon" format="integer"/>
+
+ <!-- The width of the border of a secondary button. -->
+ <attr name="templateActionButtonSecondaryBorderWidth" format="dimension"/>
+
+ <!-- The color of the border of a secondary button. -->
+ <attr name="templateActionButtonSecondaryBorderColor" format="color"/>
+
+ <!-- The margin between buttons in the action strip. -->
+ <attr name="templateActionStripButtonMargin" format="dimension"/>
+
+ <!-- The padding around the action strip. -->
+ <attr name="templateActionStripPadding" format="dimension"/>
+
+ <!-- The color of buttons in the action strip in full-screen templates. -->
+ <attr name="templateActionStripButtonBackgroundColor" format="color"/>
+
+ <!-- The light color of buttons in the action strip as FABs. -->
+ <attr name="templateActionStripFabBackgroundColorLight" format="color"/>
+
+ <!-- The dark color of buttons in the action strip as FABs. -->
+ <attr name="templateActionStripFabBackgroundColorDark" format="color"/>
+
+ <!-- The appearance of action strip buttons as FABs. -->
+ <attr name="templateActionStripFabAppearance" format="reference"/>
+
+ <!-- The appearance of action strip buttons in full-screen templates. -->
+ <attr name="templateActionStripFullTemplateFabAppearance" format="reference"/>
+
+ <!-- Ripple attributes, common for all elements using ripples. -->
+ <attr name="templateRippleColor" format="color"/>
+ <attr name="templateRippleSelectorColor" format="color"/>
+
+ <!-- Toggles and radio buttons. -->
+ <attr name="templateToggleWidth" format="dimension"/>
+ <attr name="templateToggleHeight" format="dimension"/>
+ <attr name="templateToggleInactiveTrackColor" format="color"/>
+ <attr name="templateToggleInactiveThumbColor" format="color"/>
+ <attr name="templateToggleActiveTrackColor" format="color"/>
+ <attr name="templateToggleActiveThumbColor" format="color"/>
+ <attr name="templateRadioButtonSize" format="dimension"/>
+
+ <!-- Clickable spans. -->
+ <attr name="templateClickableSpanHighlightForegroundColor" format="color"/>
+ <attr name="templateClickableSpanHighlightBackgroundColor" format="color"/>
+
+ <!-- Focus. -->
+ <attr name="templateFocusAccentColor" format="color"/>
+ <attr name="templateFocusNoContentAccentColor" format="color"/>
+ <attr name="templateFocusStateInactive" format="boolean"/>
+ <attr name="templateFocusRingColor" format="color"/>
+ <attr name="templateFocusRingNoAccentColor" format="color"/>
+
+ <!-- Editable text. -->
+ <attr name="templateEditTextStyle" format="reference"/>
+ <attr name="templateEditTextActiveColor" format="color"/>
+ <attr name="templateEditTextEnabledColor" format="color"/>
+ <attr name="templateEditTextErrorColor" format="color"/>
+ <attr name="templateEditTextDisabledColor" format="color"/>
+ <attr name="templateEditTextErrorVerticalSpacing" format="dimension"/>
+ <attr name="templateEditTextErrorHorizontalSpacing" format="dimension"/>
+
+ <!-- Search bar. -->
+ <!-- Maximum width of the search bar. -->
+ <attr name="templateSearchBarMaxWidth" format="dimension"/>
+
+ <!-- The search icon on the left side of the search bar. -->
+ <attr name="templateSearchBarIcon" format="reference"/>
+
+ <!-- Images. -->
+ <!-- The size of a large image. -->
+ <attr name="templateLargeImageSize" format="dimension"/>
+
+ <!-- The minimum size of a large image. -->
+ <attr name="templateLargeImageSizeMin" format="dimension"/>
+
+ <!-- The maximum size of a large image. -->
+ <attr name="templateLargeImageSizeMax" format="dimension"/>
+
+ <!-- Message template. -->
+ <!-- The default tint of an icon inside of the message template. -->
+ <attr name="templateMessageDefaultIconTint" format="color"/>
+
+ <!-- The style of the text in the title of the message template. -->
+ <attr name="templateMessageTitleTextStyle" format="reference"/>
+
+ <!-- The spacing used on top of the title of the message template. -->
+ <attr name="templateMessageTitleTopSpacing" format="dimension"/>
+
+ <!-- The spacing used on top of the buttons of the message template. -->
+ <attr name="templateMessageButtonsTopSpacing" format="dimension"/>
+
+ <!-- The spacing used on top and bottom of the sticky buttons. -->
+ <attr name="templateStickyButtonsVerticalSpacing" format="dimension"/>
+
+ <!-- The style of the text in the long message template. -->
+ <attr name="templateMessageLongTextStyle" format="reference"/>
+
+ <!-- The color of the text and background of the debug message showing the
+ callstack in error screens. -->
+ <attr name="templateMessageDebugTextStyle" format="reference"/>
+ <attr name="templateDebugMessageBackgroundColor" format="color"/>
+
+ <!-- Navigation routing template. -->
+ <!-- The maximum heights and width of an image in a text span.
+ body2 and body3 variants should be used along text views configured
+ with those respective font sizes. -->
+ <attr name="templateRoutingImageSpanBody2MaxHeight" format="dimension"/>
+ <attr name="templateRoutingImageSpanBody3MaxHeight" format="dimension"/>
+
+ <!-- The horizontal and vertical padding values in the routing card. -->
+ <attr name="templateNavCardPaddingHorizontal" format="dimension"/>
+ <attr name="templateNavCardPaddingVertical" format="dimension"/>
+
+ <!-- The small vertical padding value in the routing card and the travel estimate card. -->
+ <attr name="templateNavCardSmallPaddingVertical" format="dimension"/>
+
+ <!-- The horizontal padding value between the icon and the distance text in the routing card. -->
+ <attr name="templateRoutingStepsCardIconToDistanceSpacingHorizontal" format="dimension"/>
+
+ <!-- Ratio of the image span (width/height) based on the max height value -->
+ <attr name="templateRoutingImageSpanRatio" format="float"/>
+
+ <!-- The dimensions of the large image in the routing card (ratio 1:1). -->
+ <attr name="templateNavCardLargeImageSize" format="dimension"/>
+ <attr name="templateNavCardLargeImageSizeMin" format="dimension"/>
+ <attr name="templateNavCardLargeImageSizeMax" format="dimension"/>
+
+ <!-- The dimensions of the small image in the routing card (ratio 1:1). -->
+ <attr name="templateNavCardSmallImageSize" format="dimension"/>
+ <attr name="templateNavCardSmallImageSizeMin" format="dimension"/>
+ <attr name="templateNavCardSmallImageSizeMax" format="dimension"/>
+
+ <!-- Size of the large text of the routing card. -->
+ <attr name="templateNavCardLargeTextSize" format="dimension"/>
+
+ <!-- Size of the xlarge text of the routing card. -->
+ <attr name="templateNavCardXLargeTextSize" format="dimension"/>
+
+ <!-- The fallback text color used in the routing card when the OEM-provided default text color is not used. -->
+ <attr name="templateNavCardFallbackContentColor" format="color"/>
+
+ <!-- The style of the distance text in the detailed step view. -->
+ <attr name="templateRoutingDistanceStyle" format="dimension"/>
+
+ <!-- The style of the description text in the detailed step view. -->
+ <attr name="templateRoutingDescriptionStyle" format="dimension"/>
+
+ <!-- The style of the description text in the compact step view. -->
+ <attr name="templateRoutingCompactDescriptionStyle" format="dimension"/>
+
+ <!-- The style of the description text in the travel estimate view. -->
+ <attr name="templateRoutingTravelEstimateStyle" format="dimension"/>
+
+ <!-- The height of the container of the lanes image. -->
+ <attr name="templateRoutingLanesImageContainerHeight" format="dimension"/>
+
+ <!-- The vertical padding of the container of the lanes image. -->
+ <attr name="templateRoutingLanesImageContainerVerticalPadding" format="dimension"/>
+
+ <!-- The horizontal padding of the container of the lanes image. -->
+ <attr name="templateRoutingLanesImageContainerHorizontalPadding" format="dimension"/>
+
+ <!-- The color of the background of the lanes image. -->
+ <attr name="templateRoutingLanesImageBackgroundColor" format="color"/>
+
+ <!-- The color of the background of the junction image. -->
+ <attr name="templateRoutingJunctionImageBackgroundColor" format="color"/>
+
+ <!-- The style of the primary text for the title in the message view. -->
+ <attr name="templateRoutingMessagePrimaryStyle" format="reference"/>
+
+ <!-- The style of the secondary text for the message view. -->
+ <attr name="templateRoutingMessageSecondaryStyle" format="reference"/>
+
+ <!-- The horizontal inner padding between the image and the primary text in the message view. -->
+ <attr name="templateRoutingMessageInnerPaddingHorizontal" format="dimension"/>
+
+ <!-- The vertical inner padding between the primary and secondary texts in the message view. -->
+ <attr name="templateRoutingMessageInnerPaddingVertical" format="dimension"/>
+
+ <!-- The width and min height of the container which shows the current and
+ next steps in the routing card. -->
+ <attr name="templateRoutingStepsCardContentContainerMinWidth" format="dimension"/>
+ <attr name="templateRoutingStepsCardContentContainerMinHeight" format="dimension"/>
+
+ <!-- The color of the divider in the routing card. -->
+ <attr name="templateRoutingDividerColor" format="dimension"/>
+
+ <!-- Status bar. -->
+ <!-- Status bar gradient background start and end colors. -->
+ <attr name="templateStatusBarStartColor" format="color"/>
+ <attr name="templateStatusBarEndColor" format="color"/>
+
+ <!-- Defines a minimum top padding for the presenter in case there is no status bar,
+ i.e. Widescreen Android Auto does not have status bar. -->
+ <attr name="templateStatusBarMinimumTopPadding" format="dimension"/>
+
+ <!-- No content view. -->
+ <attr name="templateNoContentFocusCornerRadius" format="dimension"/>
+
+ <!-- Loading spinner. -->
+ <attr name="templateLoadingSpinnerStyle" format="reference"/>
+ <attr name="templateLoadingSpinnerColor" format="color"/>
+
+ <!-- Attributes of most dividers used throughout the UI. -->
+ <attr name="templateDividerColor" format="color"/>
+ <attr name="templateDividerThickness" format="dimension"/>
+
+ <!-- A fraction used for implementing margins that adapt to the width of the screen.
+ For example, some templates may have a 12% minimum margin (with respect of the
+ screen width) on either side. This value should be set to 1.0 minus twice
+ the margin percentage (in other words, it is the width of the content itself). -->
+ <attr name="templateAdaptiveWidthFraction" format="float"/>
+
+ <!-- The duration in milliseconds of a template transition animation e.g. a cross fade. -->
+ <attr name="templateUpdateAnimationDurationMilliseconds" format="integer"/>
+
+ <!-- Standard colors -->
+ <attr name="templateStandardBlue" format="color"/>
+ <attr name="templateStandardRed" format="color"/>
+ <attr name="templateStandardGreen" format="color"/>
+ <attr name="templateStandardYellow" format="color"/>
+
+ <!-- The spacing used between controls e.g. buttons. -->
+ <attr name="templateControlToControlSpacingHorizontal" format="dimension"/>
+
+ <!-- The maximum number of rows in a list view. -->
+ <attr name="templateListMaxLength" format="integer"/>
+
+ <!-- The maximum number of grid items in a grid view. -->
+ <attr name="templateGridMaxLength" format="integer"/>
+
+ <!-- Whether or not NavState events should be sent to the system via NavigationManager -->
+ <attr name="templateSendNavStateToSystem" format="boolean"/>
+ </declare-styleable>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml
new file mode 100644
index 0000000..31833f1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/colors.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Color definitions for the UI in the templates.
+
+ !!! IMPORTANT !!!
+ Do not refer to these colors directly from views. Colors must be referred
+ to through theme attributes (in attrs.xml).
+
+ !!! IMPORTANT !!!
+ Do not define colors with a RGB value in here (e.g. #FF9B4DF8). These
+ colors should all refer to colors defined in @color/default.
+
+ If you need a color that's not there already, make a change to add it and
+ get that approved. -->
+<resources>
+
+ <color name="template_black">@color/default_black</color>
+ <color name="template_black_64">@color/default_gradient_black_64</color>
+
+ <color name="template_white">@color/default_white</color>
+ <color name="template_white_16">@color/default_gradient_white_16</color>
+ <color name="template_white_24">@color/default_gradient_white_24</color>
+ <color name="template_white_46">@color/default_gradient_white_46</color>
+ <color name="template_white_56">@color/default_gradient_white_56</color>
+ <color name="template_white_72">@color/default_gradient_white_72</color>
+
+ <color name="template_gray_50">@color/default_gray_50</color>
+ <color name="template_gray_100">@color/default_gray_100</color>
+ <color name="template_gray_400">@color/default_gray_400</color>
+ <color name="template_gray_500">@color/default_gray_500</color>
+ <color name="template_gray_700">@color/default_gray_700</color>
+ <color name="template_gray_846">@color/default_gray_846</color>
+ <color name="template_gray_868">@color/default_gray_868</color>
+ <color name="template_gray_878">@color/default_gray_878</color>
+ <color name="template_gray_900">@color/default_gray_900</color>
+ <color name="template_gray_928">@color/default_gray_928</color>
+
+ <!-- Standard colors -->
+ <color name="template_standard_red">@color/car_app_ui_standard_red</color>
+ <color name="template_standard_red_dark">@color/car_app_ui_standard_red_dark</color>
+ <color name="template_standard_green">@color/car_app_ui_standard_green</color>
+ <color name="template_standard_green_dark">@color/car_app_ui_standard_green_dark</color>
+ <color name="template_standard_blue">@color/car_app_ui_standard_blue</color>
+ <color name="template_standard_blue_dark">@color/car_app_ui_standard_blue_dark</color>
+ <color name="template_standard_yellow">@color/car_app_ui_standard_yellow</color>
+ <color name="template_standard_yellow_dark">@color/car_app_ui_standard_yellow_dark</color>
+
+ <!-- Colors derived from OEM values. These values should be customized
+ through car_ui. -->
+ <color name="template_icon_tint_color">@color/car_ui_text_color_primary</color>
+ <color name="template_text_color_primary">@color/car_ui_text_color_primary</color>
+ <color name="template_card_background_color">@color/car_ui_activity_background_color</color>
+ <color name="template_plain_content_background_color">@color/car_ui_activity_background_color</color>
+
+ <!-- Color of text inside of a content view. -->
+ <color name="template_content_text_color">@color/template_gray_50</color>
+
+ <!-- Color of secondary text inside of a content view. -->
+ <color name="template_content_text_color_secondary">@color/template_gray_500</color>
+
+ <!-- Color of focus text inside of a content view. -->
+ <color name="template_content_text_color_focus">@color/template_standard_blue</color>
+
+ <!-- Map markers. -->
+ <color name="template_marker_default_background_color">@color/template_white</color>
+ <color name="template_marker_map_default_content_color">@color/template_black</color>
+ <color name="template_marker_list_default_content_color">@color/template_white</color>
+ <color name="template_marker_custom_background_content_color">@color/template_white</color>
+ <color name="template_marker_default_border_color">@color/template_gray_700</color>
+ <color name="template_marker_custom_border_color">@color/template_white</color>
+ <color name="template_anchor_default_background_color">@color/template_black</color>
+ <color name="template_anchor_border_color">@color/template_white</color>
+ <color name="template_anchor_dot_color">@color/template_white_56</color>
+
+ <!-- Content Containers. -->
+ <color name="template_container_background">@color/template_black</color>
+ <color name="template_content_button_color">@color/template_white</color>
+
+ <!-- Cards. -->
+ <color name="template_card_content_container_border_color">@color/template_white_24</color>
+
+ <!-- Toggles. -->
+ <color name="template_toggle_inactive_track">@color/template_white</color>
+ <color name="template_toggle_inactive_thumb">@color/template_gray_400</color>
+ <color name="template_toggle_active_track">@color/template_standard_blue</color>
+ <color name="template_toggle_active_thumb">@color/template_standard_blue</color>
+
+ <!-- Actions. -->
+ <color name="template_action_button_default_background_color">@color/car_app_ui_action_button_default_background_color</color>
+
+ <!-- Action strip FABs. -->
+ <color name="template_action_strip_fab_background_color">@color/car_app_ui_floating_button_default_background_color</color>
+ <color name="template_action_strip_fab_content_color">@color/car_app_ui_floating_button_default_text_color</color>
+
+ <!-- Message template. -->
+ <color name="template_message_debug_text_color">@color/default_message_debug_text_color</color>
+
+ <!-- Sign-in template. -->
+ <color name="template_sign_in_error_message_color">@color/template_standard_red</color>
+ <color name="template_sign_in_additional_text_color">@color/template_gray_500</color>
+
+ <!-- Read-only text. -->
+ <color name="template_read_only_text_background_color">@color/car_app_ui_read_only_text_background_color</color>
+
+ <!-- Ripples. -->
+ <color name="template_ripple_color">@color/default_controller_ripple_color</color>
+ <color name="template_ripple_selector_color">@color/default_controller_ripple_selector_color</color>
+
+ <!-- Status bar. -->
+ <color name="template_status_bar_end_color">@color/template_white</color>
+
+ <!-- Edit text. -->
+ <color name="template_edit_text_color_selector">@color/default_edit_text_color_selector</color>
+ <color name="template_edit_text_active_color">@color/car_app_ui_edit_text_active_color</color>
+ <color name="template_edit_text_enabled_color">@color/car_app_ui_edit_text_enabled_color</color>
+ <color name="template_edit_text_error_color">@color/car_app_ui_edit_text_error_color</color>
+ <color name="template_edit_text_disabled_color">@color/car_app_ui_edit_text_disabled_color</color>
+
+ <!-- Loading spinner. -->
+ <color name="template_loading_spinner_color">@color/template_standard_blue</color>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml
new file mode 100644
index 0000000..defd4c4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens.xml
@@ -0,0 +1,278 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Adaptive height and width spacing key lines. -->
+ <dimen name="template_height_keyline_0">@dimen/default_height_keyline_0</dimen>
+ <dimen name="template_height_keyline_1">@dimen/default_height_keyline_1</dimen>
+ <dimen name="template_height_keyline_2">@dimen/default_height_keyline_2</dimen>
+ <dimen name="template_height_keyline_3">@dimen/default_height_keyline_3</dimen>
+ <dimen name="template_width_keyline_0">@dimen/default_width_keyline_0</dimen>
+ <dimen name="template_width_keyline_1">@dimen/default_width_keyline_1</dimen>
+ <dimen name="template_width_keyline_2">@dimen/default_width_keyline_2</dimen>
+ <dimen name="template_width_keyline_3">@dimen/default_width_keyline_3</dimen>
+
+ <!-- Padding. -->
+ <dimen name="template_padding_0">@dimen/default_padding_0</dimen>
+ <dimen name="template_padding_1">@dimen/default_padding_1</dimen>
+ <dimen name="template_padding_2">@dimen/default_padding_2</dimen>
+ <dimen name="template_padding_3">@dimen/default_padding_3</dimen>
+ <dimen name="template_padding_4">@dimen/default_padding_4</dimen>
+ <dimen name="template_padding_5">@dimen/default_padding_5</dimen>
+ <dimen name="template_padding_6">@dimen/default_padding_6</dimen>
+ <dimen name="template_padding_7">@dimen/default_padding_7</dimen>
+ <dimen name="template_padding_8">@dimen/default_padding_8</dimen>
+
+ <!-- Minimum tap target size, used for buttons, etc. -->
+ <dimen name="template_min_tap_target_size">68dp</dimen>
+
+ <!--
+ Edge column definition
+
+ These values are used to align UI elements at the edge of the screen, even if
+ the elements belong to completely different layouts.
+ -->
+ <dimen name="template_edge_column_width">@dimen/default_edge_column_width</dimen>
+ <dimen name="template_edge_column_margin">@dimen/default_edge_column_margin</dimen>
+
+ <!--
+ This dimen represents the entire width of the edge column, margin included.
+
+ It should remain equal to:
+ 2 * template_edge_column_margin + template_edge_column_width
+ -->
+ <dimen name="template_edge_column_width_padded">@dimen/default_edge_column_width_padded</dimen>
+
+ <!-- A minimum margin value used to offset some of the text views
+ in the UI. -->
+ <dimen name="template_min_text_margin">8dp</dimen>
+
+ <!-- Common dimensions. -->
+ <item name="template_adaptive_width_fraction" format="float" type="dimen">.76</item>
+ <dimen name="template_button_touch_target_size">@dimen/car_app_ui_touch_target_size</dimen>
+
+ <!-- Images. -->
+ <dimen name="template_large_image_size">@dimen/car_app_ui_large_image_size</dimen>
+ <dimen name="template_large_image_size_min">0dp</dimen>
+ <dimen name="template_large_image_size_max">128dp</dimen>
+
+ <!-- Markers. -->
+ <dimen name="template_marker_pointer_width">16dp</dimen>
+ <dimen name="template_marker_pointer_height">10dp</dimen>
+ <dimen name="template_marker_text_horizontal_padding">8dp</dimen>
+ <dimen name="template_marker_icon_size">32dp</dimen>
+ <dimen name="template_marker_stroke">2dp</dimen>
+ <dimen name="template_marker_corner_radius">8dp</dimen>
+ <dimen name="template_marker_image_size">36dp</dimen>
+ <dimen name="template_marker_image_corner_radius">4dp</dimen>
+ <dimen name="template_marker_padding">2dp</dimen>
+
+ <!-- Plain content containers. -->
+ <dimen name="template_plain_content_container_width">400dp</dimen>
+ <dimen name="template_plain_content_container_padding">10dp</dimen>
+ <dimen name="template_plain_content_horizontal_padding">@dimen/car_app_ui_content_horizontal_margin</dimen>
+
+ <!-- Card content containers. -->
+ <dimen name="template_card_content_container_default_width">384dp</dimen>
+ <dimen name="template_card_content_container_min_width">360dp</dimen>
+ <dimen name="template_card_content_container_max_width">416dp</dimen>
+ <dimen name="template_card_content_container_oem_max_width">500dp</dimen>
+ <dimen name="template_card_content_container_start_margin">@dimen/car_app_ui_card_start_margin</dimen>
+ <dimen name="template_card_content_container_top_margin">@dimen/car_app_ui_card_top_margin</dimen>
+ <dimen name="template_card_content_container_bottom_margin">@dimen/template_width_keyline_1</dimen>
+ <dimen name="template_card_content_container_min_height">200dp</dimen>
+
+ <!-- Card container width fractions. Zero means we use the layout_width, not the fraction. -->
+ <item name="template_card_content_container_width_fraction" format="float" type="dimen">.48</item>
+ <item name="template_steps_card_content_container_width_fraction" format="float" type="dimen">.4</item>
+
+ <!-- Corresponds to +5 elevation in material spec:
+ https://standards.google/guidelines/google-material/styles/elevation.html#spec -->
+ <dimen name="template_card_content_container_elevation">12dp</dimen>
+ <dimen name="template_card_content_container_border_width">2dp</dimen>
+
+ <!-- Headers. -->
+ <dimen name="template_header_height">68dp</dimen>
+ <dimen name="template_header_text_vertical_spacing">@dimen/car_app_ui_card_header_text_padding_vertical</dimen>
+ <dimen name="template_header_text_horizontal_spacing">@dimen/car_app_ui_card_header_text_padding_horizontal</dimen>
+ <dimen name="template_header_button_icon_size">@dimen/car_app_ui_card_header_image_size</dimen>
+ <dimen name="template_header_button_start_spacing">0dp</dimen>
+ <dimen name="template_header_text_no_icon_start_spacing">@dimen/car_app_ui_card_header_no_button_text_margin_start</dimen>
+
+ <dimen name="template_header_button_focus_inset">4dp</dimen>
+
+ <!-- Lists. -->
+ <dimen name="template_row_list_max_width">@dimen/default_canvas_max_width</dimen>
+ <dimen name="template_row_list_no_scrollbar_start_padding_card">@dimen/template_padding_1</dimen>
+
+ <!-- Row List. -->
+ <dimen name="template_row_list_large_image_container_max_width">480dp</dimen>
+
+ <!-- Rows. -->
+ <dimen name="template_row_min_height">72dp</dimen>
+ <dimen name="template_row_corner_radius">@dimen/default_list_item_corner_radius</dimen>
+ <dimen name="template_row_icon_size">36dp</dimen>
+ <dimen name="template_row_image_size_small">36dp</dimen>
+ <dimen name="template_row_image_size_large">64dp</dimen>
+ <dimen name="template_row_action_max_width">240dp</dimen>
+ <dimen name="template_row_marker_min_size">36dp</dimen>
+ <dimen name="template_row_marker_label_margin">15dp</dimen>
+ <dimen name="template_row_horizontal_padding">@dimen/template_width_keyline_1</dimen>
+ <dimen name="template_row_horizontal_half_padding">@dimen/template_width_keyline_0</dimen>
+ <dimen name="template_row_text_horizontal_padding">@dimen/template_width_keyline_0</dimen>
+ <dimen name="template_row_text_horizontal_half_padding">@dimen/template_padding_0</dimen>
+ <dimen name="template_row_background_sectional_bottom_margin">@dimen/template_padding_3</dimen>
+ <dimen name="template_half_list_bottom_padding">@dimen/default_height_keyline_3</dimen>
+ <dimen name="template_half_row_padding_vertical">@dimen/default_padding_4</dimen>
+
+ <!-- Half Rows. -->
+ <dimen name="template_half_row_horizontal_padding">@dimen/car_app_ui_half_row_horizontal_padding</dimen>
+ <dimen name="template_half_row_vertical_padding">@dimen/car_app_ui_half_row_vertical_padding</dimen>
+ <dimen name="template_half_row_image_to_text_margin">@dimen/car_app_ui_half_row_image_to_text_spacing</dimen>
+ <dimen name="template_half_row_text_to_text_margin">@dimen/car_app_ui_half_row_text_to_text_spacing</dimen>
+ <dimen name="template_half_row_image_size">@dimen/car_app_ui_half_row_image_size</dimen>
+
+ <!-- Full Rows. -->
+ <dimen name="template_full_row_start_padding">@dimen/car_app_ui_full_row_start_padding</dimen>
+ <dimen name="template_full_row_end_padding">@dimen/car_app_ui_full_row_end_padding</dimen>
+ <dimen name="template_full_row_chevron_size">@dimen/car_ui_list_item_supplemental_icon_size</dimen>
+
+ <!-- Grids. -->
+ <dimen name="template_grid_max_width">@dimen/default_paged_list_view_max_content_width</dimen>
+
+ <!-- Grid items. -->
+ <dimen name="template_grid_item_image_bottom_padding">@dimen/car_app_ui_grid_item_image_to_text_spacing_vertical</dimen>
+ <dimen name="template_grid_item_text_container_max_width">196dp</dimen>
+ <dimen name="template_grid_item_text_bottom_padding">@dimen/car_app_ui_grid_item_text_to_text_spacing_vertical</dimen>
+ <dimen name="template_grid_item_horizontal_spacing">@dimen/template_width_keyline_0</dimen>
+ <dimen name="template_grid_item_vertical_spacing">@dimen/car_app_ui_grid_item_vertical_spacing</dimen>
+
+ <dimen name="template_grid_item_corner_radius">@dimen/default_list_item_corner_radius</dimen>
+ <dimen name="template_grid_item_image_background_size">50dp</dimen>
+
+ <!-- Sign-In Template. -->
+ <dimen name="template_sign_in_method_view_max_width">@dimen/car_app_ui_sign_in_method_max_width</dimen>
+ <dimen name="template_sign_in_pin_text_letter_spacing" format="float" type="dimen">.6</dimen>
+ <dimen name="template_sign_in_input_error_message_text_size">18dp</dimen>
+ <dimen name="template_sign_in_input_additional_text_size">18dp</dimen>
+ <dimen name="template_sign_in_qr_code_image_width">250dp</dimen>
+
+ <!-- Read-only Text. -->
+ <dimen name="template_read_only_text_padding">@dimen/car_app_ui_read_only_text_padding</dimen>
+
+ <!-- Scrollbar. -->
+ <dimen name="template_paged_list_scrollbar_width">@dimen/template_edge_column_width_padded</dimen>
+ <dimen name="template_paged_list_scrollbar_width_card">68dp</dimen>
+
+ <!-- Dividers. -->
+ <dimen name="template_divider_thickness">1dp</dimen>
+
+ <!-- Action buttons and FABs. -->
+ <!-- Vertical margin in action button. -->
+ <dimen name="template_action_button_vertical_margin">@dimen/template_padding_2</dimen>
+
+ <!-- Action button list row vertical spacing. -->
+ <dimen name="template_action_button_list_row_vertical_spacing">@dimen/template_padding_2</dimen>
+
+ <!-- Min width common to buttons and FABs, when the action has text. -->
+ <dimen name="template_action_with_text_min_width">156dp</dimen>
+
+ <!-- The size of an icon inside of a FAB or button. -->
+ <dimen name="template_action_icon_size">@dimen/car_app_ui_button_image_size</dimen>
+ <dimen name="template_action_icon_size_min">0dp</dimen>
+ <!-- This max size should align with the max size we can set on the system toolbar. -->
+ <dimen name="template_action_icon_size_max">88dp</dimen>
+
+ <!-- The spacing between the start side of a FAB or button and the action icon.
+ The spacing is applied only when the button has both icon and text. -->
+ <dimen name="template_action_icon_text_start_spacing">@dimen/car_app_ui_icon_button_start_spacing</dimen>
+
+ <!-- The spacing between the end side of a FAB or button and the action icon.
+ The spacing is applied only when the button has both icon and text. -->
+ <dimen name="template_action_icon_text_end_spacing">@dimen/car_app_ui_icon_button_end_spacing</dimen>
+
+ <!-- The spacing between the icon and the text in a FAB or button. -->
+ <dimen name="template_action_icon_to_text_spacing">@dimen/car_app_ui_icon_button_image_to_text_spacing</dimen>
+
+ <!-- The spacing between the start and end sides of a FAB or button and the action text.
+ The spacing is applied only when the button only has the text. -->
+ <dimen name="template_action_text_horizontal_spacing">@dimen/car_app_ui_button_text_horizontal_spacing</dimen>
+
+ <!-- The width of the border for a secondary button, e.g. a button rendered
+ in the action strip in a full screen template. -->
+ <dimen name="template_action_button_secondary_border_width">2dp</dimen>
+
+ <!-- The padding around the action strip. -->
+ <dimen name="template_action_strip_padding">@dimen/car_ui_padding_3</dimen>
+
+ <!-- The vertical margin of the sticky buttons. -->
+ <dimen name="template_sticky_buttons_vertical_spacing">@dimen/car_ui_padding_2</dimen>
+
+ <!-- Toggles and radio buttons. -->
+ <dimen name="template_toggle_width">44dp</dimen>
+ <dimen name="template_toggle_height">24dp</dimen>
+ <dimen name="template_radio_button_size">24dp</dimen>
+
+ <!-- Focus. -->
+ <dimen name="template_focus_ring_stroke_width_hovered">@dimen/default_focus_ring_stroke_width_hovered</dimen>
+ <dimen name="template_focus_ring_stroke_width_focused">@dimen/default_focus_ring_stroke_width_focused</dimen>
+ <dimen name="template_back_focus_ring_inset">4dp</dimen>
+ <dimen name="template_search_focus_ring_inset">6dp</dimen>
+ <dimen name="template_action_fab_focus_ring_size">56dp</dimen>
+ <dimen name="template_focus_oval_ripple_inset">4dp</dimen>
+
+ <!-- Routing. -->
+ <dimen name="template_routing_image_span_body2_max_height">@dimen/default_body2_line_height</dimen>
+ <dimen name="template_routing_image_span_body3_max_height">@dimen/default_body3_line_height</dimen>
+ <dimen name="template_nav_card_large_image_size">@dimen/car_app_ui_nav_card_large_image_size</dimen>
+ <dimen name="template_nav_card_large_image_size_min">0dp</dimen>
+ <dimen name="template_nav_card_large_image_size_max">128dp</dimen>
+ <dimen name="template_nav_card_small_image_size">@dimen/car_app_ui_nav_card_small_image_size</dimen>
+ <dimen name="template_nav_card_small_image_size_min">0dp</dimen>
+ <dimen name="template_nav_card_small_image_size_max">72dp</dimen>
+ <dimen name="template_routing_lanes_image_container_height">55dp</dimen>
+
+ <dimen name="template_nav_card_padding_vertical">@dimen/car_app_ui_nav_card_padding_vertical</dimen>
+ <dimen name="template_nav_card_padding_horizontal">@dimen/car_app_ui_nav_card_padding_horizontal</dimen>
+ <dimen name="template_nav_card_small_padding_vertical">@dimen/car_app_ui_nav_card_small_padding_vertical</dimen>
+ <dimen name="template_steps_card_image_to_text_spacing_horizontal">@dimen/car_app_ui_nav_card_image_to_text_spacing_horizontal</dimen>
+ <dimen name="template_steps_card_image_to_text_spacing_vertical">@dimen/car_app_ui_nav_card_image_to_text_spacing_vertical</dimen>
+
+ <dimen name="template_steps_card_content_container_min_width">320dp</dimen>
+ <dimen name="template_steps_card_content_container_max_width">372dp</dimen>
+ <dimen name="template_steps_card_content_container_oem_max_width">500dp</dimen>
+ <dimen name="template_steps_card_content_container_min_height">100dp</dimen>
+
+ <!-- Message template. -->
+ <dimen name="template_message_title_top_spacing">@dimen/car_app_ui_image_to_text_spacing_vertical</dimen>
+ <dimen name="template_message_buttons_top_spacing">@dimen/car_app_ui_text_to_control_spacing_vertical</dimen>
+ <dimen name="template_message_debug_text_size">14dp</dimen>
+ <dimen name="template_message_debug_text_line_height">18dp</dimen>
+
+ <!-- Search template. -->
+ <dimen name="template_search_bar_max_width">580dp</dimen>
+
+ <!-- Status bar. -->
+ <dimen name="template_status_bar_minimum_top_padding">0dp</dimen>
+
+ <!-- No content view. -->
+ <dimen name="template_no_content_view_focus_corner_radius">@dimen/default_no_content_focus_background_corner_radius</dimen>
+
+ <!-- Use a negative padding value to draw the focus ring outside the no content view. -->
+ <dimen name="template_no_content_view_focus_ring_padding">-8dp</dimen>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml
new file mode 100644
index 0000000..d462eba
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/dimens_default.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Standard dimensions for all template widgets.
+
+ These dimensions can be referenced directly from widgets without going
+ through a theme attribute.
+
+ See go/watevra-visd. -->
+<resources>
+
+ <dimen name="default_padding_0">4dp</dimen>
+ <dimen name="default_padding_1">8dp</dimen>
+ <dimen name="default_padding_2">12dp</dimen>
+ <dimen name="default_padding_3">16dp</dimen>
+ <dimen name="default_padding_4">24dp</dimen>
+ <dimen name="default_padding_5">32dp</dimen>
+ <dimen name="default_padding_6">48dp</dimen>
+ <dimen name="default_padding_7">64dp</dimen>
+ <dimen name="default_padding_8">96dp</dimen>
+
+ <dimen name="default_height_keyline_0">0dp</dimen>
+ <dimen name="default_height_keyline_1">8dp</dimen>
+ <dimen name="default_height_keyline_2">12dp</dimen>
+ <dimen name="default_height_keyline_3">16dp</dimen>
+ <dimen name="default_width_keyline_0">4dp</dimen>
+ <dimen name="default_width_keyline_1">8dp</dimen>
+ <dimen name="default_width_keyline_2">8dp</dimen>
+ <dimen name="default_width_keyline_3">16dp</dimen>
+
+ <dimen name="default_touch_target_minimum_size">68dp</dimen>
+ <dimen name="default_edge_column_width">@dimen/default_touch_target_minimum_size</dimen>
+ <dimen name="default_edge_column_margin">@dimen/default_width_keyline_1</dimen>
+ <dimen name="default_edge_column_width_padded">84dp</dimen>
+
+ <dimen name="default_body1_text_size">32dp</dimen>
+ <dimen name="default_body1_line_height">40dp</dimen>
+ <item name="default_body1_letter_spacing" format="float" type="dimen">0.0094</item>
+
+ <dimen name="default_body2_text_size">28dp</dimen>
+ <dimen name="default_body2_line_height">36dp</dimen>
+ <item name="default_body2_letter_spacing" format="float" type="dimen">0.0107</item>
+
+ <dimen name="default_body3_text_size">24dp</dimen>
+ <dimen name="default_body3_line_height">32dp</dimen>
+ <item name="default_body3_letter_spacing" format="float" type="dimen">0.0250</item>
+
+ <dimen name="default_display1_text_size">56dp</dimen>
+ <dimen name="default_display1_line_height">64dp</dimen>
+ <item name="default_display1_letter_spacing" format="float" type="dimen">0.0000</item>
+
+ <dimen name="default_display2_text_size">44dp</dimen>
+ <dimen name="default_display2_line_height">52dp</dimen>
+ <item name="default_display2_letter_spacing" format="float" type="dimen">0.0023</item>
+
+ <dimen name="default_display3_text_size">36dp</dimen>
+ <dimen name="default_display3_line_height">44dp</dimen>
+ <item name="default_display3_letter_spacing" format="float" type="dimen">0.0055</item>
+
+ <!-- PagedListView -->
+ <dimen name="default_paged_list_view_max_content_width">704dp</dimen>
+
+ <!-- Focus -->
+ <dimen name="default_focus_ring_stroke_width_hovered">4dp</dimen>
+ <dimen name="default_focus_ring_stroke_width_focused">6dp</dimen>
+
+ <!-- Apps Space -->
+ <dimen name="default_canvas_max_width">794dp</dimen>
+ <dimen name="default_list_item_corner_radius">8dp</dimen>
+ <dimen name="default_no_content_focus_background_corner_radius">16dp</dimen>
+
+ <!-- Edit Text -->
+ <!-- Use a negative padding value to draw the focus ring outside the edit text. -->
+ <dimen name="default_edit_text_focus_ring_padding">-6dp</dimen>
+ <dimen name="default_edit_text_focus_ring_radius">12dp</dimen>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml
new file mode 100644
index 0000000..970dd93
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/floats.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- GridItems. -->
+ <!-- Alpha value for the secondary text line in a grid item. -->
+ <item name="template_grid_item_text_alpha" format="float" type="dimen">0.72</item>
+
+ <!-- Navigation routing template. -->
+ <!-- Ratio of the image span (width/height) that should be used alongside text. -->
+ <item name="template_routing_image_span_ratio" format="float" type="dimen">3.0</item>
+
+ <!-- Row list template. -->
+ <!-- Aspect ratio of the PaneTemplate large image. -->
+ <item name="template_row_list_large_image_aspect_ratio" format="float" type="dimen">1.75</item>
+
+ <!-- Width ratio of the large image relative to the row list width -->
+ <item name="template_row_list_to_large_image_ratio" format="float" type="dimen">0.33</item>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml
new file mode 100644
index 0000000..10a9467
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/integers.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <integer name="template_grid_items_per_row">2</integer>
+
+ <!-- Actions. -->
+ <!--
+ Max ems to display for the button text if there is no icon.
+ Actual spec is 11ems, but TextView inserts the ellipsize after the maxems
+ value, so here the value is 11-1 to account for the ellipsize.
+ -->
+ <integer name="template_action_button_text_max_ems_no_icon">10</integer>
+
+ <!--
+ Max ems to display for the button text if there is an icon.
+ Actual spec is 10ems, but TextView inserts the ellipsize after the maxems
+ value, so here the value is 10-1 to account for the ellipsize.
+ -->
+ <integer name="template_action_button_text_max_ems_with_icon">9</integer>
+
+ <!--
+ Max ems to display for the fab text if there is no icon.
+ Actual spec is 7ems, but TextView inserts the ellipsize after the maxems
+ value, so here the value is 7-1 to account for the ellipsize.
+ -->
+ <integer name="template_fab_text_max_ems_no_icon">6</integer>
+
+ <!--
+ Max ems to display for the fab text if there is an icon.
+ Actual spec is 6ems, but TextView inserts the ellipsize after the maxems
+ value, so here the value is 6-1 to account for the ellipsize.
+ -->
+ <integer name="template_fab_text_max_ems_with_icon">5</integer>
+
+ <!--
+ Max ems to display for the provider sign-in button text.
+ TODO(b/184195457): remove the custom maxEms limit when we remove the limit for all
+ -->
+ <integer name="template_provider_sign_in_button_text_max_ems">100</integer>
+
+ <!-- Duration for the animation of templates switching. -->
+ <integer name="template_update_animation_duration_millis">300</integer>
+
+ <!-- Layout gravity for content areas (e.g content vertical alignment in Sign In Template
+ content).-->
+ <integer name="template_plain_content_layout_gravity">@integer/car_app_ui_content_layout_gravity</integer>
+
+ <!-- Content gravity for content areas (e.g. component horizontal alignment in Sign In
+ Template content). -->
+ <integer name="template_plain_content_gravity">@integer/car_app_ui_content_gravity</integer>
+
+ <!-- Gravity integer values (to be used as part of gravity overlayable attributes. -->
+ <integer name="gravity_bottom">80</integer>
+ <integer name="gravity_center">17</integer>
+ <integer name="gravity_center_horizontal">1</integer>
+ <integer name="gravity_center_vertical">16</integer>
+ <integer name="gravity_end">8388613</integer>
+ <integer name="gravity_left">3</integer>
+ <integer name="gravity_no_gravity">0</integer>
+ <integer name="gravity_right">5</integer>
+ <integer name="gravity_start">8388611</integer>
+ <integer name="gravity_top">48</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml
new file mode 100644
index 0000000..0779b34
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles.xml
@@ -0,0 +1,391 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Style definitions for the UI in the templates.
+
+ !!! IMPORTANT !!!
+ Do not refer to these styles directly from views. Styles must be referred
+ to through theme attributes (in attrs.xml). -->
+ <style name="Template" parent="Theme.Template"/>
+
+ <!-- The appearance of the markers in the map view. -->
+ <style name="TextAppearance.Marker" parent="TextAppearance.Template.Body3.Medium"/>
+
+ <style name="MarkerAppearance" parent="TextAppearance.Marker">
+ <item name="markerDefaultBackgroundColor">@color/template_marker_default_background_color</item>
+ <item name="markerCustomBackgroundContentColor">@color/template_marker_custom_background_content_color</item>
+ <item name="markerDefaultBorderColor">@color/template_marker_default_border_color</item>
+ <item name="markerCustomBorderColor">@color/template_marker_custom_border_color</item>
+ <item name="markerPointerWidth">@dimen/template_marker_pointer_width</item>
+ <item name="markerPointerHeight">@dimen/template_marker_pointer_height</item>
+ <item name="markerStroke">@dimen/template_marker_stroke</item>
+ <item name="markerCornerRadius">@dimen/template_marker_corner_radius</item>
+ <item name="markerPadding">@dimen/template_marker_padding</item>
+
+ <item name="anchorDefaultBackgroundColor">@color/template_anchor_default_background_color</item>
+ <item name="anchorBorderColor">@color/template_anchor_border_color</item>
+ <item name="anchorDotColor">@color/template_anchor_dot_color</item>
+
+ <item name="markerTextHorizontalPadding">@dimen/template_marker_text_horizontal_padding</item>
+ <item name="markerIconSize">@dimen/template_marker_icon_size</item>
+ <item name="markerImageSize">@dimen/template_marker_image_size</item>
+ <item name="markerImageCornerRadius">@dimen/template_marker_image_corner_radius</item>
+
+ <item name="markerListIconSize">?templateHalfRowImageSize</item>
+ </style>
+
+ <style name="MarkerAppearance.Template.Map" parent="MarkerAppearance">
+ <item name="markerDefaultContentColor">@color/template_marker_map_default_content_color</item>
+ </style>
+
+ <style name="MarkerAppearance.Template.List" parent="MarkerAppearance">
+ <item name="markerDefaultContentColor">@color/template_marker_list_default_content_color</item>
+ </style>
+
+ <!-- The style of an action button. -->
+ <style name="Widget.Template.ActionButton">
+ <item name="android:foreground">?templateActionButtonForeground</item>
+ <item name="android:background">?templateActionButtonBackground</item>
+ </style>
+
+ <!-- The style of a FAB. -->
+ <style name="Widget.Template.Fab">
+ <item name="android:clickable">true</item>
+ <item name="android:focusable">true</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center</item>
+ <item name="android:elevation">6dp</item>
+ </style>
+
+ <!-- The appearance of a FAB when actually displayed as floating, e.g.
+ over a map surface. -->
+ <style name="FabAppearance.Template.Fab" parent="Widget.Template.Fab">
+ <item name="android:background">@drawable/action_strip_fab_view_background</item>
+ <item name="android:foreground">@drawable/action_strip_button_view_focus_ring</item>
+ <item name="fabDefaultContentColor">@color/template_action_strip_fab_content_color</item>
+ </style>
+
+ <!-- The appearance of a FAB when displayed in a full screen template. -->
+ <style name="FabAppearance.Template.FullTemplate" parent="Widget.Template.Fab">
+ <item name="android:background">@drawable/action_strip_button_view_background</item>
+ <item name="android:foreground">@drawable/action_strip_button_view_focus_ring</item>
+ <item name="fabDefaultContentColor">@color/default_white</item>
+ </style>
+
+ <!-- The style of a loading spinner. -->
+ <style name="Widget.Template.Spinner">
+ <item name="android:indeterminateDrawable">@drawable/default_progress_spinner</item>
+ <item name="android:indeterminate">true</item>
+ </style>
+
+ <!-- The style of a content container. -->
+ <style name="Widget.Template.Container">
+ <item name="android:background">@drawable/car_ui_activity_background</item>
+ </style>
+
+ <!-- The style of a content container on a surface template. -->
+ <style name="Widget.Template.Container.Surface">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:paddingRight">@dimen/template_plain_content_container_padding</item>
+ </style>
+
+ <!-- The style of a plain content container. -->
+ <style name="Widget.Template.Container.Plain" />
+
+ <!-- The style of a card content container. -->
+ <style name="Widget.Template.Container.Card">
+ <item name="cardBackgroundColor">@color/template_card_background_color</item>
+ <item name="cardTextColor">@color/template_text_color_primary</item>
+ <item name="cardFallbackDarkBackgroundColor">@color/default_gray_928</item>
+ <item name="cardFallbackLightBackgroundColor">@color/template_white</item>
+ <item name="cardBorderColor">@color/template_card_content_container_border_color</item>
+ <item name="cardBorderWidth">@dimen/template_card_content_container_border_width</item>
+ <item name="cardRadius">?templateCornerRadius</item>
+ <item name="android:elevation">@dimen/template_card_content_container_elevation</item>
+ </style>
+
+ <!-- The style of a card content container with a content view (e.g. list). -->
+ <style name="Widget.Template.Container.Card.Content">
+ <item name="cardWidthFraction">@dimen/template_card_content_container_width_fraction</item>
+ <item name="cardMinWidth">@dimen/template_card_content_container_min_width</item>
+ <item name="cardMaxWidth">@dimen/template_card_content_container_max_width</item>
+ <item name="cardOemWidth">@dimen/car_app_ui_card_width</item>
+ <item name="cardOemMaxWidth">@dimen/template_card_content_container_oem_max_width</item>
+ </style>
+
+ <!-- The style of a card content container with the routing information. -->
+ <style name="Widget.Template.Container.Card.Content.Routing">
+ <item name="cardWidthFraction">@dimen/template_steps_card_content_container_width_fraction</item>
+ <item name="cardMinWidth">@dimen/template_steps_card_content_container_min_width</item>
+ <item name="cardMaxWidth">@dimen/template_steps_card_content_container_max_width</item>
+ <item name="cardOemWidth">@dimen/car_app_ui_nav_card_width</item>
+ <item name="cardOemMaxWidth">@dimen/template_steps_card_content_container_oem_max_width</item>
+ </style>
+
+ <!-- The style of a button in a content view. -->
+ <style name="Widget.Template.ContentButton">
+ <item name="android:scaleType">center</item>
+ <item name="android:tint">@color/template_content_button_color</item>
+ <item name="android:tintMode">src_atop</item>
+ </style>
+
+ <!-- The style of a list of rows, plain style. -->
+ <style name="Widget.Template.RowList">
+ <item name="listWidthFraction">@dimen/template_adaptive_width_fraction</item>
+ <item name="listMaxWidth">@dimen/template_row_list_max_width</item>
+ <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width</item>
+ <item name="listShowScrollbarDivider">false</item>
+ </style>
+
+ <!-- The style of a list of rows, card style. -->
+ <style name="Widget.Template.RowList.Card">
+ <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width_card</item>
+ <item name="listNoScrollBarStartPadding">@dimen/template_row_list_no_scrollbar_start_padding_card</item>
+ <!-- Card widths are not adaptive. -->
+ <item name="listWidthFraction">-1.0</item>
+ <item name="listShowScrollbarDivider">true</item>
+ </style>
+
+ <!-- The style of a grid, plain style. -->
+ <style name="Widget.Template.Grid">
+ <item name="listWidthFraction">@dimen/template_adaptive_width_fraction</item>
+ <item name="listMaxWidth">@dimen/template_grid_max_width</item>
+ <item name="listScrollbarWidth">@dimen/template_paged_list_scrollbar_width</item>
+ <item name="listShowScrollbarDivider">false</item>
+ </style>
+
+ <!-- The style of the title text in a grid item. -->
+ <style name="Widget.Template.Text.GridItemTitle">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.GridItemTitle</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <!-- The style of the secondary text line below the title in a grid item. -->
+ <style name="Widget.Template.Text.GridItemText">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.GridItemText</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <!-- The style of text that indicates a grid is empty -->
+ <style name="Widget.Template.GridEmpty" parent="Widget.CarAppUi.RowSecondary">
+ <item name="android:maxLines">2</item>
+ <item name="android:gravity">center</item>
+ <!-- TODO(b/174717763): overriding body3's letter spacing since it seems to cause
+ centered text to be incorrectly cropped -->
+ <item name="android:letterSpacing">0</item>
+ </style>
+
+ <style name="Widget.Template.Routing"/>
+
+ <!-- The style of the text showing the title of the routing card when in
+ arrived state. -->
+ <style name="Widget.Template.Routing.MessagePrimary">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardLargeText</item>
+ <item name="android:maxLines">2</item>
+ </style>
+
+ <!-- The style of the text showing the destination address in the routing
+ card when in arrived state. -->
+ <style name="Widget.Template.Routing.MessageSecondary">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:lineSpacingExtra">5sp</item>
+ </style>
+
+ <style name="Widget.Template.Routing.Distance">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardXLargeText</item>
+ </style>
+
+ <style name="Widget.Template.Routing.Description">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardMediumText</item>
+ <item name="android:maxLines">2</item>
+ </style>
+
+ <style name="Widget.Template.Routing.CompactDescription">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <style name="Widget.Template.Routing.TravelEstimate">
+ <item name="android:textAppearance">@style/TextAppearance.Template.NavCardSmallText</item>
+ <item name="android:maxLines">1</item>
+ </style>
+
+ <!-- Routing card textAppearance -->
+ <style name="TextAppearance.Template.NavCardSmallText" parent="TextAppearance.CarUi.Body3" />
+ <style name="TextAppearance.Template.NavCardMediumText" parent="TextAppearance.CarUi.Body2" />
+ <style name="TextAppearance.Template.NavCardLargeText" parent="TextAppearance.CarUi.Body1" >
+ <item name="android:textSize">?templateNavCardLargeTextSize</item>
+ </style>
+ <style name="TextAppearance.Template.NavCardXLargeText" parent="TextAppearance.CarUi.Body1">
+ <item name="android:textSize">?templateNavCardXLargeTextSize</item>
+ </style>
+
+ <!-- The style of the title text inside of the message template. -->
+ <style name="Widget.Template.Text.Message">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.TextBlock</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <!-- The style of the title text inside of the long message template. -->
+ <style name="Widget.Template.Text.LongMessage">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.TextBlock</item>
+ <item name="android:maxLines">2147483647</item>
+ </style>
+
+ <!-- The style of text inside of a debug info inside of the message template. -->
+ <style name="Widget.Template.Debug" parent="Widget.Template.Text.Body3">
+ <item name="android:maxLines">1024</item>
+ <item name="android:textColor">@color/template_message_debug_text_color</item>
+ <item name="android:fontFamily">monospace</item>
+ <item name="android:textSize">@dimen/template_message_debug_text_size</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/template_message_debug_text_line_height</item>
+ </style>
+
+ <style name="Widget.Template.SignIn"/>
+
+ <!-- The style of the container of sign-in content. -->
+ <style name="Widget.Template.SignIn.Container">
+ <item name="android:gravity">?templatePlainContentGravity</item>
+ <item name="android:layout_gravity">?templatePlainContentLayoutGravity</item>
+ <item name="android:layout_marginHorizontal">?templatePlainContentHorizontalPadding</item>
+ </style>
+
+ <!-- The style of the instruction text inside the sign-in template. -->
+ <style name="Widget.Template.SignIn.InstructionText">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.SignInHeader</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <!-- The style of a provider sign-in button. -->
+ <style name="Widget.Template.SignIn.ProviderSignInbutton" parent="Widget.Template.ActionButton">
+ <item name="textMaxEms">@integer/template_provider_sign_in_button_text_max_ems</item>
+ </style>
+
+ <!-- The style of the PIN text inside the sign-in template. -->
+ <style name="Widget.Template.SignIn.PinText">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.ReadOnlyText</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:gravity">center</item>
+ <item name="android:letterSpacing">@dimen/template_sign_in_pin_text_letter_spacing</item>
+ </style>
+
+ <!-- The style of the additional text inside the sign-in template. -->
+ <style name="Widget.Template.SignIn.AdditionalText">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.SignInLegal</item>
+ <item name="android:maxLines">3</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">@color/template_sign_in_additional_text_color</item>
+ <item name="android:textColorLink">?templateHyperlinkTextColor</item>
+ <item name="android:linksClickable">true</item>
+ </style>
+
+ <!-- The style of the error message inside the sign-in template. -->
+ <style name="Widget.Template.SignIn.ErrorMessage" parent="Widget.Template.Text.Body3">
+ <item name="android:maxLines">1</item>
+ <item name="android:gravity">start</item>
+ <item name="android:textColor">@color/template_sign_in_error_message_color</item>
+ <item name="android:textSize">@dimen/template_sign_in_input_error_message_text_size</item>
+ <item name="android:layout_marginStart">?templateEditTextErrorHorizontalSpacing</item>
+ <item name="android:layout_marginTop">?templateEditTextErrorVerticalSpacing</item>
+ </style>
+
+ <!-- The style of the text in an action button. -->
+ <style name="Widget.Template.Text.ActionButton">
+ <item name="android:textAppearance">@style/TextAppearance.CarAppUi.ButtonText</item>
+ <item name="android:textColor">@color/car_app_ui_action_button_text_color</item>
+ </style>
+
+ <!-- Standard styles for all template widgets.
+
+ These styles can be referenced directly from widgets without using a
+ theme attribute.
+
+ See go/watevra-visd. -->
+ <style name="TextAppearance.Template" parent="TextAppearance.Design"/>
+
+ <!-- Styles for display text, meant for larger text like titles and such. -->
+ <style name="TextAppearance.Template.Display1" parent="TextAppearance.Design.Display1"/>
+ <style name="TextAppearance.Template.Display2" parent="TextAppearance.Design.Display2"/>
+ <style name="TextAppearance.Template.Display2.Medium" parent="TextAppearance.Design.Display2.Medium"/>
+ <style name="TextAppearance.Template.Display3" parent="TextAppearance.Design.Display3"/>
+
+ <!-- Styles for body text, meant for smaller text like list row contents. -->
+ <style name="TextAppearance.Template.Body1" parent="TextAppearance.Design.Body1"/>
+ <style name="TextAppearance.Template.Body2" parent="TextAppearance.Design.Body2"/>
+ <style name="TextAppearance.Template.Body3" parent="TextAppearance.Design.Body3"/>
+ <style name="TextAppearance.Template.Body3.Medium" parent="TextAppearance.Design.Body3.Medium"/>
+
+ <style name="Widget"/>
+ <style name="Widget.Template"/>
+
+ <!-- Base style for text widgets. -->
+ <style name="Widget.Template.Text">
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <!-- Styles for text widgets. There's one per typographic style as declare in
+ the specification:
+ https://designguidelines.withgoogle.com/automotive-os-apps/visual-foundations/typography.html#typography-scale-grid-references
+
+ Note certain paragraph-level attributes such as `lineHeight` can't be
+ folded into the `TextAppearance` hence why we need to declare these as
+ part of a style. -->
+ <style name="Widget.Template.Text.Display1">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Display1</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display1_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_display1_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Display2">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Display2</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display2_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_display2_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Display3">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Display3</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_display3_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_display3_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Body1">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Body1</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body1_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_body1_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Body2">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Body2</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body2_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_body2_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Body3">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Body3</item>
+ <item name="android:lineHeight" tools:ignore="NewApi">@dimen/default_body3_line_height</item>
+ <item name="android:letterSpacing">@dimen/default_body3_letter_spacing</item>
+ </style>
+
+ <style name="Widget.Template.Text.Body3.Medium">
+ <item name="android:textAppearance">@style/TextAppearance.Template.Body3.Medium</item>
+ </style>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml
new file mode 100644
index 0000000..3595ae0
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/styles_default.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!-- Typography spec:
+ https://designguidelines.withgoogle.com/automotive-os-apps/visual-foundations/typography.html#typography-scale-grid-references -->
+ <style name="TextAppearance.Design" parent="TextAppearance.CarUi">
+ <item name="android:textStyle">normal</item>
+ <item name="android:textColor">@color/default_text_color</item>
+ </style>
+
+ <style name="TextAppearance.Design.Body1" parent="TextAppearance.CarUi.Body1"/>
+
+ <style name="TextAppearance.Design.Body2" parent="TextAppearance.CarUi.Body2"/>
+
+ <style name="TextAppearance.Design.Body3" parent="TextAppearance.CarUi.Body3"/>
+
+ <style name="TextAppearance.Design.Body3.Medium" parent="TextAppearance.Design.Body3">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="TextAppearance.Design.Display1" parent="TextAppearance.CarUi.Body1">
+ <item name="android:textSize">@dimen/default_display1_text_size</item>
+ </style>
+
+ <style name="TextAppearance.Design.Display2" parent="TextAppearance.CarUi.Body1">
+ <item name="android:textSize">@dimen/default_display2_text_size</item>
+ </style>
+
+ <style name="TextAppearance.Design.Display2.Medium" parent="TextAppearance.Design.Display2">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <style name="TextAppearance.Design.Display3" parent="TextAppearance.CarUi.Body1">
+ <item name="android:textSize">@dimen/default_display3_text_size</item>
+ </style>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml
new file mode 100644
index 0000000..16ae280
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/res/values/themes.xml
@@ -0,0 +1,344 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- The template app theme.
+
+ <p>This theme must be set on the root template view which is the parent of all the
+ template layouts and their child views.
+
+ <p>Styles used by this theme are named using the convention Type.Template.Etc
+ (for example {@code TextAppearance.Template.RowTitle}).-->
+
+ <style name="Theme.Template" parent="Theme.CarUi">
+
+ <!-- Common. -->
+ <item name="templateAdaptiveWidthFraction">@dimen/template_adaptive_width_fraction</item>
+ <!-- Margin between buttons placed side by side. -->
+ <item name="templateControlToControlSpacingHorizontal">@dimen/car_app_ui_control_to_control_spacing_horizontal</item>
+ <!-- Button height. -->
+ <item name="templateButtonHeight">@dimen/car_app_ui_button_height</item>
+ <!-- Corner radius used across the UI except for the buttons. -->
+ <item name="templateCornerRadius">@dimen/car_app_ui_corner_radius</item>
+ <!-- Corner radius used for the buttons. -->
+ <item name="templateButtonCornerRadius">@dimen/car_app_ui_button_corner_radius</item>
+
+ <!-- Standard colors. -->
+ <item name="templateStandardBlue">@color/car_app_ui_standard_blue</item>
+ <item name="templateStandardRed">@color/car_app_ui_standard_red</item>
+ <item name="templateStandardGreen">@color/car_app_ui_standard_green</item>
+ <item name="templateStandardYellow">@color/car_app_ui_standard_yellow</item>
+
+ <!-- Images. -->
+ <item name="templateLargeImageSize">@dimen/template_large_image_size</item>
+ <item name="templateLargeImageSizeMin">@dimen/template_large_image_size_min</item>
+ <item name="templateLargeImageSizeMax">@dimen/template_large_image_size_max</item>
+
+ <!-- Sign In Template. -->
+ <item name="templateSignInContainerStyle">@style/Widget.Template.SignIn.Container</item>
+ <item name="templateSignInMethodViewMaxWidth">@dimen/template_sign_in_method_view_max_width</item>
+ <item name="templateSignInInstructionTextStyle">@style/Widget.Template.SignIn.InstructionText</item>
+ <item name="templateSignInProviderSignInButtonStyle">@style/Widget.Template.SignIn.ProviderSignInbutton</item>
+ <item name="templateSignInPinTextStyle">@style/Widget.Template.SignIn.PinText</item>
+ <item name="templateSignInPinBackgroundColor">@color/template_read_only_text_background_color</item>
+ <item name="templateSignInPinCornerRadius">?templateCornerRadius</item>
+ <item name="templateSignInPinBackground">@drawable/pin_sign_in_view_background</item>
+ <item name="templateSignInPinPadding">@dimen/template_read_only_text_padding</item>
+ <item name="templateSignInQRCodeImageWidth">@dimen/template_sign_in_qr_code_image_width</item>
+ <item name="templateSignInAdditionalTextStyle">@style/Widget.Template.SignIn.AdditionalText</item>
+ <item name="templateSignInErrorMessageStyle">@style/Widget.Template.SignIn.ErrorMessage</item>
+ <item name="templateSignInInputViewStyle">@style/Widget.CarAppUi.InputView</item>
+
+ <!-- Hyperlink Text -->
+ <item name="templateHyperlinkTextColor">@color/car_app_ui_hyperlink_text_color</item>
+
+ <!-- Map markers. -->
+ <item name="templateMapMarkerAppearance">@style/MarkerAppearance.Template.Map</item>
+ <item name="templateListMarkerAppearance">@style/MarkerAppearance.Template.List</item>
+
+ <!-- Loading spinners. -->
+ <item name="templateLoadingSpinnerStyle">@style/Widget.Template.Spinner</item>
+ <item name="templateLoadingSpinnerColor">@color/template_loading_spinner_color</item>
+
+ <!-- Plain content containers. -->
+ <item name="templatePlainContentContainerStyle">@style/Widget.Template.Container.Plain</item>
+ <item name="templatePlainContentContainerWidth">@dimen/template_plain_content_container_width</item>
+ <item name="templatePlainContentLayoutGravity">@integer/template_plain_content_layout_gravity</item>
+ <item name="templatePlainContentGravity">@integer/template_plain_content_gravity</item>
+ <item name="templatePlainContentHorizontalPadding">@dimen/template_plain_content_horizontal_padding</item>
+ <item name="templatePlainContentBackgroundColor">@color/template_plain_content_background_color</item>
+
+ <!-- Card content containers. -->
+ <item name="templateCardContentContainerStyle">@style/Widget.Template.Container.Card.Content</item>
+ <item name="templateCardRoutingContentContainerStyle">@style/Widget.Template.Container.Card.Content.Routing</item>
+ <item name="templateCardContentContainerDefaultWidth">@dimen/template_card_content_container_default_width</item>
+ <item name="templateCardContentContainerStartMargin">@dimen/template_card_content_container_start_margin</item>
+ <item name="templateCardContentContainerTopMargin">@dimen/template_card_content_container_top_margin</item>
+ <item name="templateCardContentContainerBottomMargin">@dimen/template_card_content_container_bottom_margin</item>
+ <item name="templateCardContentContainerMinHeight">@dimen/template_card_content_container_min_height</item>
+
+ <!-- Content headers. -->
+ <item name="templateHeaderHeight">@dimen/template_header_height</item>
+ <item name="templateHeaderTextVerticalSpacing">@dimen/template_header_text_vertical_spacing</item>
+ <item name="templateHeaderTextStartSpacing">@dimen/template_header_text_horizontal_spacing</item>
+ <item name="templateHeaderTextEndSpacing">@dimen/template_header_text_horizontal_spacing</item>
+ <item name="templateHeaderTextStyle">@style/TextAppearance.CarAppUi.CardHeader</item>
+ <item name="templateHeaderTextNoIconStartSpacing">@dimen/template_header_text_no_icon_start_spacing</item>
+ <item name="templateHeaderButtonIconSize">@dimen/template_header_button_icon_size</item>
+ <item name="templateHeaderButtonIconTint">@color/template_icon_tint_color</item>
+ <item name="templateHeaderButtonContainerSize">@dimen/template_button_touch_target_size</item>
+ <item name="templateHeaderButtonStartSpacing">@dimen/template_header_button_start_spacing</item>
+ <item name="templateHeaderButtonBackground">@drawable/template_header_button_background</item>
+ <item name="templateHeaderBackgroundColor">@color/template_container_background</item>
+
+ <!-- Dividers. -->
+ <item name="templateDividerThickness">@dimen/template_divider_thickness</item>
+ <item name="templateDividerColor">@color/default_gradient_white_24</item>
+
+ <!-- Rows. -->
+ <item name="templateRippleColor">@color/template_ripple_color</item>
+ <item name="templateRippleSelectorColor">@color/template_ripple_selector_color</item>
+ <item name="templateRowBackgroundColor">@color/car_app_ui_row_background_color</item>
+ <item name="templateRowSelectedBackgroundColor">@color/default_gray_878</item>
+ <item name="templateRowImagePlaceholderColor">@color/default_gray_928</item>
+ <item name="templateRowBackgroundSimple">@drawable/row_background_simple</item>
+ <item name="templateRowBackgroundSectionalMiddle">@drawable/row_background_sectional_middle</item>
+ <item name="templateRowBackgroundSectionalTop">@drawable/row_background_sectional_top</item>
+ <item name="templateRowBackgroundSectionalBottom">@drawable/row_background_sectional_bottom</item>
+ <item name="templateRowBackgroundSectionalTopBottom">@drawable/row_background_sectional_top_bottom</item>
+ <item name="templateRowBackgroundSectionalBottomMargin">@dimen/template_row_background_sectional_bottom_margin</item>
+ <item name="templateRowImagePlaceholder">@drawable/row_image_placeholder</item>
+ <item name="templateRowMinHeight">@dimen/template_row_min_height</item>
+ <item name="templateRowIconSize">@dimen/template_row_icon_size</item>
+ <item name="templateRowImageSizeSmall">@dimen/template_row_image_size_small</item>
+ <item name="templateRowImageSizeLarge">@dimen/template_row_image_size_large</item>
+ <item name="templateRowMarkerMinSize">@dimen/template_row_marker_min_size</item>
+ <item name="templateRowMarkerLabelMargin">@dimen/template_row_marker_label_margin</item>
+ <item name="templateRowDefaultIconTint">@color/template_icon_tint_color</item>
+ <item name="templateRowHorizontalPadding">@dimen/template_row_horizontal_padding</item>
+ <item name="templateRowHorizontalHalfPadding">@dimen/template_row_horizontal_half_padding</item>
+ <item name="templateRowTextHorizontalPadding">@dimen/template_row_text_horizontal_padding</item>
+ <item name="templateRowTextHorizontalHalfPadding">@dimen/template_row_text_horizontal_half_padding</item>
+ <item name="templateHalfListBottomPadding">@dimen/template_half_list_bottom_padding</item>
+ <item name="templateHalfListPaddingVertical">@dimen/template_half_row_padding_vertical</item>
+
+ <!-- Half rows. -->
+ <item name="templateHalfRowMinHeight">@dimen/car_app_ui_half_row_min_height</item>
+ <item name="templateHalfRowHorizontalPadding">@dimen/template_half_row_horizontal_padding</item>
+ <item name="templateHalfRowVerticalPadding">@dimen/template_half_row_vertical_padding</item>
+ <item name="templateHalfRowImageToTextSpacing">@dimen/template_half_row_image_to_text_margin</item>
+ <item name="templateHalfRowTextToTextSpacing">@dimen/template_half_row_text_to_text_margin</item>
+ <item name="templateHalfRowImageSize">@dimen/template_half_row_image_size</item>
+
+ <!-- Full rows. -->
+ <item name="templateFullRowStartPadding">@dimen/template_full_row_start_padding</item>
+ <item name="templateFullRowEndPadding">@dimen/template_full_row_end_padding</item>
+ <item name="templateFullRowChevronIcon">@drawable/car_ui_preference_icon_chevron</item>
+ <item name="templateHalfRowChevronIcon">@drawable/car_ui_preference_icon_chevron</item>
+ <item name="templateFullRowChevronHeight">@dimen/template_full_row_chevron_size</item>
+ <item name="templateFullRowChevronWidth">@dimen/template_full_row_chevron_size</item>
+
+ <!-- Note, these containers are sized to match the height of the title of the row
+ so that they appear vertically aligned at the center when both are top aligned. -->
+ <item name="templateRowSelectionContainerHeight">@dimen/default_body2_line_height</item>
+
+ <!-- Text styles for list elements. -->
+ <item name="templateRowTitleStyle">@style/Widget.CarAppUi.RowTitle</item>
+ <item name="templateRowSecondaryTextStyle">@style/Widget.CarAppUi.RowSecondary</item>
+ <item name="templateRowSectionHeaderStyle">@style/Widget.CarAppUi.RowSectionHeader</item>
+ <item name="templateRowListEmptyTextStyle">@style/Widget.CarAppUi.RowListEmpty</item>
+
+ <!-- The image dimensions (for PaneTemplate) in the row list template. -->
+ <item name="templateRowListToLargeImageRatio">@dimen/template_row_list_to_large_image_ratio</item>
+ <item name="templateRowListLargeImageContainerMaxWidth">@dimen/template_row_list_large_image_container_max_width</item>
+ <item name="templateRowListLargeImageAspectRatio">@dimen/template_row_list_large_image_aspect_ratio</item>
+
+ <!-- Padding between the (PaneTemplate) image and row list -->
+ <item name="templateRowListAndImagePadding">@dimen/default_width_keyline_1</item>
+
+ <!-- Grids. -->
+ <item name="templateGridStyle">@style/Widget.Template.Grid</item>
+
+ <!-- Grid items. -->
+ <item name="templateGridItemImageBottomPadding">@dimen/template_grid_item_image_bottom_padding</item>
+ <item name="templateGridItemDefaultIconTint">@color/template_icon_tint_color</item>
+ <item name="templateGridItemTextContainerMaxWidth">@dimen/template_grid_item_text_container_max_width</item>
+ <item name="templateGridItemTextBottomPadding">@dimen/template_grid_item_text_bottom_padding</item>
+ <item name="templateGridItemHorizontalSpacing">@dimen/template_grid_item_horizontal_spacing</item>
+ <item name="templateGridItemVerticalSpacing">@dimen/template_grid_item_vertical_spacing</item>
+ <item name="templateGridItemsPerRow">@integer/template_grid_items_per_row</item>
+ <item name="templateGridEmptyTextStyle">@style/Widget.Template.GridEmpty</item>
+ <item name="templateGridItemBackground">@drawable/template_grid_item_background</item>
+ <item name="templateGridItemBackgroundColor">@color/car_app_ui_grid_item_background_color</item>
+
+ <!-- Text styles for grid items. -->
+ <item name="templateGridItemTitleStyle">@style/Widget.Template.Text.GridItemTitle</item>
+ <item name="templateGridItemTextStyle">@style/Widget.Template.Text.GridItemText</item>
+
+ <!-- Action buttons and FABs. -->
+ <item name="templateActionButtonMargin">?templateControlToControlSpacingHorizontal</item>
+ <item name="templateActionButtonStyle">@style/Widget.Template.ActionButton</item>
+ <item name="templateActionButtonTextStyle">@style/Widget.Template.Text.ActionButton</item>
+ <item name="templateActionButtonForeground">@drawable/action_button_focus_ring</item>
+ <item name="templateActionButtonDefaultBackgroundColor">@color/car_app_ui_action_button_default_background_color</item>
+ <item name="templateActionButtonPrimaryBackgroundColor">@color/car_app_ui_action_button_primary_background_color</item>
+ <item name="templateActionButtonBackground">@drawable/car_app_ui_action_button_background</item>
+ <item name="templateActionButtonHeight">?templateButtonHeight</item>
+ <item name="templateActionButtonTouchTargetSize">@dimen/template_button_touch_target_size</item>
+ <item name="templateActionButtonListButtonStretchHorizontal">@bool/car_app_ui_action_button_list_button_stretch_horizontal</item>
+ <item name="templateActionButtonListGravity">@integer/car_app_ui_action_button_list_gravity</item>
+ <item name="templateActionButtonListButtonContentAlignment">@integer/car_app_ui_action_button_list_button_content_alignment</item>
+ <item name="templateActionButtonListButtonMaxWidth">@dimen/car_app_ui_action_button_list_button_max_width</item>
+ <item name="templateActionButtonSideAlignmentSpacing">@dimen/car_app_ui_button_side_alignment_spacing</item>
+ <item name="templateActionButtonListRowVerticalSpacing">@dimen/template_action_button_list_row_vertical_spacing</item>
+ <item name="templateActionIconSize">@dimen/template_action_icon_size</item>
+ <item name="templateActionIconSizeMin">@dimen/template_action_icon_size_min</item>
+ <item name="templateActionIconSizeMax">@dimen/template_action_icon_size_max</item>
+ <item name="templateActionIconTextStartSpacing">@dimen/template_action_icon_text_start_spacing</item>
+ <item name="templateActionIconTextEndSpacing">@dimen/template_action_icon_text_end_spacing</item>
+ <item name="templateActionIconToTextSpacing">@dimen/template_action_icon_to_text_spacing</item>
+ <item name="templateActionTextHorizontalSpacing">@dimen/template_action_text_horizontal_spacing</item>
+ <item name="templateActionWithTextMinWidth">@dimen/template_action_with_text_min_width</item>
+ <item name="templateActionButtonUseOemColors">@bool/car_app_ui_is_action_color_overridden</item>
+ <item name="templateActionButtonPrimaryHorizontalOrder">@integer/car_app_ui_action_button_primary_horizontal_order</item>
+ <!-- Min width common to buttons and FABs. It is the same as the height of
+ the action button so that the button appears with 1:1 aspect ratio when it has just
+ an icon inside. -->
+ <item name="templateActionWithoutTextMinWidth">?templateButtonHeight</item>
+ <item name="templateActionDefaultIconTint">@color/template_white</item>
+ <item name="templateActionButtonTextMaxEmsNoIcon">@integer/template_action_button_text_max_ems_no_icon</item>
+ <item name="templateActionButtonTextMaxEmsWithIcon">@integer/template_action_button_text_max_ems_with_icon</item>
+ <item name="templateFabTextMaxEmsNoIcon">@integer/template_fab_text_max_ems_no_icon</item>
+ <item name="templateFabTextMaxEmsWithIcon">@integer/template_fab_text_max_ems_with_icon</item>
+ <item name="templateActionButtonSecondaryBorderWidth">@dimen/template_action_button_secondary_border_width</item>
+ <item name="templateActionButtonSecondaryBorderColor">@color/template_white_46</item>
+ <item name="templateActionStripButtonMargin">?templateControlToControlSpacingHorizontal</item>
+ <item name="templateActionStripPadding">@dimen/template_action_strip_padding</item>
+
+ <item name="templateActionStripButtonBackgroundColor">@color/template_black</item>
+ <item name="templateActionStripFabAppearance">@style/FabAppearance.Template.Fab</item>
+ <item name="templateActionStripFullTemplateFabAppearance">@style/FabAppearance.Template.FullTemplate</item>
+
+ <!-- FAB background color.
+ They point to the same resource, which is set to a different value between light and dark modes. -->
+ <item name="templateActionStripFabBackgroundColorLight">@color/template_action_strip_fab_background_color</item>
+ <item name="templateActionStripFabBackgroundColorDark">@color/template_action_strip_fab_background_color</item>
+
+ <!-- Toggles and radio buttons. -->
+ <item name="templateToggleWidth">@dimen/template_toggle_width</item>
+ <item name="templateToggleHeight">@dimen/template_toggle_height</item>
+ <item name="templateToggleInactiveTrackColor">@color/template_toggle_inactive_track</item>
+ <item name="templateToggleInactiveThumbColor">@color/template_toggle_inactive_thumb</item>
+ <item name="templateToggleActiveTrackColor">@color/template_toggle_active_track</item>
+ <item name="templateToggleActiveThumbColor">@color/template_toggle_active_thumb</item>
+ <item name="templateRadioButtonSize">@dimen/template_radio_button_size</item>
+
+ <!-- Clickable spans. -->
+ <item name="templateClickableSpanHighlightForegroundColor">@color/template_black</item>
+ <item name="templateClickableSpanHighlightBackgroundColor">@color/template_focus_ring_color_selector</item>
+
+ <!-- Full screen message -->
+ <item name="templateMessageDefaultIconTint">@color/template_white</item>
+ <item name="templateMessageTitleTextStyle">@style/Widget.Template.Text.Message</item>
+ <item name="templateMessageTitleTopSpacing">@dimen/template_message_title_top_spacing</item>
+ <item name="templateMessageButtonsTopSpacing">@dimen/template_message_buttons_top_spacing</item>
+ <item name="templateStickyButtonsVerticalSpacing">@dimen/template_sticky_buttons_vertical_spacing</item>
+
+ <item name="templateMessageLongTextStyle">@style/Widget.Template.Text.LongMessage</item>
+ <item name="templateMessageDebugTextStyle">@style/Widget.Template.Debug</item>
+ <item name="templateDebugMessageBackgroundColor">@color/template_gray_900</item>
+
+ <!-- Focus. -->
+ <item name="templateFocusAccentColor">@color/template_focus_ring_color_selector</item>
+ <item name="templateFocusNoContentAccentColor">@color/default_focus_no_content</item>
+ <item name="templateFocusRingColor">@color/default_focus_blue</item>
+ <item name="templateFocusRingNoAccentColor">@color/default_focus_no_content</item>
+
+ <!-- EditText. -->
+ <item name="templateEditTextStyle">@style/Widget.CarAppUi.EditText</item>
+ <item name="templateEditTextActiveColor">@color/template_edit_text_active_color</item>
+ <item name="templateEditTextEnabledColor">@color/template_edit_text_enabled_color</item>
+ <item name="templateEditTextErrorColor">@color/template_edit_text_error_color</item>
+ <item name="templateEditTextDisabledColor">@color/template_edit_text_disabled_color</item>
+ <item name="templateEditTextErrorVerticalSpacing">@dimen/car_app_ui_edit_text_error_vertical_spacing</item>
+ <item name="templateEditTextErrorHorizontalSpacing">@dimen/car_app_ui_edit_text_error_horizontal_spacing</item>
+
+ <!-- Search bar. -->
+ <item name="templateSearchBarMaxWidth">@dimen/template_search_bar_max_width</item>
+ <item name="templateSearchBarIcon">@drawable/search_bar_icon</item>
+
+ <!-- Routing -->
+ <item name="templateRoutingStepsCardIconToDistanceSpacingHorizontal">@dimen/template_steps_card_image_to_text_spacing_horizontal</item>
+ <item name="templateRoutingImageSpanRatio">@dimen/template_routing_image_span_ratio</item>
+ <item name="templateRoutingImageSpanBody2MaxHeight">@dimen/template_routing_image_span_body2_max_height</item>
+ <item name="templateRoutingImageSpanBody3MaxHeight">@dimen/template_routing_image_span_body3_max_height</item>
+ <item name="templateNavCardLargeTextSize" format="dimension">@dimen/car_app_ui_nav_card_large_text_size</item>
+ <item name="templateNavCardXLargeTextSize" format="dimension">@dimen/car_app_ui_nav_card_xlarge_text_size</item>
+ <item name="templateNavCardLargeImageSize" format="dimension">@dimen/template_nav_card_large_image_size</item>
+ <item name="templateNavCardLargeImageSizeMin" format="dimension">@dimen/template_nav_card_large_image_size_min</item>
+ <item name="templateNavCardLargeImageSizeMax" format="dimension">@dimen/template_nav_card_large_image_size_max</item>
+ <item name="templateNavCardSmallImageSize" format="dimension">@dimen/template_nav_card_small_image_size</item>
+ <item name="templateNavCardSmallImageSizeMin" format="dimension">@dimen/template_nav_card_small_image_size_min</item>
+ <item name="templateNavCardSmallImageSizeMax" format="dimension">@dimen/template_nav_card_small_image_size_max</item>
+ <item name="templateNavCardFallbackContentColor">@color/default_white</item>
+ <item name="templateRoutingDistanceStyle">@style/Widget.Template.Routing.Distance</item>
+ <item name="templateRoutingDescriptionStyle">@style/Widget.Template.Routing.Description</item>
+ <item name="templateRoutingCompactDescriptionStyle">@style/Widget.Template.Routing.CompactDescription</item>
+ <item name="templateRoutingTravelEstimateStyle">@style/Widget.Template.Routing.TravelEstimate</item>
+ <item name="templateRoutingLanesImageContainerHeight">@dimen/template_routing_lanes_image_container_height</item>
+ <item name="templateRoutingLanesImageContainerVerticalPadding">@dimen/template_padding_0</item>
+ <item name="templateRoutingLanesImageContainerHorizontalPadding">@dimen/template_padding_2</item>
+ <item name="templateRoutingLanesImageBackgroundColor">@color/template_white_16</item>
+ <item name="templateRoutingJunctionImageBackgroundColor">@color/template_white_16</item>
+ <item name="templateRoutingMessagePrimaryStyle">@style/Widget.Template.Routing.MessagePrimary</item>
+ <item name="templateRoutingMessageSecondaryStyle">@style/Widget.Template.Routing.MessageSecondary</item>
+ <item name="templateRoutingMessageInnerPaddingHorizontal">@dimen/template_padding_3</item>
+ <item name="templateRoutingMessageInnerPaddingVertical">@dimen/template_padding_1</item>
+ <item name="templateNavCardPaddingHorizontal">@dimen/template_nav_card_padding_horizontal</item>
+ <item name="templateNavCardPaddingVertical">@dimen/template_nav_card_padding_vertical</item>
+ <item name="templateNavCardSmallPaddingVertical">@dimen/template_nav_card_small_padding_vertical</item>
+ <item name="templateRoutingStepsCardContentContainerMinWidth">@dimen/template_steps_card_content_container_min_width</item>
+ <item name="templateRoutingStepsCardContentContainerMinHeight">@dimen/template_steps_card_content_container_min_height</item>
+ <item name="templateRoutingDividerColor">@color/template_white_16</item>
+
+ <!-- Status bar gradient background start and end colors. -->
+ <item name="templateStatusBarStartColor">@android:color/transparent</item>
+ <item name="templateStatusBarEndColor">@color/template_status_bar_end_color</item>
+ <item name="templateStatusBarMinimumTopPadding">@dimen/template_status_bar_minimum_top_padding</item>
+
+ <!-- No content view. -->
+ <item name="templateNoContentFocusCornerRadius">@dimen/template_no_content_view_focus_corner_radius</item>
+
+ <!-- Animation. -->
+ <item name="templateUpdateAnimationDurationMilliseconds">@integer/template_update_animation_duration_millis</item>
+
+ <!-- This is necessary so that the floating elements such as the action
+ strip don't get their shadows clipped. -->
+ <item name="android:clipChildren">false</item>
+ <item name="android:clipToPadding">false</item>
+
+ <!-- The max number of rows in a list view. -->
+ <item name="templateListMaxLength">@integer/car_app_ui_list_max_length</item>
+
+ <!-- The max number of grid items in a grid view. -->
+ <item name="templateGridMaxLength">@integer/car_app_ui_grid_max_length</item>
+
+ <item name="templateSendNavStateToSystem">@bool/send_navstates_to_system</item>
+
+ </style>
+
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java
new file mode 100644
index 0000000..9babeca
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AbstractHeaderView.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.OnClickDelegate;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.ui.toolbar.MenuItem;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.Toolbar.NavButtonMode;
+import com.android.car.ui.toolbar.ToolbarController;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/** A view that displays the header for the templates. */
+public abstract class AbstractHeaderView {
+ protected final TemplateContext mTemplateContext;
+ protected final ToolbarController mToolbarController;
+
+ // TODO(b/183853224): Replace with equivalent ToolbarController, once is available
+ @SuppressWarnings("deprecation")
+ private final Toolbar.OnBackListener mBackListener =
+ new Toolbar.OnBackListener() {
+ @Override
+ public boolean onBack() {
+ if (mTemplateContext != null) {
+ mTemplateContext.getBackPressedHandler().onBackPressed();
+ }
+ return true;
+ }
+ };
+
+ protected AbstractHeaderView(
+ TemplateContext templateContext, ToolbarController toolbarController) {
+ mTemplateContext = templateContext;
+ mToolbarController = toolbarController;
+ }
+
+ @VisibleForTesting
+ protected ToolbarController getToolbarController() {
+ return mToolbarController;
+ }
+
+ /** Updates the header action */
+ protected void setAction(@Nullable Action action) {
+ if (action != null && action.getType() == Action.TYPE_BACK) {
+ mToolbarController.registerOnBackListener(mBackListener);
+ mToolbarController.setNavButtonMode(NavButtonMode.BACK);
+ } else {
+ mToolbarController.unregisterOnBackListener(mBackListener);
+ mToolbarController.setNavButtonMode(NavButtonMode.DISABLED);
+ }
+
+ if (action != null && action.getType() == Action.TYPE_APP_ICON) {
+ mToolbarController.setLogo(mTemplateContext.getCarAppPackageInfo().getRoundAppIcon());
+ } else {
+ mToolbarController.setLogo(0);
+ }
+ }
+
+ /** Updates the [ActionStrip] associated with this toolbar */
+ public void setActionStrip(@Nullable ActionStrip actionStrip, ActionsConstraints constraints) {
+ validateActionStrip(actionStrip, constraints);
+ if (actionStrip == null) {
+ mToolbarController.setMenuItems(null);
+ } else {
+ List<MenuItem> menuItems = createMenuItems(actionStrip, mTemplateContext);
+ mToolbarController.setMenuItems(menuItems);
+ }
+ }
+
+ /** Adds a toggle to this toolbar */
+ public void addToggle(@Nullable Drawable icon, @Nullable Consumer<Boolean> onClickListener) {
+ List<MenuItem> menuItemList = new ArrayList<>(mToolbarController.getMenuItems());
+ if (icon != null) {
+ menuItemList.add(new MenuItem.Builder(mTemplateContext).setIcon(icon).build());
+ }
+ menuItemList.add(
+ new MenuItem.Builder(mTemplateContext)
+ .setCheckable()
+ .setOnClickListener(
+ item -> {
+ if (onClickListener != null) {
+ onClickListener.accept(item.isChecked());
+ }
+ })
+ .build());
+ mToolbarController.setMenuItems(menuItemList);
+ }
+
+ /** Ensure the model satisfies the input constraints. */
+ private void validateActionStrip(
+ @Nullable ActionStrip actionStrip, ActionsConstraints constraints) {
+ ActionStripWrapper actionStripWrapper =
+ actionStrip == null ? null : new ActionStripWrapper.Builder(actionStrip).build();
+ try {
+ ActionStripUtils.validateRequiredTypes(actionStripWrapper, constraints);
+ } catch (ActionStripUtils.ValidationException exception) {
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(mTemplateContext.getCarAppPackageInfo().getComponentName())
+ .setCause(exception)
+ .build());
+ }
+ }
+
+ /** Converts an [ActionStrip] to a list of [MenuItem]s. */
+ protected static List<MenuItem> createMenuItems(
+ ActionStrip actionStrip, TemplateContext templateContext) {
+ List<MenuItem> menuItems = new ArrayList<>();
+ for (Object action : actionStrip.getActions()) {
+ if (action instanceof Action) {
+ MenuItem menuItem = createMenuItem((Action) action, templateContext);
+ menuItems.add(menuItem);
+ } else {
+ Log.e(LogTags.TEMPLATE, "Action is not supported: " + action);
+ }
+ }
+ return menuItems;
+ }
+
+ /** Converts an {@link Action} to a {@link MenuItem}. */
+ private static MenuItem createMenuItem(Action action, TemplateContext templateContext) {
+ CarIcon carIcon = action.getIcon();
+ boolean isTinted = carIcon == null || carIcon.getType() != CarIcon.TYPE_APP_ICON;
+ MenuItem.Builder menuItemBuilder =
+ new MenuItem.Builder(templateContext)
+ .setPrimary(true)
+ .setEnabled(true)
+ .setTinted(isTinted)
+ .setShowIconAndTitle(true)
+ .setTitle(CarTextUtils.toCharSequenceOrEmpty(templateContext, action.getTitle()));
+ OnClickDelegate onClickDelegate = action.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ menuItemBuilder.setOnClickListener(
+ item -> CommonUtils.dispatchClick(templateContext, onClickDelegate));
+ }
+
+ MenuItem menuItem = menuItemBuilder.build();
+
+ int menuItemIconSize =
+ (int)
+ templateContext
+ .getResources()
+ .getDimension(com.android.car.ui.R.dimen.car_ui_toolbar_menu_item_icon_size);
+ carIcon = ImageUtils.getIconFromAction(action);
+ if (carIcon != null) {
+ ImageViewParams imageViewParams;
+ CarColor tintColor = carIcon.getTint();
+ if (tintColor != null && tintColor.getColor() != 0) {
+ imageViewParams =
+ ImageViewParams.builder()
+ .setDefaultTint(tintColor.getColor())
+ .setForceTinting(true)
+ .build();
+ } else {
+ imageViewParams = ImageViewParams.DEFAULT;
+ }
+
+ ImageUtils.setImageTargetSrc(
+ templateContext,
+ carIcon,
+ menuItem::setIcon,
+ imageViewParams,
+ menuItemIconSize,
+ menuItemIconSize);
+ }
+ return menuItem;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java
new file mode 100644
index 0000000..96b4c15
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonListView.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT;
+import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET;
+import static com.android.car.libraries.apphost.common.CarHostConfig.PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.CarHostConfig.PrimaryActionOrdering;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView.ActionFlag;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Displays a list of {@link Action}s as buttons in a single horizontal layout. */
+public class ActionButtonListView extends LinearLayout {
+
+ public enum Gravity {
+ /* Indicates that action button list can be rendered within the content. */
+ CENTER,
+
+ /* Indicates that action button list should be pinned to the bottom of the content. */
+ BOTTOM
+ }
+
+ /** A flag that indicates whether the buttons stretch horizontally to fill the available space. */
+ private final boolean mButtonsStretch;
+
+ /**
+ * The maximum button width.
+ *
+ * <p>This limit is only applied when {@link #mButtonsStretch} is set to {@code true}.
+ */
+ private final int mButtonMaxWidth;
+
+ /** The horizontal spacing between the button list and the parent view. */
+ private final int mHorizontalSpacing;
+
+ /** Minimum touch area for each button in this list */
+ private final int mMinTouchTargetSize;
+
+ @ColorInt private final int mDefaultButtonBackgroundColor;
+
+ public ActionButtonListView(Context context) {
+ this(context, null);
+ }
+
+ public ActionButtonListView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ActionButtonListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ActionButtonListView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionButtonListButtonStretchHorizontal,
+ R.attr.templateActionButtonListButtonMaxWidth,
+ R.attr.templatePlainContentHorizontalPadding,
+ R.attr.templateActionButtonTouchTargetSize,
+ R.attr.templateActionButtonDefaultBackgroundColor,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mButtonsStretch = ta.getBoolean(0, false);
+ mButtonMaxWidth = ta.getDimensionPixelSize(1, 0);
+ mHorizontalSpacing = ta.getDimensionPixelSize(2, 0);
+ mMinTouchTargetSize = ta.getDimensionPixelSize(3, 0);
+ mDefaultButtonBackgroundColor = ta.getColor(4, 0);
+ ta.recycle();
+ }
+
+ /** Returns the {@link ActionButtonView} for testing. */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public ActionButtonView getActionButtonView(int index) {
+ int maxIndex = getChildCount() - 1;
+ if (index > maxIndex || index < 0) {
+ throw new IndexOutOfBoundsException(
+ "Action index is not within bounds of [0, " + maxIndex + "]");
+ }
+
+ View child = getChildAt(index);
+ if (child instanceof ActionButtonView) {
+ return (ActionButtonView) child;
+ }
+ throw new IllegalStateException(
+ "Found unexpected type of view in action list: " + child.getClass());
+ }
+
+ /** Returns the size of the list for testing. */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public int size() {
+ return getChildCount();
+ }
+
+ /**
+ * Sets the {@link Action}s that will be mapped into buttons.
+ *
+ * @see ActionFlag
+ */
+ public void setActionList(
+ TemplateContext templateContext, List<Action> actionList, ActionButtonListParams params) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+
+ removeAllViews();
+
+ if (actionList == null || actionList.isEmpty()) {
+ this.setVisibility(View.GONE);
+ return;
+ }
+
+ setVisibility(View.VISIBLE);
+
+ @StyleableRes final int[] themeAttrs = {R.attr.templateActionButtonMargin};
+ TypedArray ta = templateContext.obtainStyledAttributes(themeAttrs);
+ int actionMargin = ta.getDimensionPixelOffset(0, 0);
+ ta.recycle();
+
+ int maxActions = params.getMaxActions();
+ if (actionList.size() > maxActions) {
+ L.w(
+ LogTags.TEMPLATE,
+ "The number of actions exceeds the maximum allowed action count, skipping later actions");
+ actionList = actionList.subList(0, maxActions);
+ }
+
+ if (params.allowOemReordering()) {
+ int primaryActionOrder = templateContext.getCarHostConfig().getPrimaryActionOrder();
+ actionList = reorderActionList(actionList, primaryActionOrder);
+ }
+
+ // Calculate the stretching button width by subtracting the margins and paddings from the
+ // screen width, and dividing the remaining width by the button count. Then cap the
+ // resulting width at the button max width.
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ int buttonCount = actionList.size();
+ int stretchingButtonWidth =
+ min(
+ (screenWidth - actionMargin * (buttonCount - 1) - 2 * mHorizontalSpacing) / buttonCount,
+ mButtonMaxWidth);
+
+ ViewGroup touchContainer = (ViewGroup) getParent();
+ touchContainer.setTouchDelegate(null);
+
+ boolean allowAppColor =
+ getAllowAppColor(templateContext, actionList, params, mDefaultButtonBackgroundColor);
+ params = ActionButtonListParams.builder(params).setAllowAppColor(allowAppColor).build();
+
+ int count = 0;
+ for (Action action : actionList) {
+ View view = inflater.inflate(R.layout.action_button_view, this, false);
+ ((ActionButtonView) view).setAction(templateContext, action, params);
+
+ LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+ if (count > 0) {
+ layoutParams.setMarginStart(actionMargin);
+ }
+
+ if (mButtonsStretch) {
+ layoutParams.width = stretchingButtonWidth;
+ }
+
+ addView(view, layoutParams);
+ ViewUtils.setMinTapTarget(touchContainer, view, mMinTouchTargetSize);
+ ++count;
+ }
+ }
+
+ /** Returns whether app-provided colors can be applied to the buttons. */
+ private static boolean getAllowAppColor(
+ TemplateContext templateContext,
+ List<Action> actionList,
+ ActionButtonListParams params,
+ @ColorInt int defaultButtonBackgroundColor) {
+ if (templateContext.getCarHostConfig().isButtonColorOverriddenByOEM()
+ && params.allowOemColorOverride()) {
+ // OEM overrides app colors
+ return false;
+ }
+
+ // Allow app colors only if the contrast check passes
+ return checkColorContrast(
+ templateContext, actionList, params.getSurroundingColor(), defaultButtonBackgroundColor);
+ }
+
+ /**
+ * Checks the color contrast between contents of the given action list and the background color.
+ */
+ private static boolean checkColorContrast(
+ TemplateContext templateContext,
+ List<Action> actionList,
+ @ColorInt int surroundingColor,
+ @ColorInt int defaultButtonBackgroundColor) {
+ for (Action action : actionList) {
+ // Check if the background color has enough contrast against the surrounding color.
+ CarColor backgroundCarColor = action.getBackgroundColor();
+ if (backgroundCarColor != null) {
+ if (!CarColorUtils.checkColorContrast(
+ templateContext, backgroundCarColor, surroundingColor)) {
+ return false;
+ }
+ }
+
+ // Check if the text color has enough contrast against the background color.
+ @ColorInt
+ int backgroundColor =
+ ActionButtonViewUtils.getBackgroundColor(
+ templateContext,
+ action,
+ /* surroundingColor= */ surroundingColor,
+ /* defaultBackgroundColor= */ defaultButtonBackgroundColor);
+ CarText title = action.getTitle();
+ if (title != null) {
+ if (!CarTextUtils.checkColorContrast(templateContext, title, backgroundColor)) {
+ return false;
+ }
+ }
+
+ // Check if the icon tint has enough contrast against the background color.
+ CarIcon icon = action.getIcon();
+ if (icon != null) {
+ CarColor tint = icon.getTint();
+ if (tint != null) {
+ if (!CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /** {@link ActionButtonView}s will carry out the action when clicked on without toast. */
+ public void enableActionButtons() {
+ for (int index = 0; index < getChildCount(); index++) {
+ View child = getChildAt(index);
+ if (child instanceof ActionButtonView) {
+ ActionButtonView button = (ActionButtonView) child;
+ button.enableActionButton();
+ }
+ }
+ }
+
+ /**
+ * {@link ActionButtonView}s will show a toast with given message instead of carrying out the
+ * action when clicked on.
+ */
+ public void disableActionButtons(String disabledToastMessage) {
+ for (int index = 0; index < getChildCount(); index++) {
+ View child = getChildAt(index);
+ if (child instanceof ActionButtonView) {
+ ActionButtonView button = (ActionButtonView) child;
+ button.disableActionButton(disabledToastMessage);
+ }
+ }
+ }
+
+ private List<Action> reorderActionList(
+ List<Action> actionList, @PrimaryActionOrdering int primaryActionOrder) {
+ ArrayList<Action> mutableActionList = new ArrayList<>(actionList);
+ if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET) {
+ return actionList;
+ }
+ int indexOfPrimaryAction = 0;
+ @Nullable Action primaryAction = null;
+ for (Action action : mutableActionList) {
+ if (ActionButtonViewUtils.isPrimaryAction(action)) {
+ primaryAction = action;
+ break;
+ }
+ indexOfPrimaryAction++;
+ }
+ if (primaryAction != null) {
+ mutableActionList.remove(indexOfPrimaryAction);
+ if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT) {
+ mutableActionList.add(0, primaryAction);
+ } else if (primaryActionOrder == PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT) {
+ mutableActionList.add(primaryAction);
+ }
+ }
+ return mutableActionList;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java
new file mode 100644
index 0000000..fdeb929
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonView.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static androidx.car.app.model.Action.TYPE_BACK;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.Toast;
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnClickDelegate;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Displays an {@link Action} as a button. */
+public class ActionButtonView extends FrameLayout {
+ private static final int[] BUTTON_PRIMARY =
+ new int[] {R.attr.type_primary};
+ private static final int[] BUTTON_CUSTOM =
+ new int[] {R.attr.type_custom};
+ private static final int[] BUTTON_CUSTOM_PRIMARY =
+ new int[] {
+ R.attr.type_custom,
+ R.attr.type_primary
+ };
+
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_SUPPORT_REORDERING_BY_OEM,
+ FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActionFlag {}
+
+ public static final int FLAG_SUPPORT_REORDERING_BY_OEM = 1 << 0;
+ public static final int FLAG_SUPPORT_CUSTOMIZED_COLOR_BY_OEM = 1 << 1;
+
+ @ColorInt private final int mDefaultBackgroundColor;
+ @ColorInt private final int mDefaultIconTint;
+ private final int mMinWidthWithText;
+ private final int mMinWidthWithoutText;
+ private final int mSideAlignmentSpacing;
+ private final int mCustomMaxEms;
+ private boolean mIsPrimary;
+ private boolean mIsCustom;
+ private boolean mIsEnabled;
+ private String mDisabledToastMessage;
+
+ /**
+ * The content alignment.
+ *
+ * <p>The possible values are:
+ *
+ * <ul>
+ * <li>0: center (default)
+ * <li>1: left
+ * <li>2: right
+ * </ul>
+ */
+ private final int mContentAlignment;
+
+ /** A flag that indicates whether the buttons stretch horizontally to fill the available space. */
+ private final boolean mButtonsStretch;
+
+ public ActionButtonView(Context context) {
+ this(context, null);
+ }
+
+ public ActionButtonView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ActionButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ActionButtonView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionButtonDefaultBackgroundColor,
+ R.attr.templateActionDefaultIconTint,
+ R.attr.templateActionWithTextMinWidth,
+ R.attr.templateActionWithoutTextMinWidth,
+ R.attr.templateActionButtonSideAlignmentSpacing,
+ R.attr.templateActionButtonListButtonContentAlignment,
+ R.attr.templateActionButtonListButtonStretchHorizontal,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mDefaultBackgroundColor = ta.getColor(0, 0);
+ mDefaultIconTint = ta.getColor(1, 0);
+ mMinWidthWithText = ta.getDimensionPixelSize(2, 0);
+ mMinWidthWithoutText = ta.getDimensionPixelSize(3, 0);
+ mSideAlignmentSpacing = ta.getDimensionPixelSize(4, 0);
+ mContentAlignment = ta.getInteger(5, 0);
+ mButtonsStretch = ta.getBoolean(6, false);
+ ta.recycle();
+
+ // TODO(b/184195457): remove the custom maxEms limit when we remove the limit for all
+ ta =
+ context.obtainStyledAttributes(
+ attrs, R.styleable.ActionButtonView, defStyleAttr, defStyleRes);
+ mCustomMaxEms = ta.getInt(R.styleable.ActionButtonView_textMaxEms, 0);
+ ta.recycle();
+
+ mIsEnabled = true;
+ }
+
+ /** Returns the {@link android.view.View} title for testing. */
+ @Nullable
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public String getTitle() {
+ CarUiTextView carUiTextView = findViewById(R.id.action_text);
+ if (carUiTextView == null) {
+ return null;
+ }
+ return carUiTextView.getText().toString();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ }
+
+ /** Updates the view from the {@link Action} model. */
+ public ActionButtonView setAction(
+ TemplateContext templateContext, Action action, ActionButtonListParams params) {
+ L.v(LogTags.TEMPLATE, "Setting action view with action: %s", action);
+
+ removeAllViews();
+
+ final boolean allowAppColor = params.allowAppColor();
+
+ // Set the background color
+ final CarColor color = action.getBackgroundColor();
+ @ColorInt int appBackgroundColor = mDefaultBackgroundColor;
+ if (color != null && allowAppColor) {
+ appBackgroundColor =
+ ActionButtonViewUtils.getBackgroundColor(
+ templateContext,
+ action,
+ /* surroundingColor= */ params.getSurroundingColor(),
+ /* defaultBackgroundColor= */ mDefaultBackgroundColor);
+ }
+
+ final boolean useAppColors = appBackgroundColor != mDefaultBackgroundColor;
+ if (useAppColors) {
+ // Set the background as tint to not override the round-corner drawable with ripple effects.
+ setBackgroundTintList(ColorStateList.valueOf(appBackgroundColor));
+ }
+
+ boolean useOemColor =
+ templateContext.getCarHostConfig().isButtonColorOverriddenByOEM()
+ && params.allowOemColorOverride();
+ updateState(
+ /* isCustom= */ useAppColors,
+ /* isPrimary= */ useOemColor && ActionButtonViewUtils.isPrimaryAction(action));
+
+ // Check if the title's color span has enough contrast against the background color
+ final CarColorConstraints textColorConstraints;
+ CarText titleText = action.getTitle();
+ if (allowAppColor
+ && titleText != null
+ && CarTextUtils.checkColorContrast(templateContext, titleText, appBackgroundColor)) {
+ textColorConstraints = CarColorConstraints.UNCONSTRAINED;
+ } else {
+ textColorConstraints = CarColorConstraints.NO_COLOR;
+ }
+
+ final CarTextParams carTextParams =
+ CarTextParams.builder()
+ .setColorSpanConstraints(textColorConstraints)
+ .setBackgroundColor(appBackgroundColor)
+ .build();
+ CharSequence title =
+ CarTextUtils.toCharSequenceOrEmpty(templateContext, titleText, carTextParams);
+ CarIcon icon = ImageUtils.getIconFromAction(action);
+
+ boolean hasTitle = title.length() > 0;
+
+ setMinimumWidth(hasTitle ? mMinWidthWithText : mMinWidthWithoutText);
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ if (icon != null && hasTitle) {
+ inflater.inflate(R.layout.action_button_view_icon_text, this);
+ } else if (hasTitle) {
+ inflater.inflate(R.layout.action_button_view_text, this);
+ } else {
+ inflater.inflate(R.layout.action_button_view_icon, this);
+ }
+
+ if (icon != null) {
+ ImageView iconView = findViewById(R.id.action_icon);
+ ImageViewParams imageViewParams =
+ ImageViewParams.builder()
+ .setDefaultTint(mDefaultIconTint)
+ .setForceTinting(true)
+ .setIgnoreAppTint(!allowAppColor)
+ .setBackgroundColor(appBackgroundColor)
+ .build();
+ ImageUtils.setImageSrc(templateContext, icon, iconView, imageViewParams);
+ }
+
+ if (hasTitle) {
+ CarUiTextView carUiTextView = findViewById(R.id.action_text);
+ if (mCustomMaxEms > 0) {
+ carUiTextView.setMaxEms(mCustomMaxEms);
+ } else if (mButtonsStretch) {
+ // If max EMS is not set and the button stretches, allow the buttons to fill all
+ // available space.
+ carUiTextView.setMaxWidth(Integer.MAX_VALUE);
+ }
+
+ carUiTextView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, action.getTitle(), carTextParams, carUiTextView.getMaxLines()));
+ }
+
+ // Update the click listener, if one is set.
+ if (action.getType() == TYPE_BACK) {
+ setOnClickListener(
+ v -> {
+ if (!mIsEnabled) {
+ showDisabledToast(templateContext);
+ return;
+ }
+
+ templateContext.getBackPressedHandler().onBackPressed();
+ });
+ } else {
+ OnClickDelegate onClickDelegate = action.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ setOnClickListener(
+ v -> {
+ ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_BUTTON_CLICKED);
+
+ if (!mIsEnabled) {
+ showDisabledToast(templateContext);
+ return;
+ }
+
+ CommonUtils.dispatchClick(templateContext, onClickDelegate);
+ });
+ } else {
+ setOnClickListener(null);
+ }
+ }
+
+ // Set the content alignment and margins
+ View contentView = getChildAt(0);
+ if (contentView != null) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) contentView.getLayoutParams();
+ int contentGravity = getContentGravity(mContentAlignment);
+ layoutParams.gravity = contentGravity;
+
+ // If the content is aligned to the side, use side-alignment-specific horizontal
+ // margins.
+ if (contentGravity != Gravity.CENTER) {
+ layoutParams.leftMargin = mSideAlignmentSpacing;
+ layoutParams.rightMargin = mSideAlignmentSpacing;
+ }
+
+ contentView.setLayoutParams(layoutParams);
+ }
+
+ return this;
+ }
+
+ /** {@link ActionButtonView} will carry out the action when clicked on without toast. */
+ public void enableActionButton() {
+ mIsEnabled = true;
+ }
+
+ /**
+ * {@link ActionButtonView} will show a toast with given message instead of carrying out the
+ * action when clicked on.
+ */
+ public void disableActionButton(String disabledToastMessage) {
+ mIsEnabled = false;
+ mDisabledToastMessage = disabledToastMessage;
+ }
+
+ private void showDisabledToast(TemplateContext templateContext) {
+ templateContext.getToastController().showToast(mDisabledToastMessage, Toast.LENGTH_SHORT);
+ }
+
+ /** Returns {@code true} if action button is enabled. */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public boolean isActionButtonEnabled() {
+ return mIsEnabled;
+ }
+
+ /** Gets the gravity value that corresponds to the content alignment value. */
+ private static int getContentGravity(int contentAlignment) {
+ int gravity = Gravity.CENTER_VERTICAL;
+ switch (contentAlignment) {
+ case 1:
+ gravity |= Gravity.LEFT;
+ break;
+ case 2:
+ gravity |= Gravity.RIGHT;
+ break;
+ case 0: // fall-through
+ default:
+ gravity = Gravity.CENTER;
+ }
+
+ return gravity;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ int[] additionalStates;
+ if (mIsPrimary && mIsCustom) {
+ additionalStates = BUTTON_CUSTOM_PRIMARY;
+ } else if (mIsPrimary) {
+ additionalStates = BUTTON_PRIMARY;
+ } else if (mIsCustom) {
+ additionalStates = BUTTON_CUSTOM;
+ } else {
+ return super.onCreateDrawableState(extraSpace);
+ }
+ int[] state = super.onCreateDrawableState(extraSpace + additionalStates.length);
+ mergeDrawableStates(state, additionalStates);
+ return state;
+ }
+
+ private void updateState(boolean isCustom, boolean isPrimary) {
+ if (isCustom != mIsCustom || isPrimary != mIsPrimary) {
+ mIsCustom = isCustom;
+ mIsPrimary = isPrimary;
+ refreshDrawableState();
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java
new file mode 100644
index 0000000..596f9d7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionButtonViewUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import androidx.annotation.ColorInt;
+import androidx.car.app.model.Action;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+
+/** Util class for {@link ActionButtonView}. */
+final class ActionButtonViewUtils {
+
+ /** Returns whether the given action is a primary action. */
+ static boolean isPrimaryAction(Action action) {
+ return (action.getFlags() & Action.FLAG_PRIMARY) != 0;
+ }
+
+ /** Returns the background color of the given action. */
+ static int getBackgroundColor(
+ TemplateContext templateContext,
+ Action action,
+ @ColorInt int surroundingColor,
+ @ColorInt int defaultBackgroundColor) {
+ return CarColorUtils.resolveColor(
+ templateContext,
+ /* carColor= */ action.getBackgroundColor(),
+ /* isDark= */ true,
+ /* defaultColor= */ defaultBackgroundColor,
+ /* constraints= */ CarColorConstraints.UNCONSTRAINED,
+ /* backgroundColor= */ surroundingColor);
+ }
+
+ private ActionButtonViewUtils() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java
new file mode 100644
index 0000000..4776734
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionListUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import androidx.car.app.model.Action;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Util class for {@link Action} lists. */
+public final class ActionListUtils {
+ /**
+ * Returns whether the given object is a list of {@link Action}s or not.
+ *
+ * <p>An empty list is not considered an action list.
+ */
+ @SuppressWarnings("unchecked")
+ public static boolean isActionList(Object obj) {
+ if (!(obj instanceof List)) {
+ return false;
+ }
+
+ List<Object> list = (List) obj;
+ if (list.isEmpty()) {
+ return false;
+ }
+
+ // Only check if the first element is an action. When we create a list of actions later, we
+ // will
+ // skip non-action elements.
+ return list.get(0) instanceof Action;
+ }
+
+ /**
+ * Returns a list of {@link Action}s if the given object is an action list, and an empty list if
+ * it is not.
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Action> getActionList(Object obj) {
+ List<Action> actionList = new ArrayList<>();
+ if (obj instanceof List) {
+ List<Object> list = (List) obj;
+ for (Object element : list) {
+ if (element instanceof Action) {
+ actionList.add((Action) element);
+ }
+ }
+ }
+
+ return actionList;
+ }
+
+ private ActionListUtils() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java
new file mode 100644
index 0000000..e4beced
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.template.view.model.ActionWrapper;
+import com.google.common.collect.ImmutableList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Util class for {@link ActionStrip}. */
+final class ActionStripUtils {
+ /**
+ * Validates the {@link ActionStrip} against the {@link ActionsConstraints} instance's required
+ * types.
+ *
+ * @throws ValidationException if the action strip does not meet the required type constraints.
+ */
+ static void validateRequiredTypes(
+ @Nullable ActionStripWrapper actionStrip, ActionsConstraints constraints)
+ throws ValidationException {
+ List<ActionWrapper> actions =
+ actionStrip == null ? ImmutableList.of() : actionStrip.getActions();
+
+ // Check for any missing required types.
+ Set<Integer> requiredActionTypes = constraints.getRequiredActionTypes();
+ if (!requiredActionTypes.isEmpty()) {
+ Set<Integer> requiredTypes = new HashSet<>(requiredActionTypes);
+
+ for (ActionWrapper action : actions) {
+ requiredTypes.remove(action.get().getType());
+ }
+
+ if (!requiredTypes.isEmpty()) {
+ StringBuilder missingTypeError = new StringBuilder();
+ for (int type : requiredTypes) {
+ missingTypeError.append(Action.typeToString(type)).append(";");
+ }
+ throw new ValidationException("Missing required action types: " + missingTypeError);
+ }
+ }
+ }
+
+ private ActionStripUtils() {}
+
+ /** An exception thrown if the action strip validation fails. */
+ static class ValidationException extends Exception {
+ private ValidationException(String errorMessage) {
+ super(errorMessage);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java
new file mode 100644
index 0000000..e1f9ed1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ActionStripView.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static android.widget.LinearLayout.VERTICAL;
+import static com.android.car.libraries.apphost.template.view.model.ActionStripWrapper.INVALID_FOCUSED_ACTION_INDEX;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.template.view.model.ActionStripWrapper;
+import com.android.car.libraries.apphost.template.view.model.ActionWrapper;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionStripUtils.ValidationException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A view that displays an action strip for the templates. */
+public class ActionStripView extends FrameLayout {
+ public static final long ACTIONSTRIP_ACTIVE_STATE_DURATION_MILLIS = SECONDS.toMillis(10);
+ private static final int MSG_ACTIONSTRIP_ACTIVE_STATE = 1;
+
+ /** A delegate that responds to the visibility updates due to active state changes. */
+ public interface ActiveStateDelegate {
+ /** Invoked when the view's visibility changes due to the active state change. */
+ void onActiveStateVisibilityChanged();
+ }
+
+ private final Handler mHandler = new Handler(new HandlerCallback());
+
+ private boolean mIsActive = true;
+ private boolean mAllowTwoLines = false;
+ private LinearLayout mPrimaryContainer;
+ private LinearLayout mSecondaryContainer;
+ private ViewGroup mTouchContainer;
+ private final int mButtonMargin;
+ private final int mButtonHeight;
+ private final int mMinTouchTargetSize;
+ private TemplateContext mTemplateContext;
+ @StyleRes private final int mFabStyleResId;
+
+ @Nullable private ActiveStateDelegate mActiveStateDelegate;
+
+ @Nullable ComponentName mAppName;
+
+ public ActionStripView(Context context) {
+ this(context, null);
+ }
+
+ public ActionStripView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ActionStripView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings({"ResourceType"})
+ public ActionStripView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionStripButtonMargin,
+ R.attr.templateActionButtonHeight,
+ R.attr.templateActionButtonTouchTargetSize,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mButtonMargin = ta.getDimensionPixelSize(0, 0);
+ mButtonHeight = ta.getDimensionPixelOffset(1, 0);
+ mMinTouchTargetSize = ta.getDimensionPixelOffset(2, 0);
+ ta.recycle();
+
+ // Get the fab appearance style resource id from the view's attributes.
+ TypedArray viewStyledAttributes =
+ context.obtainStyledAttributes(
+ attrs, R.styleable.ActionStripView, defStyleAttr, defStyleRes);
+ mFabStyleResId =
+ viewStyledAttributes.getResourceId(R.styleable.ActionStripView_fabAppearance, -1);
+ viewStyledAttributes.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mPrimaryContainer = findViewById(R.id.action_strip_container);
+ mSecondaryContainer = findViewById(R.id.action_strip_container_secondary);
+ mTouchContainer = findViewById(R.id.action_strip_touch_container);
+ }
+
+ /** Returns whether the buttons are allowed to be arranged in two lines. */
+ public boolean getAllowTwoLines() {
+ return mAllowTwoLines;
+ }
+
+ /** Sets the {@link ActiveStateDelegate} for this action strip view. */
+ public void setActiveStateDelegate(ActiveStateDelegate activeStateDelegate) {
+ this.mActiveStateDelegate = activeStateDelegate;
+ }
+
+ /**
+ * Updates the {@link ActionStrip} to the {@link ActionStripView}.
+ *
+ * @see {@link #setActionStrip(TemplateContext, ActionStrip, ActionsConstraints, boolean)}.
+ */
+ public void setActionStrip(
+ TemplateContext templateContext,
+ @Nullable ActionStrip actionStrip,
+ ActionsConstraints constraints) {
+ setActionStrip(templateContext, actionStrip, constraints, false);
+ }
+
+ /**
+ * Updates the {@link ActionStrip} to the {@link ActionStripView}.
+ *
+ * @see {@link #setActionStrip(TemplateContext, ActionStrip, ActionsConstraints, boolean)}.
+ */
+ public void setActionStrip(
+ TemplateContext templateContext,
+ @Nullable ActionStrip actionStrip,
+ ActionsConstraints constraints,
+ boolean allowTwoLines) {
+ ActionStripWrapper actionStripWrapper =
+ actionStrip == null ? null : new ActionStripWrapper.Builder(actionStrip).build();
+ setActionStrip(templateContext, actionStripWrapper, constraints, allowTwoLines);
+ }
+
+ /**
+ * Updates the {@link ActionStrip} to the {@link ActionStripView}.
+ *
+ * <p>The {@link ActionStrip} will be validated against the given {@link ActionsConstraints}
+ * instance. If the number of {@link Action}s in the action strip exceeds the max allowed actions
+ * as specified in the constraints, the {@link Action}s beyond the allowed number will be dropped
+ * from the view.
+ *
+ * <p>If the {@link ActionStrip} is {@code null} or if there are no {@link Action}s added to the
+ * view, the action strip will be hidden.
+ *
+ * <p>If {@code allowTwoLines} is {@code true}, the buttons are positioned in two lines. The last
+ * two actions will be in the primary container, and the rest in the secondary container.
+ */
+ public void setActionStrip(
+ TemplateContext templateContext,
+ @Nullable ActionStripWrapper actionStrip,
+ ActionsConstraints constraints,
+ boolean allowTwoLines) {
+ mAllowTwoLines = allowTwoLines;
+ mAppName = templateContext.getCarAppPackageInfo().getComponentName();
+ mTemplateContext = templateContext;
+ // Ensure the model satisfies the input constraints.
+ try {
+ ActionStripUtils.validateRequiredTypes(actionStrip, constraints);
+ } catch (ValidationException exception) {
+ templateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(templateContext.getCarAppPackageInfo().getComponentName())
+ .setCause(exception)
+ .build());
+ }
+
+ if (actionStrip == null) {
+ setVisibility(GONE);
+ return;
+ }
+
+ // Set the host-determined index of the action button to focus. Otherwise, if a button was
+ // focused, get its index before removing the button views.
+ int focusedActionIndex =
+ actionStrip.getFocusedActionIndex() == INVALID_FOCUSED_ACTION_INDEX
+ ? getCurrentFocusedActionIndex()
+ : actionStrip.getFocusedActionIndex();
+ mPrimaryContainer.removeAllViews();
+ mSecondaryContainer.removeAllViews();
+
+ int maxAllowedActions = constraints.getMaxActions();
+ int maxAllowedCustomTitles = constraints.getMaxCustomTitles();
+ List<ActionWrapper> actions = actionStrip.getActions();
+ List<ActionWrapper> allowedActions = new ArrayList<>();
+
+ for (ActionWrapper action : actions) {
+ CarText title = action.get().getTitle();
+ if (title != null && !title.isEmpty()) {
+ if (--maxAllowedCustomTitles < 0) {
+ L.w(
+ LogTags.TEMPLATE,
+ "Dropping actions in action strip over max custom title limit of %d",
+ constraints.getMaxCustomTitles());
+ break;
+ }
+ }
+
+ if (--maxAllowedActions < 0) {
+ L.w(
+ LogTags.TEMPLATE,
+ "Dropping actions in action strip over max limit of %d",
+ constraints.getMaxActions());
+ break;
+ }
+
+ allowedActions.add(action);
+ }
+
+ // Go through the actions in reverse, and add them to the appropriate containers. If two
+ // lines are allowed, the last two actions will be in the primary container, and the rest in
+ // the secondary container.
+ int lastPrimaryContainerActionIndex = allowTwoLines ? max(allowedActions.size() - 2, 0) : 0;
+ for (int i = allowedActions.size() - 1; i >= 0; i--) {
+ ActionWrapper action = allowedActions.get(i);
+
+ FabView fabView = new FabView(getContext(), null, 0, mFabStyleResId);
+ LinearLayout container =
+ i >= lastPrimaryContainerActionIndex ? mPrimaryContainer : mSecondaryContainer;
+ container.addView(fabView, 0);
+
+ // Set the action on a fab view.
+ fabView.setAction(templateContext, action);
+ }
+
+ updateFabViewLayoutParams(mPrimaryContainer);
+ updateFabViewLayoutParams(mSecondaryContainer);
+
+ List<View> actionButtons = getActionButtons();
+ int actionCount = actionButtons.size();
+ if (actionCount < 1) {
+ setVisibility(GONE);
+ } else {
+ // If a button was focused before, restore the focus.
+ if (focusedActionIndex >= 0) {
+ int indexToFocus = min(focusedActionIndex, actionCount - 1);
+ actionButtons.get(indexToFocus).requestFocus();
+ }
+
+ mPrimaryContainer.setVisibility(mPrimaryContainer.getChildCount() > 0 ? VISIBLE : GONE);
+ mSecondaryContainer.setVisibility(mSecondaryContainer.getChildCount() > 0 ? VISIBLE : GONE);
+
+ // Synchronize the visibility and the FABs clickable states with the active/idle state,
+ // and do not show the strip's buttons unless it is current active.
+ setVisibility(mIsActive ? VISIBLE : GONE);
+ setFabViewClickableState(mIsActive);
+ }
+
+ updateTouchTarget();
+
+ ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_STRIP_SIZE, actionCount);
+ }
+
+ /**
+ * Requests the action strip is active after the specified delay.
+ *
+ * <p>When {@code true}, the action strip fades in if it is not currently visible. If {@code
+ * false}, the action strip fades out.
+ *
+ * <p>If there is a currently pending request to activate/de-activate the action strip that has
+ * not been processed yet, the previous request will be cancelled and the new request will be
+ * queued.
+ */
+ public void setActiveStateWithDelay(boolean isActive, long millis) {
+ mHandler.removeMessages(MSG_ACTIONSTRIP_ACTIVE_STATE);
+ Message message = mHandler.obtainMessage(MSG_ACTIONSTRIP_ACTIVE_STATE);
+ message.obj = isActive;
+ mHandler.sendMessageDelayed(message, millis);
+ }
+
+ /**
+ * Sets whether the action strip is active.
+ *
+ * <p>This will immediately activate/de-activate the action strip and cancel any pending requests
+ * that might have been sent via {@link #setActiveStateWithDelay}.
+ */
+ public void setActiveState(boolean isActive) {
+ mHandler.removeMessages(MSG_ACTIONSTRIP_ACTIVE_STATE);
+ setActionStateInternal(isActive);
+ }
+
+ private void setActionStateInternal(boolean isActive) {
+ if (mIsActive == isActive) {
+ return;
+ }
+
+ if (mTemplateContext != null) {
+ ViewUtils.logCarAppTelemetry(
+ mTemplateContext, isActive ? UiAction.ACTION_STRIP_SHOW : UiAction.ACTION_STRIP_HIDE);
+ }
+
+ mIsActive = isActive;
+
+ List<Animator> animations = new ArrayList<>();
+ boolean isVisible = isActive && !getActionButtons().isEmpty();
+ int animResId =
+ isVisible ? R.anim.fab_view_animation_fade_in : R.anim.fab_view_animation_fade_out;
+ for (View actionButton : getActionButtons()) {
+ Animator animation = AnimatorInflater.loadAnimator(getContext(), animResId);
+ animation.setTarget(actionButton);
+ animations.add(animation);
+ }
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(animations);
+ animatorSet.setInterpolator(new FastOutSlowInInterpolator());
+ animatorSet.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ // Make the Fab clickable/non-clickable as soon as the animation starts.
+ // Updating this only after the animation has started prevents the user
+ // clicking in the action strip area to activate the strip, and the FAB
+ // responding to the same click event.
+ // TODO(b/165887188): add test for this.
+ setFabViewClickableState(isActive);
+
+ if (isVisible) {
+ setVisibility(VISIBLE);
+
+ ActiveStateDelegate delegate = mActiveStateDelegate;
+ if (delegate != null) {
+ delegate.onActiveStateVisibilityChanged();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!isVisible) {
+ setVisibility(GONE);
+
+ ActiveStateDelegate delegate = mActiveStateDelegate;
+ if (delegate != null) {
+ delegate.onActiveStateVisibilityChanged();
+ }
+ }
+ }
+ });
+
+ ThreadUtils.runOnMain(() -> animatorSet.start());
+ }
+
+ /**
+ * Returns the index of the focused button.
+ *
+ * <p>If none are focused, returns {@link ActionStripWrapper#INVALID_FOCUSED_ACTION_INDEX}.
+ */
+ private int getCurrentFocusedActionIndex() {
+ int focusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+ List<View> actionButtons = getActionButtons();
+
+ for (int i = 0; i < actionButtons.size(); i++) {
+ View fabView = actionButtons.get(i);
+ if (fabView.isFocused()) {
+ focusedActionIndex = i;
+ break;
+ }
+ }
+
+ return focusedActionIndex;
+ }
+
+ /** Gets all action button views in the action strip. */
+ private List<View> getActionButtons() {
+ ArrayList<View> actionButtons = new ArrayList<>();
+ for (int i = 0; i < mSecondaryContainer.getChildCount(); i++) {
+ actionButtons.add(mSecondaryContainer.getChildAt(i));
+ }
+ for (int i = 0; i < mPrimaryContainer.getChildCount(); i++) {
+ actionButtons.add(mPrimaryContainer.getChildAt(i));
+ }
+ return actionButtons;
+ }
+
+ private void updateFabViewLayoutParams(LinearLayout container) {
+ for (int i = 0; i < container.getChildCount(); i++) {
+ FabView fabView = (FabView) container.getChildAt(i);
+
+ LinearLayout.LayoutParams layoutParams =
+ new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, mButtonHeight);
+
+ // Set the margins on buttons after the first one.
+ if (i > 0) {
+ if (container.getOrientation() == VERTICAL) {
+ layoutParams.topMargin = mButtonMargin;
+ } else {
+ layoutParams.leftMargin = mButtonMargin;
+ }
+ }
+
+ fabView.setLayoutParams(layoutParams);
+ }
+ }
+
+ private void setFabViewClickableState(boolean clickable) {
+ for (View actionButton : getActionButtons()) {
+ FabView view = (FabView) actionButton;
+ view.setClickable(clickable);
+ }
+ }
+
+ private void updateTouchTarget() {
+ mTouchContainer.setTouchDelegate(null);
+ for (View actionButton : getActionButtons()) {
+ FabView view = (FabView) actionButton;
+ ViewUtils.setMinTapTarget(mTouchContainer, view, mMinTouchTargetSize);
+ }
+ }
+
+ /** A {@link Handler.Callback} for delay activate/de-activate the action strip. */
+ private class HandlerCallback implements Handler.Callback {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_ACTIONSTRIP_ACTIVE_STATE) {
+ boolean isActive = (boolean) msg.obj;
+ setActionStateInternal(isActive);
+ } else {
+ L.w(LogTags.TEMPLATE, "Unknown message: %s", msg);
+ }
+ return false;
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml
new file mode 100644
index 0000000..595afa7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest package="com.android.car.libraries.templates.host.view"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-sdk android:minSdkVersion="21"/>
+</manifest>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java
new file mode 100644
index 0000000..79f7750
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/BleedingCardView.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.templates.host.R;
+import java.util.Arrays;
+
+/**
+ * A card view that "bleeds" through the bottom of its parent.
+ *
+ * <p>"Bleeding" means its rounded corners become square at the bottom when the card's bottom is at,
+ * or past its parent's bottom, thus creating an effect as if the card is "bleeding through" (or
+ * "peeking out of") the bottom of the parent.
+ */
+public class BleedingCardView extends FrameLayout {
+ // Percentage of the length of the card radius that the background radius is reduced by to avoid
+ // it showing up from underneath the foreground border and creating a subtle but ugly aliasing
+ // effect
+ private static final float BACKGROUND_RADIUS_PERCENTAGE = 0.25f;
+
+ private final int mRadius;
+ private final int mBorderWidth;
+ @ColorInt private final int mBorderColor;
+ @ColorInt private int mBackgroundColor;
+ private final float mWidthFraction;
+ private final int mMinWidth;
+ private final int mMaxWidth;
+ private final int mOemWidth;
+ private final int mOemMaxWidth;
+
+ public BleedingCardView(Context context) {
+ this(context, null);
+ }
+
+ public BleedingCardView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BleedingCardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"})
+ public BleedingCardView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ super(context, attrs, defStyleAttrs, defStyleRes);
+
+ TypedArray ta =
+ context.obtainStyledAttributes(attrs, R.styleable.BleedingCardView, defStyleAttrs, 0);
+ mBorderWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardBorderWidth, 0);
+ mBorderColor = ta.getColor(R.styleable.BleedingCardView_cardBorderColor, 0);
+ @ColorInt
+ int backgroundColor = ta.getColor(R.styleable.BleedingCardView_cardBackgroundColor, 0);
+ @ColorInt int textColor = ta.getColor(R.styleable.BleedingCardView_cardTextColor, 0);
+ @ColorInt
+ int fallbackDarkBackgroundColor =
+ ta.getColor(R.styleable.BleedingCardView_cardFallbackDarkBackgroundColor, 0);
+ @ColorInt
+ int fallbackLightBackgroundColor =
+ ta.getColor(R.styleable.BleedingCardView_cardFallbackLightBackgroundColor, 0);
+ mRadius = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardRadius, 0);
+ mWidthFraction = ta.getFloat(R.styleable.BleedingCardView_cardWidthFraction, 0.f);
+ mMinWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardMinWidth, 0);
+ mMaxWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardMaxWidth, 0);
+ mOemWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardOemWidth, 0);
+ mOemMaxWidth = ta.getDimensionPixelSize(R.styleable.BleedingCardView_cardOemMaxWidth, 0);
+ ta.recycle();
+
+ setClipToOutline(true);
+
+ mBackgroundColor =
+ calculateBackgroundColor(
+ backgroundColor, textColor, fallbackDarkBackgroundColor, fallbackLightBackgroundColor);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ updateCardBackground();
+ }
+
+ public int getCardRadius() {
+ return mRadius;
+ }
+
+ @ColorInt
+ public int getCardBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ /** Sets the background color and triggers an update if it has changed. */
+ public void setCardBackgroundColor(@ColorInt int backgroundColor) {
+ if (mBackgroundColor == backgroundColor) {
+ return;
+ }
+ mBackgroundColor = backgroundColor;
+ updateCardBackground();
+ }
+
+ /** Sets the card width either based on the set {@link #mOemWidth}, or {@link #mWidthFraction}. */
+ private void setCardWidthIfNeeded() {
+ // TODO(b/162419749): Set the percent width in the xml file, without using ConstraintLayout.
+ if (mOemWidth > 0) {
+ // If the OEM defined the card width, use it after checking for min and max values.
+ int cardWidth = mOemWidth;
+ cardWidth = min(cardWidth, mOemMaxWidth);
+ cardWidth = max(cardWidth, mMinWidth);
+ getLayoutParams().width = cardWidth;
+ } else if (mWidthFraction > 0) {
+ // If the width fraction is set, use it after checking for min and max values.
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ int cardWidth = (int) (screenWidth * mWidthFraction);
+ cardWidth = min(cardWidth, mMaxWidth);
+ cardWidth = max(cardWidth, mMinWidth);
+ getLayoutParams().width = cardWidth;
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ updateCardBackground();
+ }
+
+ /** Returns a background color with proper contrast ratio for the given text color. */
+ @ColorInt
+ private int calculateBackgroundColor(
+ @ColorInt int backgroundColor,
+ @ColorInt int textColor,
+ @ColorInt int fallbackDarkBackgroundColor,
+ @ColorInt int fallbackLightBackgroundColor) {
+ if (CarColorUtils.hasMinimumColorContrast(textColor, backgroundColor)) {
+ return backgroundColor;
+ } else if (CarColorUtils.hasMinimumColorContrast(textColor, fallbackDarkBackgroundColor)) {
+ return fallbackDarkBackgroundColor;
+ } else {
+ return fallbackLightBackgroundColor;
+ }
+ }
+
+ private Drawable createBackground(float[] radii) {
+ // Create a drawable for the background.
+ GradientDrawable backDrawable = new GradientDrawable();
+
+ // Reduce the radius a bit to avoid the background popping from outside of the border.
+ float reduction = mRadius * BACKGROUND_RADIUS_PERCENTAGE;
+ float[] backRadii = Arrays.copyOf(radii, 8);
+ for (int i = 0; i < radii.length; ++i) {
+ radii[i] -= reduction;
+ }
+ backDrawable.setCornerRadii(backRadii);
+ backDrawable.setColor(mBackgroundColor);
+
+ return backDrawable;
+ }
+
+ private Drawable createForeground(float[] radii) {
+ // Create the border drawable.
+ GradientDrawable borderDrawable = new GradientDrawable();
+
+ // Blend the border with the background color. This method returns a fully opaque color. We
+ // do
+ // this instead of drawing the border over the background with an alpha so that any contents
+ // of the card get are drawn underneath the border (e.g. the lighter rectangle we display
+ // over
+ // the lanes image) don't get blended with the border, and the border is rather of a single
+ // color.
+ borderDrawable.setStroke(
+ mBorderWidth, CarColorUtils.blendColorsSrc(mBorderColor, mBackgroundColor));
+ borderDrawable.setCornerRadii(radii);
+ return borderDrawable;
+ }
+
+ private void updateCardBackground() {
+ setCardWidthIfNeeded();
+
+ // Determine whether the card is bleeding, i.e. if it goes past the bottom of the parent.
+ boolean isBleeding = isBleeding();
+
+ // Remove the bottom rounded corners if the card is bleeding.
+ float bottomRadius = isBleeding ? 0 : mRadius;
+ float[] radii =
+ new float[] {
+ mRadius, mRadius, mRadius, mRadius, bottomRadius, bottomRadius, bottomRadius, bottomRadius
+ };
+
+ // Set the background.
+ setBackground(createBackground(radii));
+
+ // Set the foreground border.
+ Drawable foreground = createForeground(radii);
+ if (isBleeding) {
+ // If bleeding, inset the bottom with a negative value to hide the bottom border.
+ foreground = new InsetDrawable(foreground, 0, 0, 0, -mBorderWidth);
+ }
+ setForeground(foreground);
+
+ // Set the card view's outline with rounded corners to clip its child views (e.g. junction
+ // image).
+ ViewOutlineProvider outlineProvider =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int bottom = view.getHeight();
+ if (isBleeding()) {
+ // If the card view is bleeding, add the radius value so that only the
+ // top corners are rounded.
+ bottom += mRadius;
+ }
+ outline.setRoundRect(0, 0, view.getWidth(), bottom, mRadius);
+ }
+ };
+ setOutlineProvider(outlineProvider);
+
+ invalidate();
+ }
+
+ private boolean isBleeding() {
+ ViewGroup parent = (ViewGroup) getParent();
+ boolean isBleeding = false;
+ if (parent != null) {
+ int parentHeight = parent.getHeight();
+ int bottom = getTop() + getHeight();
+ if (bottom >= parentHeight) {
+ isBleeding = true;
+ }
+ }
+ return isBleeding;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java
new file mode 100644
index 0000000..ea8764e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditText.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.widget.EditText;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.input.CarEditable;
+import com.android.car.libraries.apphost.input.CarEditableListener;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.libraries.templates.host.R;
+
+/**
+ * A EditText for use in-car. This EditText:
+ *
+ * <ul>
+ * <li>Disables selection
+ * <li>Disables Cut/Copy/Paste
+ * <li>Force-disables suggestions
+ * </ul>
+ */
+public class CarEditText extends EditText implements CarEditable {
+ private static final int[] ERROR_STATE =
+ new int[] {R.attr.state_error};
+ private static final boolean SELECTION_CLAMPING_ENABLED = false;
+
+ private int mLastSelEnd = 0;
+ private int mLastSelStart = 0;
+ private boolean mCursorClamped;
+ private boolean mInErrorState;
+
+ private CarEditableListener mCarEditableListener;
+ private KeyListener mListener;
+ private InputManager mInputManager;
+
+ /**
+ * Listener for events when the user interacts with the keyboard similar to {@link
+ * android.text.method.KeyListener}.
+ */
+ public interface KeyListener {
+ /** Callback when a key is pressed. */
+ void onKeyDown(char key);
+
+ /** Callback when a key is released. */
+ void onKeyUp(char key);
+
+ /** Callback when text has been changed by another input connection or copy/paste. */
+ void onCommitText(String input);
+
+ /** Callback when the user closes the keyboard. */
+ void onCloseKeyboard();
+
+ /** Callback when the text field has been cleared. */
+ void onDelete();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ public CarEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ setTextIsSelectable(false);
+ setLongClickable(false);
+ setFocusableInTouchMode(true);
+ setSelection(getText().length());
+ mCursorClamped = true;
+ setOnEditorActionListener(
+ new OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (mListener != null && actionId == EditorInfo.IME_ACTION_DONE) {
+ mListener.onCloseKeyboard();
+ }
+ // Return false because we don't want to hijack the default behavior.
+ return false;
+ }
+ });
+ setCustomSelectionActionModeCallback(
+ new Callback() {
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {}
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+ });
+ setOnTouchListener(
+ (v, event) -> {
+ if (MotionEvent.ACTION_UP == event.getAction()) {
+ mInputManager.startInput(CarEditText.this);
+ }
+ return false;
+ });
+ }
+
+ public void setKeyListener(KeyListener listener) {
+ mListener = listener;
+ }
+
+ public void setInputManager(InputManager inputManager) {
+ mInputManager = inputManager;
+ }
+
+ /** Sets whether this edit box is in error state or not */
+ public void setErrorState(boolean inErrorState) {
+ mInErrorState = inErrorState;
+ refreshDrawableState();
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ super.onSelectionChanged(selStart, selEnd);
+ if (mCursorClamped && SELECTION_CLAMPING_ENABLED) {
+ setSelection(mLastSelStart, mLastSelEnd);
+ return;
+ }
+ if (mCarEditableListener != null) {
+ mCarEditableListener.onUpdateSelection(mLastSelStart, mLastSelEnd, selStart, selEnd);
+ }
+ mLastSelStart = selStart;
+ mLastSelEnd = selEnd;
+ }
+
+ @Override
+ @Nullable
+ public ActionMode startActionMode(Callback callback) {
+ return null;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] state;
+ if (mInErrorState) {
+ state = super.onCreateDrawableState(extraSpace + 1);
+ mergeDrawableStates(state, ERROR_STATE);
+ } else {
+ state = super.onCreateDrawableState(extraSpace);
+ }
+ return state;
+ }
+
+ @Override
+ public void setCarEditableListener(CarEditableListener listener) {
+ mCarEditableListener = listener;
+ }
+
+ @Override
+ public void setInputEnabled(boolean enabled) {
+ mCursorClamped = !enabled;
+ }
+
+ @Override
+ public boolean performClick() {
+ boolean result = super.performClick();
+ mInputManager.startInput(CarEditText.this);
+ return result;
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+ return new InputConnectionWrapper(inputConnection, false) {
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ // TODO(b/208707793): Remove the handleKeyEventNoWindowFocus if found system side fix for R
+ if (Build.VERSION.SDK_INT == VERSION_CODES.R) {
+ return handleKeyEventNoWindowFocus(event);
+ }
+ if (mListener != null) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ mListener.onKeyDown((char) event.getKeyCode());
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ mListener.onKeyUp((char) event.getKeyCode());
+ }
+ return true;
+ } else {
+ return super.sendKeyEvent(event);
+ }
+ }
+
+ private boolean handleKeyEventNoWindowFocus(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
+ return super.deleteSurroundingText(1, 0);
+ } else {
+ return super.commitText(Character.toString(event.getNumber()), 1);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean commitText(CharSequence charSequence, int i) {
+ if (mListener != null) {
+ mListener.onCommitText(charSequence.toString());
+ return true;
+ }
+ return super.commitText(charSequence, i);
+ }
+
+ @Override
+ public boolean deleteSurroundingText(int i, int i1) {
+ if (mListener != null) {
+ mListener.onDelete();
+ return true;
+ }
+ return super.deleteSurroundingText(i, i1);
+ }
+ };
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java
new file mode 100644
index 0000000..1674db7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarEditTextWrapper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.annotation.SuppressLint;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.input.CarEditable;
+import com.android.car.libraries.apphost.input.CarEditableListener;
+import com.android.car.libraries.apphost.input.InputManager;
+
+/** A wrapper for {@link EditText} to make it conform to {@link CarEditable}. */
+public class CarEditTextWrapper implements CarEditable {
+ private final EditText mEditText;
+ private int mLastSelectionEnd = 0;
+ private int mLastSelectionStart = 0;
+ @Nullable private CarEditableListener mCarEditableListener;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @SuppressWarnings("nullness:argument") // Accessing "this" inside click listener.
+ public CarEditTextWrapper(EditText editText, InputManager inputManager) {
+ mEditText = editText;
+
+ // Setup an accessibility delegate to get the text selection changes. This is required in
+ // order
+ // to conform to the CarEditable which requires a text selection update listener.
+ AccessibilityDelegate accessibilityDelegate =
+ new AccessibilityDelegate() {
+ @Override
+ public void sendAccessibilityEvent(View host, int eventType) {
+ super.sendAccessibilityEvent(host, eventType);
+ if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
+ if (mCarEditableListener != null) {
+ mCarEditableListener.onUpdateSelection(
+ mLastSelectionStart,
+ mLastSelectionEnd,
+ mEditText.getSelectionStart(),
+ mEditText.getSelectionEnd());
+ }
+ mLastSelectionStart = mEditText.getSelectionStart();
+ mLastSelectionEnd = mEditText.getSelectionEnd();
+ }
+ }
+ };
+ editText.setAccessibilityDelegate(accessibilityDelegate);
+ editText.setOnClickListener((view) -> inputManager.startInput(CarEditTextWrapper.this));
+ editText.setOnFocusChangeListener(
+ (view, hasFocus) -> {
+ if (!hasFocus) {
+ inputManager.stopInput();
+ }
+ });
+ // Android will dispatch in the following order:
+ // onTouch
+ // onFocus
+ // onClick
+ // However if internally the view consumes any it will stop dispatching. If an EditText
+ // does not have focus it will consume the focus and not send the onClick.
+ editText.setOnTouchListener(
+ (v, event) -> {
+ if (MotionEvent.ACTION_UP == event.getAction()) {
+ inputManager.startInput(CarEditTextWrapper.this);
+ }
+ return false;
+ });
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return mEditText.onCreateInputConnection(outAttrs);
+ }
+
+ @Override
+ public void setCarEditableListener(@Nullable CarEditableListener listener) {
+ mCarEditableListener = listener;
+ }
+
+ @Override
+ public void setInputEnabled(boolean enabled) {
+ mEditText.setEnabled(enabled);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java
new file mode 100644
index 0000000..0e3d108
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarImageView.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.templates.host.R;
+
+/** An {@link ImageView} that enforces size limits on OEM-customized width and height. */
+@SuppressLint("AppCompatCustomView")
+public final class CarImageView extends ImageView {
+ private final int mMinWidth;
+ private final int mMaxWidth;
+ private final int mMinHeight;
+ private final int mMaxHeight;
+
+ public CarImageView(Context context) {
+ this(context, null);
+ }
+
+ public CarImageView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CarImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CarImageView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray ta =
+ context.obtainStyledAttributes(attrs, R.styleable.CarImageView, defStyleAttr, 0);
+ mMinWidth = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMinWidth, 0);
+ mMaxWidth = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMaxWidth, Integer.MAX_VALUE);
+ mMinHeight = ta.getDimensionPixelSize(R.styleable.CarImageView_imageMinHeight, 0);
+ mMaxHeight =
+ ta.getDimensionPixelSize(R.styleable.CarImageView_imageMaxHeight, Integer.MAX_VALUE);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Set the OEM-customizable image size, with the min and max limits.
+ ViewUtils.enforceViewSizeLimit(this, mMinWidth, mMaxWidth, mMinHeight, mMaxHeight);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java
new file mode 100644
index 0000000..8a9fd75
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarProgressBar.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.templates.host.R;
+
+/** An {@link ProgressBar} that enforces size limits on OEM-customized width and height. */
+public class CarProgressBar extends ProgressBar {
+ private final int mMinSize;
+ private final int mMaxSize;
+
+ public CarProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public CarProgressBar(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CarProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CarProgressBar(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray ta =
+ context.obtainStyledAttributes(attrs, R.styleable.CarProgressBar, defStyleAttr, 0);
+ mMinSize = ta.getDimensionPixelSize(R.styleable.CarProgressBar_imageMinSize, 0);
+ mMaxSize = ta.getDimensionPixelSize(R.styleable.CarProgressBar_imageMaxSize, Integer.MAX_VALUE);
+ ta.recycle();
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Set the OEM-customizable image size, with the min and max limits.
+ ViewUtils.enforceViewSizeLimit(this, mMinSize, mMaxSize, mMinSize, mMaxSize);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java
new file mode 100644
index 0000000..7a92013
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CarUiTextUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.util.Objects.requireNonNull;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.ui.CarUiText;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Util class for {@link CarUiText}. */
+public class CarUiTextUtils {
+ private CarUiTextUtils() {}
+
+ /** Creates a {@link CarUiText} from a {@link CarText}. */
+ public static CarUiText fromCarText(
+ TemplateContext context, @Nullable CarText carText, int maxLines) {
+ return fromCarText(context, carText, CarTextParams.DEFAULT, maxLines);
+ }
+
+ /** Creates a {@link CarUiText} from a {@link CarText}. */
+ public static CarUiText fromCarText(
+ TemplateContext context, @Nullable CarText carText, CarTextParams params, int maxLines) {
+ if (CarText.isNullOrEmpty(carText)) {
+ return new CarUiText("", maxLines);
+ }
+ requireNonNull(carText);
+
+ List<CharSequence> textVariants = new ArrayList<>();
+ textVariants.add(CarTextUtils.toCharSequenceOrEmpty(context, carText, params));
+ for (int i = 0; i < carText.getVariants().size(); i++) {
+ textVariants.add(CarTextUtils.toCharSequenceOrEmpty(context, carText, params, i));
+ }
+ return new CarUiText.Builder(textVariants)
+ .setMaxLines(maxLines)
+ .setMaxChars(context.getConstraintsProvider().getStringCharacterLimit())
+ .build();
+ }
+
+ /** Creates a {@link CarUiText} from a {@link CharSequence}. */
+ public static CarUiText fromCharSequence(
+ TemplateContext context, @NonNull CharSequence charSequence, int maxLines) {
+
+ return new CarUiText.Builder(charSequence)
+ .setMaxLines(maxLines)
+ .setMaxChars(context.getConstraintsProvider().getStringCharacterLimit())
+ .build();
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java
new file mode 100644
index 0000000..d3c1662
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/CardHeaderView.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnContentRefreshDelegate;
+import androidx.core.graphics.drawable.IconCompat;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.TemplateValidator;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** A view that displays the header for the templates. */
+public class CardHeaderView extends LinearLayout {
+ private CarUiTextView mHeaderTitle;
+ private ImageView mHeaderButtonIcon;
+ private FrameLayout mHeaderButtonContainer;
+ private FrameLayout mRefreshButtonContainer;
+ private ImageView mRefreshButtonIcon;
+ @ColorInt private final int mHeaderIconTint;
+
+ public CardHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public CardHeaderView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CardHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings("nullness:argument") // Fix UnderInitialization warnings
+ public CardHeaderView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ LayoutInflater.from(context).inflate(R.layout.header_view, this);
+
+ @StyleableRes final int[] themeAttrs = {R.attr.templateHeaderButtonIconTint};
+ TypedArray themeAttrsArray = context.obtainStyledAttributes(themeAttrs);
+ mHeaderIconTint = themeAttrsArray.getColor(0, 0);
+ themeAttrsArray.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHeaderTitle = findViewById(R.id.header_title);
+ mHeaderButtonContainer = findViewById(R.id.header_button_container);
+ mHeaderButtonIcon = findViewById(R.id.header_icon);
+ mRefreshButtonContainer = findViewById(R.id.refresh_button_container);
+ mRefreshButtonIcon = findViewById(R.id.refresh_icon);
+ ViewUtils.setMinTapTarget(
+ this,
+ mHeaderButtonContainer,
+ getResources().getDimensionPixelSize(R.dimen.template_min_tap_target_size));
+ }
+
+ /**
+ * Update the {@link HeaderView} to show the given {@code title} and header {@code action}.
+ *
+ * <p>If the inputs are {@code null} then the view is hidden.
+ */
+ public void setContent(
+ TemplateContext templateContext, @Nullable CarText title, @Nullable Action action) {
+ setContent(templateContext, title, action, null);
+ }
+
+ /**
+ * Update the {@link HeaderView} to show the given {@code title}, header {@code action}, and, if
+ * {@code contentRefreshDelegate} is not {@code null}, a refresh button that allow users to
+ * interact with to trigger refreshes.
+ *
+ * <p>If the inputs are {@code null} then the view is hidden.
+ */
+ public void setContent(
+ TemplateContext templateContext,
+ @Nullable CarText title,
+ @Nullable Action action,
+ @Nullable OnContentRefreshDelegate contentRefreshDelegate) {
+ boolean isVisible = title != null;
+ if (isVisible) {
+ mHeaderTitle.setText(
+ CarUiTextUtils.fromCarText(templateContext, title, mHeaderTitle.getMaxLines()));
+
+ mHeaderTitle.setVisibility(VISIBLE);
+ } else {
+ mHeaderTitle.setVisibility(GONE);
+ }
+
+ isVisible |= updateHeaderButton(templateContext, action);
+ isVisible |= updateRefreshButton(templateContext, contentRefreshDelegate);
+ setVisibility(isVisible ? VISIBLE : GONE);
+ }
+
+ /**
+ * Updates the optional button in the header.
+ *
+ * @return true if the button ended up visible, false otherwise.
+ */
+ private boolean updateHeaderButton(TemplateContext templateContext, @Nullable Action action) {
+ if (action == null) {
+ mHeaderButtonContainer.setVisibility(GONE);
+ return false;
+ }
+
+ mHeaderButtonContainer.setVisibility(VISIBLE);
+
+ ImageUtils.setImageSrc(
+ templateContext,
+ ImageUtils.getIconFromAction(action),
+ mHeaderButtonIcon,
+ ImageViewParams.builder().setDefaultTint(mHeaderIconTint).setForceTinting(true).build());
+
+ if (action.getType() == Action.TYPE_APP_ICON) {
+ // Special treatment for app icon as it is un-clickable and un-focusable.
+ mHeaderButtonContainer.setFocusable(false);
+ mHeaderButtonContainer.setClickable(false);
+ } else if (action.getType() == Action.TYPE_BACK) {
+ // Special treatment for back as it doesn't have a custom click listener
+ mHeaderButtonContainer.setOnClickListener(
+ view -> templateContext.getBackPressedHandler().onBackPressed());
+ mHeaderButtonContainer.setFocusable(true);
+ mHeaderButtonContainer.setClickable(true);
+ } else {
+ OnClickDelegate onClickDelegate = action.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ mHeaderButtonContainer.setOnClickListener(
+ view -> CommonUtils.dispatchClick(templateContext, onClickDelegate));
+ mHeaderButtonContainer.setFocusable(true);
+ mHeaderButtonContainer.setClickable(true);
+ } else {
+ mHeaderButtonContainer.setFocusable(false);
+ mHeaderButtonContainer.setClickable(false);
+ }
+ }
+
+ return true;
+ }
+
+ private boolean updateRefreshButton(
+ TemplateContext templateContext, @Nullable OnContentRefreshDelegate contentRefreshDelegate) {
+ if (!templateContext.getCarHostConfig().isPoiContentRefreshEnabled()
+ || contentRefreshDelegate == null) {
+ mRefreshButtonContainer.setVisibility(GONE);
+ mRefreshButtonContainer.setFocusable(false);
+ mRefreshButtonContainer.setClickable(false);
+ return false;
+ }
+
+ CarIcon icon =
+ new CarIcon.Builder(
+ IconCompat.createWithResource(
+ getContext(), templateContext.getHostResourceIds().getRefreshIconDrawable()))
+ .build();
+ ImageUtils.setImageSrc(
+ templateContext,
+ icon,
+ mRefreshButtonIcon,
+ ImageViewParams.builder().setDefaultTint(mHeaderIconTint).setForceTinting(true).build());
+
+ mRefreshButtonContainer.setVisibility(VISIBLE);
+ mRefreshButtonContainer.setFocusable(true);
+ mRefreshButtonContainer.setClickable(true);
+ mRefreshButtonContainer.setOnClickListener(
+ view -> {
+ TemplateValidator templateValidator =
+ templateContext.getAppHostService(TemplateValidator.class);
+ if (templateValidator != null) {
+ templateValidator.setIsNextTemplateContentRefreshIfSameType(true);
+ }
+ templateContext.getAppDispatcher().dispatchContentRefreshRequest(contentRefreshDelegate);
+ });
+
+ return true;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java
new file mode 100644
index 0000000..a5db03b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ClickableSpanTextContainer.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import com.android.car.libraries.templates.host.R;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A container for a {@link TextView} that allows moving focus between clickable spans.
+ *
+ * <p>Only the vertical focus movement are supported.
+ */
+public class ClickableSpanTextContainer extends FrameLayout implements OnGlobalFocusChangeListener {
+ /** An invalid span index. */
+ private static final int INVALID_INDEX = -1;
+
+ private final ForegroundColorSpan mHighlightForegroundSpan;
+ private final BackgroundColorSpan mHighlightBackgroundSpan;
+
+ private final List<ClickableSpan> mClickableSpans = new ArrayList<>();
+ private int mSelectedSpanIndex = INVALID_INDEX;
+
+ /** Indicates whether the focus moved in between spans. */
+ private boolean mMovedClickableSpanFocus = false;
+
+ private TextView mClickableSpanCarTextView;
+
+ public ClickableSpanTextContainer(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public ClickableSpanTextContainer(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ClickableSpanTextContainer(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public ClickableSpanTextContainer(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateClickableSpanHighlightForegroundColor,
+ R.attr.templateClickableSpanHighlightBackgroundColor,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ int highlightForegroundColor = ta.getColor(0, Color.TRANSPARENT);
+ int highlightBackgroundColor = ta.getColor(1, Color.TRANSPARENT);
+ ta.recycle();
+
+ mHighlightForegroundSpan = new ForegroundColorSpan(highlightForegroundColor);
+ mHighlightBackgroundSpan = new BackgroundColorSpan(highlightBackgroundColor);
+ }
+
+ /** Sets the given text for the wrapped text view. */
+ public void setText(@Nullable CharSequence text) {
+ mClickableSpanCarTextView.setText(text);
+
+ mClickableSpans.clear();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ Collections.addAll(
+ mClickableSpans, spannable.getSpans(0, text.length(), ClickableSpan.class));
+ }
+ }
+
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (newFocus == null) {
+ // The focus left the window, remove the link highlight.
+ removeLinkHighlight();
+ return;
+ }
+
+ if (oldFocus == null) {
+ // The focus came back to the window, show the link highlight again if applicable.
+ updateSelectedSpan();
+ return;
+ }
+
+ int focusDirection = focusDirection(oldFocus, newFocus);
+ if (newFocus.equals(mClickableSpanCarTextView)) {
+ // The focus moved from another view to this view. Determine which clickable is
+ // selected.
+ if (mMovedClickableSpanFocus) {
+ // The user moved focus, but we brought the focus back to this view after changing
+ // the
+ // selected clickable span index to simulate the clickable span focus movement. Do
+ // not reset
+ // the index.
+ mMovedClickableSpanFocus = false;
+ } else {
+ if (focusDirection == FOCUS_UP) {
+ // The focus moved up. Select the last span in the list.
+ mSelectedSpanIndex =
+ mClickableSpans.isEmpty() ? INVALID_INDEX : mClickableSpans.size() - 1;
+ } else {
+ // The focus moved down. Select the first span in the list.
+ mSelectedSpanIndex = mClickableSpans.isEmpty() ? INVALID_INDEX : 0;
+ }
+ }
+
+ updateSelectedSpan();
+ } else if (oldFocus.equals(mClickableSpanCarTextView)) {
+ // The focus moved from this view to another view.
+ if (mSelectedSpanIndex != INVALID_INDEX) {
+ if (focusDirection == FOCUS_UP && mSelectedSpanIndex > 0) {
+ // Focus moved up within the span list, select an earlier span and focus on the
+ // text view
+ // again.
+ mSelectedSpanIndex--;
+ mMovedClickableSpanFocus = true;
+ mClickableSpanCarTextView.requestFocus();
+ } else if (focusDirection == FOCUS_DOWN
+ && mSelectedSpanIndex < mClickableSpans.size() - 1) {
+ // Focus moved down within the span list, select a later span and focus on the
+ // text view
+ // again.
+ mSelectedSpanIndex++;
+ mMovedClickableSpanFocus = true;
+ mClickableSpanCarTextView.requestFocus();
+ } else {
+ // Focus moved out of the span list, remove the selected span.
+ mSelectedSpanIndex = INVALID_INDEX;
+ updateSelectedSpan();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mClickableSpanCarTextView = findViewById(R.id.clickable_span_text_view);
+
+ // Enable clickable spans here, setting these in the resourc1e file does not work
+ mClickableSpanCarTextView.setMovementMethod(LinkMovementMethod.getInstance());
+ mClickableSpanCarTextView.setHighlightColor(Color.TRANSPARENT);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ getViewTreeObserver().addOnGlobalFocusChangeListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ getViewTreeObserver().removeOnGlobalFocusChangeListener(this);
+
+ super.onDetachedFromWindow();
+ }
+
+ /** Updates the selected clickable span. */
+ private void updateSelectedSpan() {
+ Spannable spannable = (Spannable) mClickableSpanCarTextView.getText();
+ if (spannable == null) {
+ return;
+ }
+
+ if (mSelectedSpanIndex == INVALID_INDEX) {
+ Selection.removeSelection(spannable);
+ spannable.removeSpan(mHighlightForegroundSpan);
+ spannable.removeSpan(mHighlightBackgroundSpan);
+ } else {
+ // highlight the selected span.
+ ClickableSpan span = mClickableSpans.get(mSelectedSpanIndex);
+ int spanStart = spannable.getSpanStart(span);
+ int spanEnd = spannable.getSpanEnd(span);
+ Selection.setSelection(spannable, spanStart, spanEnd);
+
+ spannable.setSpan(
+ mHighlightForegroundSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(
+ mHighlightBackgroundSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ /**
+ * Removes the link highlight from the selected span.
+ *
+ * <p>This method only removes the visual highlight, but not the selected span.
+ */
+ private void removeLinkHighlight() {
+ Spannable spannable = (Spannable) mClickableSpanCarTextView.getText();
+ if (spannable != null) {
+ spannable.removeSpan(mHighlightForegroundSpan);
+ spannable.removeSpan(mHighlightBackgroundSpan);
+ }
+ }
+
+ /**
+ * Determines which direction the focus moved from the old to new focus.
+ *
+ * <p>This method only determines the vertical focus direction.
+ */
+ private static int focusDirection(View oldFocus, View newFocus) {
+ int[] oldLocation = getViewLocationInWindow(oldFocus);
+ int[] newLocation = getViewLocationInWindow(newFocus);
+ int oldLocationY = oldLocation[1];
+ int newLocationY = newLocation[1];
+ return oldLocationY > newLocationY ? FOCUS_UP : FOCUS_DOWN;
+ }
+
+ private static int[] getViewLocationInWindow(@Nullable View view) {
+ int[] location = new int[2];
+ if (view != null) {
+ view.getLocationInWindow(location);
+ }
+ return location;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java
new file mode 100644
index 0000000..0e731e7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ContentView.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_HIDE_ROW_DIVIDERS;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.templates.host.R;
+
+/** A view that displays content such as a list, a pane, or an error screen. */
+public class ContentView extends LinearLayout {
+ private ViewGroup mViewGroup;
+
+ public ContentView(Context context) {
+ this(context, null);
+ }
+
+ public ContentView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ContentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings({"ResourceType", "method.invocation.invalid", "argument.type.incompatible"})
+ public ContentView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mViewGroup = findViewById(R.id.container);
+ }
+
+ /** Sets a {@link GridWrapper} as the content for this view. */
+ public void setGridContent(TemplateContext templateContext, GridWrapper gridWrapper) {
+ View view = mViewGroup.getChildCount() > 0 ? mViewGroup.getChildAt(0) : null;
+ if (view != null) {
+ if (!(view instanceof GridView)) {
+ removeView(view);
+ view = null;
+ }
+ }
+
+ if (view == null) {
+ view = LayoutInflater.from(getContext()).inflate(R.layout.grid_view, mViewGroup, false);
+ mViewGroup.addView(view);
+ }
+
+ ((GridView) view).setGrid(templateContext, gridWrapper);
+ }
+
+ /** Sets a {@link RowListWrapper} as the content for this view. */
+ public void setRowListContent(TemplateContext templateContext, RowListWrapper rowList) {
+ View view = mViewGroup.getChildCount() > 0 ? mViewGroup.getChildAt(0) : null;
+ if (view != null) {
+ if (!(view instanceof RowListView)) {
+ removeView(view);
+ view = null;
+ }
+ }
+
+ if (view == null) {
+ boolean hasRowDividers = (rowList.getListFlags() & LIST_FLAGS_HIDE_ROW_DIVIDERS) == 0;
+ int layout =
+ rowList.isHalfList()
+ ? R.layout.half_list_view
+ : hasRowDividers ? R.layout.full_list_view : R.layout.full_list_no_divider_view;
+ view = LayoutInflater.from(getContext()).inflate(layout, mViewGroup, false);
+ mViewGroup.addView(view);
+ }
+
+ ((RowListView) view).setRowList(templateContext, rowList);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java
new file mode 100644
index 0000000..23b4211
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/FabView.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static androidx.car.app.model.Action.TYPE_BACK;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.OnClickDelegate;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.template.view.model.ActionWrapper;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** Displays an {@link Action} as a FAB. */
+// TODO(b/158142806): Merge with ActionButtonView
+public class FabView extends LinearLayout {
+ private Object mAction;
+ private final int mMinWidthWithText;
+ private final int mMinWidthWithoutText;
+ @ColorInt private final int mContentColor;
+ @ColorInt private final int mBackgroundColorLight;
+ @ColorInt private final int mBackgroundColorDark;
+
+ public FabView(Context context) {
+ this(context, null);
+ }
+
+ public FabView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FabView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public FabView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateActionWithTextMinWidth,
+ R.attr.templateActionWithoutTextMinWidth,
+ R.attr.templateActionStripFabBackgroundColorLight,
+ R.attr.templateActionStripFabBackgroundColorDark
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mMinWidthWithText = ta.getDimensionPixelSize(0, 0);
+ mMinWidthWithoutText = ta.getDimensionPixelSize(1, 0);
+ mBackgroundColorLight = ta.getColor(2, 0);
+ mBackgroundColorDark = ta.getColor(3, 0);
+ ta.recycle();
+
+ ta = context.obtainStyledAttributes(defStyleRes, new int[] {R.attr.fabDefaultContentColor});
+ mContentColor = ta.getColor(0, -1);
+ ta.recycle();
+ }
+
+ /** Returns whether the button contains a text or not. */
+ public boolean hasTitle() {
+ CarUiTextView textView = findViewById(R.id.action_text);
+ return textView != null && !TextUtils.isEmpty(textView.getText());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public Object getAction() {
+ return mAction;
+ }
+
+ /** Updates the view from based on the input {@code action}. */
+ public void setAction(TemplateContext templateContext, ActionWrapper actionWrapper) {
+ removeAllViews();
+ Action action = actionWrapper.get();
+ mAction = action;
+
+ CharSequence title = CarTextUtils.toCharSequenceOrEmpty(templateContext, action.getTitle());
+ CarIcon icon = ImageUtils.getIconFromAction(action);
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+
+ boolean hasTitle = title.length() > 0;
+ setMinimumWidth(hasTitle ? mMinWidthWithText : mMinWidthWithoutText);
+
+ if (icon != null && hasTitle) {
+ inflater.inflate(R.layout.fab_view_icon_text, this);
+ } else if (hasTitle) {
+ inflater.inflate(R.layout.fab_view_text, this);
+ } else {
+ inflater.inflate(R.layout.action_button_view_icon, this);
+ }
+
+ if (icon != null) {
+ @ColorInt
+ int backgroundColor =
+ CommonUtils.isDarkMode(templateContext) ? mBackgroundColorDark : mBackgroundColorLight;
+ ImageViewParams imageViewParams =
+ ImageViewParams.builder()
+ .setDefaultTint(mContentColor)
+ .setForceTinting(true)
+ .setBackgroundColor(backgroundColor)
+ .build();
+
+ ImageView iconView = findViewById(R.id.action_icon);
+ ImageUtils.setImageSrc(templateContext, icon, iconView, imageViewParams);
+ }
+
+ // Add the text view.
+ if (hasTitle) {
+ CarUiTextView carUiTextView = findViewById(R.id.action_text);
+ carUiTextView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, action.getTitle(), carUiTextView.getMaxLines()));
+ carUiTextView.setTextColor(mContentColor);
+ }
+
+ // Update the click listener, if one is set.
+ if (action.getType() == TYPE_BACK) {
+ setOnClickListener(v -> templateContext.getBackPressedHandler().onBackPressed());
+ } else {
+ OnClickDelegate onClickDelegate = action.getOnClickDelegate();
+ ActionWrapper.OnClickListener hostListener = actionWrapper.getOnClickListener();
+ if (onClickDelegate != null || hostListener != null) {
+ setOnClickListener(
+ v -> {
+ if (hostListener != null) {
+ hostListener.onClick();
+ }
+
+ ViewUtils.logCarAppTelemetry(templateContext, UiAction.ACTION_STRIP_FAB_CLICKED);
+ if (onClickDelegate != null) {
+ CommonUtils.dispatchClick(templateContext, onClickDelegate);
+ }
+ });
+ } else {
+ setOnClickListener(null);
+ }
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java
new file mode 100644
index 0000000..365faae
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.lang.Math.min;
+
+import android.content.Context;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.constraints.ConstraintManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/** A grid adapter for {@link GridItemWrapper}s. */
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public class GridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+ implements CarUiRecyclerView.ItemCap {
+
+ private final Context mContext;
+ private final int mItemsPerRow;
+ private List<GridRowWrapper> mRowWrappers;
+ private List<GridItemWrapper> mItemWrappers;
+
+ private TemplateContext mTemplateContext;
+ private int mMaxItemCount;
+
+ static GridAdapter create(Context context, int itemsPerRow) {
+ return new GridAdapter(context, itemsPerRow);
+ }
+
+ void setGridItems(TemplateContext templateContext, List<GridItemWrapper> gridItemWrappers) {
+ mTemplateContext = templateContext;
+ mMaxItemCount =
+ mTemplateContext
+ .getConstraintsProvider()
+ .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);
+ mItemWrappers = gridItemWrappers;
+ mRowWrappers = GridRowWrapper.create(gridItemWrappers, mItemsPerRow);
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ return new RecyclerView.ViewHolder(
+ LayoutInflater.from(mContext).inflate(R.layout.grid_item_view, viewGroup, false)) {};
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, int index) {
+ GridItemWrapper gridItemWrapper = mItemWrappers.get(index);
+ GridRowWrapper gridRowWrapper = findGridRowWrapperForItemAt(index);
+ ((GridItemView) viewHolder.itemView)
+ .setGridItem(
+ mTemplateContext,
+ gridItemWrapper,
+ gridRowWrapper.hasGridItemsWithTitle(),
+ gridRowWrapper.hasGridItemsWithText());
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mMaxItemCount == CarUiRecyclerView.ItemCap.UNLIMITED) {
+ return mItemWrappers.size();
+ } else {
+ return min(mItemWrappers.size(), mMaxItemCount);
+ }
+ }
+
+ @Override
+ public void setMaxItems(int maxItems) {
+ TemplateContext templateContext = mTemplateContext;
+ if (templateContext == null) {
+ return;
+ }
+
+ int gridMaxLength =
+ templateContext
+ .getConstraintsProvider()
+ .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);
+ if (maxItems == CarUiRecyclerView.ItemCap.UNLIMITED) {
+ mMaxItemCount = gridMaxLength;
+ } else {
+ mMaxItemCount = min(maxItems, gridMaxLength);
+ }
+ }
+
+ @VisibleForTesting
+ public List<GridRowWrapper> getRowWrappers() {
+ return mRowWrappers;
+ }
+
+ @VisibleForTesting
+ public List<GridItemWrapper> getItemWrappers() {
+ return mItemWrappers;
+ }
+
+ /** Returns the {@link GridRowWrapper} associated with the item at given index. */
+ private GridRowWrapper findGridRowWrapperForItemAt(int index) {
+ int currentIndex = index;
+ for (GridRowWrapper gridRowWrapper : mRowWrappers) {
+ int rowItemsCount = gridRowWrapper.getGridRowItems().size();
+ if (currentIndex < rowItemsCount) {
+ return gridRowWrapper;
+ }
+ currentIndex -= rowItemsCount;
+ }
+
+ throw new IndexOutOfBoundsException(
+ String.format("index = %d >= %d = count", index, getItemCount()));
+ }
+
+ private GridAdapter(Context context, int itemsPerRow) {
+ mContext = context;
+ mItemWrappers = ImmutableList.of();
+ mRowWrappers = ImmutableList.of();
+ mItemsPerRow = itemsPerRow;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java
new file mode 100644
index 0000000..d03f278
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemView.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.OnClickDelegate;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.SelectionGroup;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** A view that can display a {@link GridItem} model. */
+public class GridItemView extends LinearLayout {
+ private static final int[] STATE_INACTIVE_FOCUS = {R.attr.templateFocusStateInactive};
+
+ /** Text parameters for secondary text in a grid item. */
+ private static final CarTextParams TEXT_PARAMS_SECONDARY_TEXT =
+ CarTextParams.builder()
+ .setColorSpanConstraints(CarColorConstraints.STANDARD_ONLY)
+ .setMaxImages(0)
+ .build();
+ /**
+ * Indicates whether or not this grid item has inactive focus.
+ *
+ * <p>The grid item has an inactive focus when it is not clickable.
+ */
+ private boolean mHasInactiveFocus;
+
+ private final int mLargeImageSizeMin;
+ private final int mLargeImageSizeMax;
+ @ColorInt private final int mDefaultIconTint;
+ @ColorInt private final int mBackgroundColor;
+ private final int mHorizontalTextBottomPadding;
+ private final Drawable mGridItemBackground;
+
+ private LinearLayout mImageContainer;
+ private LinearLayout mTextContainer;
+ private CarUiTextView mTitleView;
+ private CarUiTextView mTextview;
+ private ImageView mImageView;
+ private ProgressBar mProgressBar;
+ private int mTextTopPadding;
+ private int mTextBottomPadding;
+
+ public GridItemView(Context context) {
+ this(context, null);
+ }
+
+ public GridItemView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @SuppressWarnings("nullness:assignment")
+ public GridItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateLargeImageSizeMin,
+ R.attr.templateLargeImageSizeMax,
+ R.attr.templateGridItemDefaultIconTint,
+ R.attr.templateGridItemTextBottomPadding,
+ R.attr.templateGridItemBackground,
+ R.attr.templateGridItemBackgroundColor,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mLargeImageSizeMin = ta.getDimensionPixelSize(0, 0);
+ mLargeImageSizeMax = ta.getDimensionPixelSize(1, Integer.MAX_VALUE);
+ mDefaultIconTint = ta.getColor(2, 0);
+ mHorizontalTextBottomPadding = ta.getDimensionPixelSize(3, 0);
+ mGridItemBackground = ta.getDrawable(4);
+ mBackgroundColor = ta.getColor(5, 0);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageContainer = findViewById(R.id.grid_item_image_container);
+ mTextContainer = findViewById(R.id.grid_item_text_container);
+ mTitleView = findViewById(R.id.grid_item_title);
+ mTextview = findViewById(R.id.grid_item_text);
+ mImageView = findViewById(R.id.grid_item_image);
+ mProgressBar = findViewById(R.id.grid_item_progress_bar);
+
+ // Cache TextContainer padding since the padding is updated every time {@link #setGridItem} is
+ // called.
+ mTextTopPadding = mTextContainer.getPaddingTop();
+ mTextBottomPadding = mTextContainer.getPaddingBottom();
+
+ ViewUtils.enforceViewSizeLimit(mImageContainer, mLargeImageSizeMin, mLargeImageSizeMax);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mHasInactiveFocus) {
+ // We are going to add 1 extra state.
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ mergeDrawableStates(drawableState, STATE_INACTIVE_FOCUS);
+ return drawableState;
+ } else {
+ return super.onCreateDrawableState(extraSpace);
+ }
+ }
+
+ /** Updates the view with the given {@link GridItemWrapper}. */
+ public void setGridItem(
+ TemplateContext templateContext,
+ GridItemWrapper gridItemWrapper,
+ boolean shouldShowTitle,
+ boolean shouldShowText) {
+ GridItem gridItem = gridItemWrapper.getGridItem();
+
+ L.v(LogTags.TEMPLATE, "Setting grid item view with grid item: %s", gridItem);
+
+ // Unset any click/focus listeners tied to the previous content. New ones will be added
+ // below.
+ setOnClickListener(null);
+ setOnFocusChangeListener(null);
+
+ updateTextView(
+ templateContext, mTitleView, gridItem.getTitle(), CarTextParams.DEFAULT, shouldShowTitle);
+
+ // Allow standard colors for the secondary text only if the color contrast check passed.
+ boolean colorContrastCheckPassed =
+ checkColorContrast(templateContext, gridItem, mBackgroundColor);
+ CarTextParams secondaryTextParams =
+ colorContrastCheckPassed ? TEXT_PARAMS_SECONDARY_TEXT : CarTextParams.DEFAULT;
+ updateTextView(
+ templateContext, mTextview, gridItem.getText(), secondaryTextParams, shouldShowText);
+
+ mTextContainer.setPadding(
+ 0,
+ mTextTopPadding,
+ 0,
+
+ // If there is not secondary text to be shown, add an extra padding at the bottom of
+ // the
+ // container. This makes it so that there's more separation between rows when
+ // there's only
+ // a title (for example, in the system's wallpaper picker), while we use up some
+ // more
+ // of the vertical space for text when there is a secondary line.
+ mTextBottomPadding + (shouldShowText ? 0 : mHorizontalTextBottomPadding));
+
+ SelectionGroup selectionGroup = gridItemWrapper.getSelectionGroup();
+ OnClickDelegate onClickDelegate = gridItem.getOnClickDelegate();
+
+ boolean isLoading = gridItem.isLoading();
+
+ // The grid item is clickable iff...
+ boolean isClickable =
+ // ...it is not in the loading state, and
+ !isLoading
+ && (
+ // ...it has a click listener coming from the client
+ onClickDelegate != null
+ // ...is selectable
+ || selectionGroup != null);
+
+ // Show either the image or the loading spinner.
+ mProgressBar.setVisibility(isLoading ? VISIBLE : GONE);
+ mImageView.setVisibility(isLoading ? GONE : VISIBLE);
+ if (!isLoading) {
+ int imageType = gridItem.getImageType();
+
+ // Show the grid item image.
+ CarIcon image = gridItem.getImage();
+ ImageUtils.setImageSrc(
+ templateContext,
+ image,
+ mImageView,
+ ImageViewParams.builder()
+ .setDefaultTint(mDefaultIconTint)
+ .setForceTinting(imageType == GridItem.IMAGE_TYPE_ICON)
+ .setBackgroundColor(mBackgroundColor)
+ .setIgnoreAppTint(!colorContrastCheckPassed)
+ .build());
+
+ // Set the onClickListener on the grid item iff...
+ if (onClickDelegate != null) {
+ // ...it has a click listener from the client. Dispatch click event to the
+ // onClickListener.
+ setOnClickListener(
+ v -> {
+ CommonUtils.dispatchClick(templateContext, onClickDelegate);
+ });
+ } else if (selectionGroup != null) {
+ // ...it is part of a selection group. Dispatch a selection change event to the
+ // selection
+ // group's onSelectedListener.
+ setOnClickListener(
+ v -> {
+ int currentSelectionIndex = selectionGroup.getSelectedIndex();
+ int newIndex = gridItemWrapper.getGridItemIndex();
+
+ if (currentSelectionIndex != newIndex) {
+ selectionGroup.setSelectedIndex(newIndex);
+ }
+
+ // Dispatch the selection callbacks.
+ // Note the selection event is dispatched regardless of selection index
+ // actually
+ // changing.
+ templateContext
+ .getAppDispatcher()
+ .dispatchSelected(
+ selectionGroup.getOnSelectedDelegate(),
+ selectionGroup.getRelativeIndex(newIndex));
+ });
+ }
+ }
+
+ setClickable(isClickable);
+ setBackground(mGridItemBackground);
+ setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ setInactiveFocus(!isClickable);
+ }
+
+ /** Checks the color contrast between contents of the given grid item and the background color. */
+ private static boolean checkColorContrast(
+ TemplateContext templateContext, GridItem gridItem, @ColorInt int backgroundColor) {
+ // Only the secondary text can be colored, so check it
+ CarText secondaryText = gridItem.getText();
+ if (secondaryText != null) {
+ if (!CarTextUtils.checkColorContrast(templateContext, secondaryText, backgroundColor)) {
+ return false;
+ }
+ }
+
+ CarIcon image = gridItem.getImage();
+ if (image == null) {
+ return true;
+ }
+ CarColor tint = image.getTint();
+ if (tint == null) {
+ return true;
+ }
+
+ return CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor);
+ }
+
+ private static void updateTextView(
+ TemplateContext templateContext,
+ CarUiTextView carUiTextView,
+ @Nullable CarText text,
+ CarTextParams textParams,
+ boolean shouldShowTextView) {
+ // The visibility of the text view inside a grid view depends on all the grid items in the
+ // row. It's possible that this particular grid item doesn't have a valid title or text, but
+ // another grid item in the row may have a title. We need to have consistent height and
+ // focus states for all the grid items in a gird row. Using information provided by the grid
+ // row container to decide the visibility of text view's inside a grid item.
+ carUiTextView.setVisibility(shouldShowTextView ? VISIBLE : GONE);
+
+ // With the "normal" buffer type, the text view sets a spanned text with immutable spans.
+ // BufferType.SPANNABLE allows mutable spans, but causes issues with ellipsized texts
+ // (See b/157754626).
+ carUiTextView.setText(
+ CarUiTextUtils.fromCarText(templateContext, text, textParams, carUiTextView.getMaxLines()));
+ }
+
+ /** @see #mHasInactiveFocus */
+ private void setInactiveFocus(boolean hasInactiveFocus) {
+ if (mHasInactiveFocus != hasInactiveFocus) {
+ mHasInactiveFocus = hasInactiveFocus;
+
+ // Refresh the drawable state so that it includes the inactive focus state.
+ refreshDrawableState();
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java
new file mode 100644
index 0000000..8c40bad
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridItemWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.GridItem;
+import com.android.car.libraries.apphost.template.view.model.SelectionGroup;
+
+/** A host side wrapper for {@link GridItem}. */
+public class GridItemWrapper {
+ private final GridItem mGridItem;
+ private final int mGridItemIndex;
+
+ /**
+ * The selection group this grid item belongs to, or {@code null} if the grid item does not belong
+ * to one.
+ *
+ * <p>Selection groups are used to establish mutually-exclusive scopes of grid item selection.
+ */
+ @Nullable private final SelectionGroup mSelectionGroup;
+
+ /** Returns a {@link Builder} that wraps a grid item with the provided index. */
+ public static Builder wrap(
+ GridItem gridItem, int gridItemIndex, @Nullable SelectionGroup selectionGroup) {
+ Builder builder = new Builder(gridItem, gridItemIndex);
+ if (selectionGroup != null) {
+ builder.setSelectionGroup(selectionGroup);
+ }
+ return builder;
+ }
+
+ private GridItemWrapper(Builder builder) {
+ mGridItem = builder.mGridItem;
+ mGridItemIndex = builder.mGridItemIndex;
+ mSelectionGroup = builder.mSelectionGroup;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + mGridItem + ", group: " + mSelectionGroup + "]";
+ }
+
+ /** Returns the actual {@link GridItem} object that this instance is wrapping. */
+ public GridItem getGridItem() {
+ return mGridItem;
+ }
+
+ /** Returns the absolute index of the grid item in the flattened container list. */
+ public int getGridItemIndex() {
+ return mGridItemIndex;
+ }
+
+ @Nullable
+ SelectionGroup getSelectionGroup() {
+ return mSelectionGroup;
+ }
+
+ /** The builder class for {@link GridItemWrapper}. */
+ public static class Builder {
+ private final GridItem mGridItem;
+ private final int mGridItemIndex;
+ @Nullable private SelectionGroup mSelectionGroup;
+
+ private Builder(GridItem gridItem, int gridItemIndex) {
+ mGridItem = gridItem;
+ mGridItemIndex = gridItemIndex;
+ }
+
+ /**
+ * Sets the selection group this grid item belongs to, or {@code null} if the grid item does not
+ * belong to one.
+ */
+ public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+ mSelectionGroup = selectionGroup;
+ return this;
+ }
+
+ /** Build the {@link GridItemWrapper}. */
+ public GridItemWrapper build() {
+ return new GridItemWrapper(this);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java
new file mode 100644
index 0000000..48a9851
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridRowWrapper.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.lang.Math.min;
+
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A host side wrapper for a list of {@link GridItem}s that represent a row of the grid. */
+public class GridRowWrapper {
+ private final List<GridItemWrapper> mGridRowItems;
+ private final int mGridRowIndex;
+ private final int mMaxColsPerGridRow;
+
+ private GridRowWrapper(
+ List<GridItemWrapper> gridRowItems, int gridRowIndex, int maxColsPerGridRow) {
+ mGridRowItems = gridRowItems;
+ mGridRowIndex = gridRowIndex;
+ mMaxColsPerGridRow = maxColsPerGridRow;
+ }
+
+ public List<GridItemWrapper> getGridRowItems() {
+ return mGridRowItems;
+ }
+
+ public int getGridRowIndex() {
+ return mGridRowIndex;
+ }
+
+ public int getMaxColsPerGridRow() {
+ return mMaxColsPerGridRow;
+ }
+
+ /**
+ * Creates a list of {@link GridRowWrapper}s from the provided list of {@link GridItemWrapper}s
+ * based on the {@code numberOfColumns}.
+ */
+ public static List<GridRowWrapper> create(
+ List<GridItemWrapper> gridItemWrappers, int numberOfColumns) {
+ List<GridRowWrapper> gridRowWrappers = new ArrayList<>();
+
+ int itemCount = gridItemWrappers.size();
+ int gridRowIndex = 0;
+ int beginIndex = 0;
+ while (beginIndex < itemCount) {
+ gridRowWrappers.add(
+ new GridRowWrapper(
+ gridItemWrappers.subList(beginIndex, min(itemCount, beginIndex + numberOfColumns)),
+ gridRowIndex,
+ numberOfColumns));
+ gridRowIndex++;
+ beginIndex += numberOfColumns;
+ }
+
+ return gridRowWrappers;
+ }
+
+ /**
+ * Returns {@code true} if any of the {@link GridItem}s consisting this grid row has a title set.
+ */
+ public boolean hasGridItemsWithTitle() {
+ for (GridItemWrapper gridItemWrapper : mGridRowItems) {
+ CarText carText = gridItemWrapper.getGridItem().getTitle();
+ if (carText != null && !carText.isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if any of the {@link GridItem}s consisting this grid row has a secondary
+ * line of text set.
+ */
+ public boolean hasGridItemsWithText() {
+ for (GridItemWrapper gridItemWrapper : mGridRowItems) {
+ CarText carText = gridItemWrapper.getGridItem().getText();
+ if (carText != null && !carText.isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java
new file mode 100644
index 0000000..5a2890a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridView.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.recyclerview.CarUiLayoutStyle;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView.CarUiRecyclerViewLayout;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.Objects;
+
+/** A view that can render a grid of {@link GridItem}s wrapped inside a {@link GridWrapper}. */
+public class GridView extends FrameLayout {
+ private final AdapterDataObserver mAdapterDataObserver =
+ new AdapterDataObserver() {
+ // call to update() not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ update();
+ }
+ };
+
+ /** The number of items in a grid row. */
+ private final int mItemsPerRow;
+
+ private GridAdapter mGridRowAdapter;
+
+ private ViewGroup mProgressContainer;
+ private CarUiTextView mEmptyListTextView;
+ private CarUiRecyclerView mRecyclerView;
+ private RowVisibilityObserver mRowVisibilityObserver;
+ private boolean mIsLoading;
+
+ public GridView(Context context) {
+ this(context, null);
+ }
+
+ public GridView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GridView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"})
+ public GridView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateGridItemsPerRow,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mItemsPerRow = ta.getInteger(0, 0);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mProgressContainer = findViewById(R.id.progress_container);
+ mEmptyListTextView = findViewById(R.id.list_no_items_text);
+ mRecyclerView = findViewById(R.id.grid_paged_list_view);
+ mRecyclerView.setLayoutStyle(
+ new CarUiLayoutStyle() {
+ @Override
+ public int getSpanCount() {
+ return mItemsPerRow;
+ }
+
+ @Override
+ public int getLayoutType() {
+ return CarUiRecyclerViewLayout.GRID;
+ }
+
+ @Override
+ public int getOrientation() {
+ return CarUiLayoutStyle.VERTICAL;
+ }
+
+ @Override
+ public boolean getReverseLayout() {
+ return false;
+ }
+
+ @Override
+ public int getSize() {
+ return CarUiRecyclerView.SIZE_LARGE;
+ }
+ });
+ mRowVisibilityObserver = RowVisibilityObserver.create(Objects.requireNonNull(mRecyclerView));
+ mGridRowAdapter = GridAdapter.create(getContext(), mItemsPerRow);
+ mRecyclerView.setAdapter(mGridRowAdapter);
+ mGridRowAdapter.registerAdapterDataObserver(mAdapterDataObserver);
+ update();
+ }
+
+ void setGrid(TemplateContext templateContext, GridWrapper gridWrapper) {
+ boolean isLoading = gridWrapper.isLoading();
+ if (mIsLoading != isLoading) {
+ // Trigger a visibility update if the loading state has changed.
+ mIsLoading = isLoading;
+ update();
+
+ if (mIsLoading) {
+ // Do not update the GridPagedListView/GridRowAdapter, as we want to maintain the
+ // grid items list size during the loading phase until the new content is populated.
+ return;
+ }
+ }
+
+ CarText emptyListCarText = gridWrapper.getEmptyListText();
+ CharSequence emptyText;
+ if (emptyListCarText != null && !emptyListCarText.isEmpty()) {
+ emptyText =
+ CarTextUtils.toCharSequenceOrEmpty(templateContext, gridWrapper.getEmptyListText());
+ } else {
+ emptyText =
+ templateContext.getText(
+ templateContext.getHostResourceIds().getTemplateListNoItemsText());
+ }
+ mEmptyListTextView.setText(
+ CarUiTextUtils.fromCharSequence(
+ templateContext, emptyText, mEmptyListTextView.getMaxLines()));
+ mRowVisibilityObserver.setOnItemVisibilityChangedListener(
+ (startIndexInclusive, endIndexExclusive) -> {
+ OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate =
+ gridWrapper.getOnItemVisibilityChangedDelegate();
+ if (onItemVisibilityChangedDelegate != null) {
+ templateContext
+ .getAppDispatcher()
+ .dispatchItemVisibilityChanged(
+ onItemVisibilityChangedDelegate, startIndexInclusive, endIndexExclusive);
+ }
+ });
+
+ mGridRowAdapter.setGridItems(templateContext, gridWrapper.getGridItemWrappers());
+
+ if (!gridWrapper.isRefresh()) {
+ mRecyclerView.scrollToPosition(0);
+ }
+
+ ViewUtils.logCarAppTelemetry(
+ templateContext, UiAction.GRID_ITEM_LIST_SIZE, gridWrapper.getGridItemWrappers().size());
+ }
+
+ private void update() {
+ boolean isLoading = mIsLoading;
+ if (isLoading) {
+ mProgressContainer.setVisibility(VISIBLE);
+
+ // Mark the content views as invisible so that the size of the container remains the
+ // same
+ // while the progress bar is showing.
+ mEmptyListTextView.setVisibility(INVISIBLE);
+ mRecyclerView.setVisibility(INVISIBLE);
+ return;
+ }
+
+ mProgressContainer.setVisibility(GONE);
+
+ // If the grid item list is empty, hide it and display a message instead.
+ boolean isEmpty = mGridRowAdapter.getItemCount() == 0;
+ if (isEmpty) {
+ mEmptyListTextView.setVisibility(VISIBLE);
+ mRecyclerView.setVisibility(GONE);
+
+ // When the empty list text view is displayed, show the focus ring by not clipping
+ // children.
+ setClipChildren(false);
+ mEmptyListTextView.setFocusable(true);
+ } else {
+ mEmptyListTextView.setVisibility(GONE);
+ mRecyclerView.setVisibility(VISIBLE);
+
+ // When the grid view is displayed, clip its rows that get out of the view boundary.
+ setClipChildren(true);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java
new file mode 100644
index 0000000..688e29c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/GridWrapper.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.Item;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.SelectionGroup;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/** A host side wrapper for {@link ItemList} that's part of a {@link GridTemplate}. */
+public class GridWrapper {
+ private final boolean mIsLoading;
+ private final boolean mIsRefresh;
+ @Nullable private final CarText mEmptyListText;
+ @Nullable private final OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+ private final List<GridItemWrapper> mGridItemWrappers;
+
+ /** Converts an {@link ItemList} into a {@link GridWrapper.Builder}. */
+ public static Builder wrap(@Nullable ItemList itemList) {
+ if (itemList == null) {
+ return new Builder();
+ }
+
+ List<Item> gridItems = itemList.getItems();
+ Builder builder =
+ new Builder()
+ .setGridItems(gridItems)
+ .setEmptyListText(itemList.getNoItemsMessage())
+ .setOnItemVisibilityChangedDelegate(itemList.getOnItemVisibilityChangedDelegate());
+
+ OnSelectedDelegate onSelectedDelegate = itemList.getOnSelectedDelegate();
+ if (onSelectedDelegate != null) {
+ builder.setSelectionGroup(
+ SelectionGroup.create(
+ 0, gridItems.size() - 1, itemList.getSelectedIndex(), onSelectedDelegate));
+ }
+
+ return builder;
+ }
+
+ private GridWrapper(Builder builder) {
+ mIsLoading = builder.mIsLoading;
+ mIsRefresh = builder.mIsRefresh;
+ mEmptyListText = builder.mEmptyListText;
+ mOnItemVisibilityChangedDelegate = builder.mOnItemVisibilityChangedDelegate;
+ mGridItemWrappers = buildGridItemWrappers(builder.mGridItems, builder.mSelectionGroup);
+ }
+
+ @Nullable
+ public OnItemVisibilityChangedDelegate getOnItemVisibilityChangedDelegate() {
+ return mOnItemVisibilityChangedDelegate;
+ }
+
+ public boolean isEmpty() {
+ return mGridItemWrappers.isEmpty();
+ }
+
+ @Nullable
+ CarText getEmptyListText() {
+ return mEmptyListText;
+ }
+
+ List<GridItemWrapper> getGridItemWrappers() {
+ return mGridItemWrappers;
+ }
+
+ boolean isLoading() {
+ return mIsLoading;
+ }
+
+ boolean isRefresh() {
+ return mIsRefresh;
+ }
+
+ /** Builds the {@link GridItemWrapper}s for a given list. */
+ private static ImmutableList<GridItemWrapper> buildGridItemWrappers(
+ @Nullable List<Item> gridItems, @Nullable SelectionGroup selectionGroup) {
+ if (gridItems == null || gridItems.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ int beginIndex = 0;
+ ImmutableList.Builder<GridItemWrapper> gridItemWrapperBuilder = new ImmutableList.Builder<>();
+ for (Item item : gridItems) {
+ if (!(item instanceof GridItem)) {
+ L.w(LogTags.TEMPLATE, "Item in list is not a GridItem, dropping item");
+ }
+ gridItemWrapperBuilder.add(
+ GridItemWrapper.wrap((GridItem) item, beginIndex, selectionGroup).build());
+ beginIndex++;
+ }
+
+ return gridItemWrapperBuilder.build();
+ }
+
+ /** The builder class for {@link GridWrapper}. */
+ public static class Builder {
+ @Nullable private List<Item> mGridItems;
+ @Nullable private OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+ private boolean mIsLoading;
+ private boolean mIsRefresh;
+ @Nullable private CarText mEmptyListText;
+ @Nullable private SelectionGroup mSelectionGroup;
+
+ private Builder() {
+ mGridItems = null;
+ }
+
+ /** Sets the grid items in the {@link Builder} */
+ public Builder setGridItems(List<Item> gridItems) {
+ mGridItems = gridItems;
+ return this;
+ }
+
+ /**
+ * Sets the {@link OnItemVisibilityChangedDelegate} which can receive callbacks when the
+ * visibility of a items changes.
+ *
+ * <p>If set to {@code null} it will clear the delegate and no callbacks will be received.
+ */
+ public Builder setOnItemVisibilityChangedDelegate(
+ @Nullable OnItemVisibilityChangedDelegate delegate) {
+ mOnItemVisibilityChangedDelegate = delegate;
+ return this;
+ }
+
+ /**
+ * Sets whether the list is loading.
+ *
+ * <p>If set to {@code true}, the UI shows a loading indicator and ignore any grid items added
+ * to the list. If set to {@code false}, the UI shows the actual grid item contents.
+ */
+ public Builder setIsLoading(boolean isLoading) {
+ mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets whether the grid is a refresh of the existing grid.
+ *
+ * <p>If set to {@code true}, the UI will not scroll to top, otherwise it will.
+ */
+ public Builder setIsRefresh(boolean isRefresh) {
+ mIsRefresh = isRefresh;
+ return this;
+ }
+
+ /** Sets the text to be displayed when there are no items in the list. */
+ public Builder setEmptyListText(@Nullable CarText emptyListText) {
+ mEmptyListText = emptyListText;
+ return this;
+ }
+
+ /**
+ * Sets the selection group these grid items belong to, or {@code null} if the grid items do not
+ * belong to one.
+ */
+ public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+ mSelectionGroup = selectionGroup;
+ return this;
+ }
+
+ /** Builds the {@link GridWrapper}. */
+ public GridWrapper build() {
+ return new GridWrapper(this);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java
new file mode 100644
index 0000000..56ec415
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/HeaderView.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.view.View;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarText;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.ui.core.CarUi;
+import com.android.car.ui.toolbar.ToolbarController;
+
+/** A view that displays the header for the templates. */
+public class HeaderView extends AbstractHeaderView {
+
+ private HeaderView(TemplateContext templateContext, ToolbarController toolbarController) {
+ super(templateContext, toolbarController);
+ }
+
+ /**
+ * Set or clear the content of the view.
+ *
+ * <p>If the {@code title} is {@code null} then the view is hidden.
+ */
+ public void setContent(
+ TemplateContext templateContext, @Nullable CarText title, @Nullable Action action) {
+ mToolbarController.setTitle(CarTextUtils.toCharSequenceOrEmpty(templateContext, title));
+ setAction(action);
+ }
+
+ /** Installs a {@link HeaderView} around the given container view. */
+ @SuppressWarnings("nullness:argument") // InsetsChangedListener is nullable.
+ public static HeaderView install(TemplateContext mTemplateContext, View container) {
+ ToolbarController toolbarController = CarUi.installBaseLayoutAround(container, null, true);
+ if (toolbarController == null) {
+ throw new NullPointerException("Toolbar Controller could not be created.");
+ }
+ return new HeaderView(mTemplateContext, toolbarController);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java
new file mode 100644
index 0000000..c8932d2
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/InputSignInView.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static android.text.InputType.TYPE_CLASS_NUMBER;
+import static android.text.InputType.TYPE_CLASS_PHONE;
+import static android.text.InputType.TYPE_CLASS_TEXT;
+import static android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD;
+import static android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+import static android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+import static android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD;
+import static androidx.car.app.model.signin.InputSignInMethod.INPUT_TYPE_PASSWORD;
+import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_DEFAULT;
+import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_EMAIL;
+import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_NUMBER;
+import static androidx.car.app.model.signin.InputSignInMethod.KEYBOARD_PHONE;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.LinearLayout;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.InputCallbackDelegate;
+import androidx.car.app.model.signin.InputSignInMethod;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.input.CarEditable;
+import com.android.car.libraries.apphost.input.CarEditableListener;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** A view that displays {@link InputSignInMethod} UI. */
+public class InputSignInView extends LinearLayout implements CarEditable {
+ private final int mMaxWidth;
+
+ private CarEditText mSignInEditText;
+ private CarUiTextView mSignInEditTextErrorMessage;
+ @Nullable private TextWatcher mTextWatcher;
+
+ public InputSignInView(Context context) {
+ this(context, null);
+ }
+
+ public InputSignInView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InputSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings("ResourceType")
+ public InputSignInView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateSignInMethodViewMaxWidth,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mMaxWidth = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+ return mSignInEditText.onCreateInputConnection(editorInfo);
+ }
+
+ @Override
+ public void setCarEditableListener(CarEditableListener listener) {}
+
+ @Override
+ public void setInputEnabled(boolean enabled) {}
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ if (mMaxWidth > 0 && mMaxWidth < measuredWidth) {
+ int measureMode = MeasureSpec.getMode(widthMeasureSpec);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /** Sets the {@link InputSignInMethod} for the view. */
+ public void setSignInMethod(
+ TemplateContext templateContext,
+ InputSignInMethod inputSignInMethod,
+ InputManager inputManager,
+ CharSequence disabledInputHint,
+ boolean isRefresh) {
+ clearEditTextListeners();
+
+ // TODO(b/183434044): move this logic to CarRestrictedEditText
+ setInputHint(templateContext, inputSignInMethod, disabledInputHint);
+
+ // TODO(b/183434044): move this logic to CarRestrictedEditText
+ setInitialText(templateContext, inputSignInMethod, isRefresh);
+
+ setErrorMessage(templateContext, inputSignInMethod);
+
+ // TODO(b/183434044): move this logic to CarRestrictedEditText
+ setInputType(inputSignInMethod);
+
+ setShowKeyboardByDefault(templateContext, inputSignInMethod, inputManager);
+
+ // Make sure to set these at the end so that setting initial text etc doesn't trigger text
+ // change callbacks.
+ setEditTextListeners(templateContext, inputSignInMethod, inputManager);
+ }
+
+ /** Clears the edit text focus. */
+ public void clearEditTextFocus() {
+ mSignInEditText.clearFocus();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mSignInEditText = findViewById(R.id.input_sign_in_box);
+ mSignInEditTextErrorMessage = findViewById(R.id.input_sign_in_error_message);
+ }
+
+ private void clearEditTextListeners() {
+ mSignInEditText.setOnClickListener(null);
+ mSignInEditText.setOnEditorActionListener(null);
+ if (mTextWatcher != null) {
+ mSignInEditText.removeTextChangedListener(mTextWatcher);
+ mTextWatcher = null;
+ }
+ }
+
+ private void setEditTextListeners(
+ TemplateContext templateContext,
+ InputSignInMethod inputSignInMethod,
+ InputManager inputManager) {
+ mSignInEditText.setOnEditorActionListener(
+ (view, actionId, event) -> {
+ inputManager.stopInput();
+ String inputText = mSignInEditText.getText().toString().trim();
+ if (TextUtils.isEmpty(inputText)) {
+ return false;
+ } else {
+ submitInput(templateContext, inputText, inputSignInMethod);
+ return true;
+ }
+ });
+ mTextWatcher =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence text, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence text, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable text) {
+ updateInputText(templateContext, text.toString(), inputSignInMethod);
+ }
+ };
+ mSignInEditText.addTextChangedListener(mTextWatcher);
+ mSignInEditText.setInputManager(inputManager);
+ }
+
+ private void setInputHint(
+ TemplateContext templateContext,
+ InputSignInMethod inputSignInMethod,
+ CharSequence disabledInputHint) {
+ CarText inputHint = inputSignInMethod.getHint();
+ CharSequence hint = CarTextUtils.toCharSequenceOrEmpty(templateContext, inputHint);
+ mSignInEditText.setHint(mSignInEditText.isEnabled() ? hint : disabledInputHint);
+ }
+
+ private void setInitialText(
+ TemplateContext templateContext, InputSignInMethod inputSignInMethod, boolean isRefresh) {
+ CarText initialText = inputSignInMethod.getDefaultValue();
+
+ CharSequence text = mSignInEditText.getText();
+ if (!isRefresh || text == null || text.length() == 0) {
+ mSignInEditText.setText(CarTextUtils.toCharSequenceOrEmpty(templateContext, initialText));
+ mSignInEditText.setSelection(mSignInEditText.getText().length());
+ }
+ }
+
+ private void setErrorMessage(
+ TemplateContext templateContext, InputSignInMethod inputSignInMethod) {
+ CarText errorMessage = inputSignInMethod.getErrorMessage();
+ if (!CarText.isNullOrEmpty(errorMessage)) {
+ mSignInEditTextErrorMessage.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, errorMessage, mSignInEditTextErrorMessage.getMaxLines()));
+ mSignInEditTextErrorMessage.setVisibility(VISIBLE);
+ mSignInEditText.setErrorState(true);
+ } else {
+ mSignInEditTextErrorMessage.setVisibility(GONE);
+ mSignInEditText.setErrorState(false);
+ }
+ }
+
+ private void setInputType(InputSignInMethod inputSignInMethod) {
+ int inputType;
+ switch (inputSignInMethod.getKeyboardType()) {
+ case KEYBOARD_PHONE:
+ inputType = TYPE_CLASS_PHONE;
+ break;
+ case KEYBOARD_NUMBER:
+ inputType = TYPE_CLASS_NUMBER;
+ if (inputSignInMethod.getInputType() == INPUT_TYPE_PASSWORD) {
+ inputType |= TYPE_NUMBER_VARIATION_PASSWORD;
+ }
+ break;
+ case KEYBOARD_EMAIL:
+ inputType =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS | TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+ break;
+ case KEYBOARD_DEFAULT:
+ default:
+ inputType = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+ if (inputSignInMethod.getInputType() == INPUT_TYPE_PASSWORD) {
+ inputType |= TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ }
+ mSignInEditText.setInputType(inputType);
+ }
+
+ private void setShowKeyboardByDefault(
+ TemplateContext templateContext,
+ InputSignInMethod inputSignInMethod,
+ InputManager inputManager) {
+ boolean isRestricted = templateContext.getConstraintsProvider().isConfigRestricted();
+ if (inputSignInMethod.isShowKeyboardByDefault() && !isRestricted) {
+ inputManager.startInput(this);
+ }
+ }
+
+ private void submitInput(
+ TemplateContext templateContext, String inputText, InputSignInMethod inputSignInMethod) {
+ InputCallbackDelegate delegate = inputSignInMethod.getInputCallbackDelegate();
+ if (delegate != null) {
+ templateContext.getAppDispatcher().dispatchInputSubmitted(delegate, inputText);
+ } else {
+ L.w(LogTags.TEMPLATE, "Input callback is expected on the template but not set");
+ }
+ }
+
+ private void updateInputText(
+ TemplateContext templateContext, String inputText, InputSignInMethod inputSignInMethod) {
+ InputCallbackDelegate delegate = inputSignInMethod.getInputCallbackDelegate();
+ if (delegate != null) {
+ templateContext.getAppDispatcher().dispatchInputTextChanged(delegate, inputText);
+ } else {
+ L.w(LogTags.TEMPLATE, "Input callback is expected on the template but not set");
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java
new file mode 100644
index 0000000..fa1db8c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/MarkerFactory.java
@@ -0,0 +1,592 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_CENTER_XY_INSIDE;
+import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_FIT_CENTER;
+import static java.lang.Math.max;
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Cap;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.PlaceMarker;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.CommonUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import org.checkerframework.checker.initialization.qual.UnderInitialization;
+import org.checkerframework.checker.initialization.qual.UnknownInitialization;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** A factory of bitmaps to be used as map markers in different templates. */
+public class MarkerFactory {
+ // Cache the default anchor so that we don't have to draw if users did not customize.
+ private final Bitmap mDefaultAnchorBitmap;
+
+ // Cache the default marker so that we don't have to draw if users did not customize.
+ private final Bitmap mDefaultMarkerBitmap;
+
+ // Mask for clipping an image within bounds. A marker image's draw area is slightly bigger than
+ // icons with rounded corners.
+ @MonotonicNonNull private Bitmap mMarkerImageMask;
+ private final Paint mMarkerImageMaskPaint;
+
+ // Default path used for drawing the standard-size map marker.
+ private final Path mDefaultMarkerPath;
+ private final MarkerAppearance mAppearance;
+
+ /** Create a MarkerFactory */
+ public static MarkerFactory create(Context context, MarkerAppearance appearance) {
+ return new MarkerFactory(context, appearance);
+ }
+
+ /** Returns a map marker bitmap with the given {@link PlaceMarker} configuration. */
+ // TODO(b/144920236): cache and reuse bitmaps when applicable.
+ public Bitmap createPoiMarkerBitmap(
+ TemplateContext templateContext, @Nullable PlaceMarker marker) {
+ if (marker == null) {
+ return mDefaultMarkerBitmap;
+ }
+
+ // Use the dark variant for background color.
+ @ColorInt int markerColor = resolveMarkerColor(templateContext, marker);
+ boolean useDefaultMarker = markerColor == mAppearance.mMarkerDefaultBackgroundColor;
+
+ CarText label = marker.getLabel();
+ CarIcon icon = marker.getIcon();
+ if (label == null && icon == null && useDefaultMarker) {
+ return mDefaultMarkerBitmap;
+ }
+
+ boolean needWideMarker = false;
+ Bitmap markerBitmap;
+ String labelString = label == null ? null : label.toString();
+ if (icon == null && labelString != null) {
+ // If we need to draw a label, check if we need to draw a wider marker to fit the text.
+ Rect bounds = new Rect();
+ mAppearance.mDefaultTextPaint.getTextBounds(labelString, 0, labelString.length(), bounds);
+
+ if (bounds.width() > mAppearance.mMarkerSize - mAppearance.mTextHorizontalPadding * 2) {
+ needWideMarker = true;
+ markerBitmap =
+ Bitmap.createBitmap(
+ bounds.width() + mAppearance.mTextHorizontalPadding * 2,
+ mAppearance.mMarkerSize + mAppearance.mMarkerPointerHeight,
+ Config.ARGB_8888);
+ markerBitmap.setDensity(mAppearance.mDensityDpi);
+ } else {
+ markerBitmap = Bitmap.createBitmap(mDefaultMarkerBitmap);
+ }
+ } else {
+ markerBitmap = Bitmap.createBitmap(mDefaultMarkerBitmap);
+ }
+
+ Canvas canvas = new Canvas(markerBitmap);
+ if (needWideMarker || !useDefaultMarker) {
+ drawMarker(
+ canvas,
+ mAppearance,
+ markerColor,
+ useDefaultMarker
+ ? mAppearance.mMarkerDefaultBorderColor
+ : mAppearance.mMarkerCustomBorderColor);
+ }
+
+ Bitmap contentBitmap = getContentForMapMarker(templateContext, marker, useDefaultMarker);
+ if (contentBitmap != null) {
+ canvas.drawBitmap(
+ contentBitmap,
+ // Width may have been adjusted so we use the bitmap's width as source of truth.
+ (markerBitmap.getWidth() - contentBitmap.getWidth()) / 2f,
+ (mAppearance.mMarkerSize - contentBitmap.getHeight()) / 2f,
+ mAppearance.mDefaultTextPaint);
+ }
+
+ return markerBitmap;
+ }
+
+ /**
+ * Returns the bitmap representing the content (icon of text) that should appear within a marker
+ * in the map view, or {code null} if the marker's content is not specified.
+ */
+ @Nullable
+ private Bitmap getContentForMapMarker(
+ TemplateContext templateContext, PlaceMarker marker, boolean hasDefaultBackground) {
+ CarText label = marker.getLabel();
+ String labelString = label != null ? label.toString() : null;
+ CarIcon icon = marker.getIcon();
+ Bitmap contentBitmap = null;
+
+ // The icon value takes precedence over the label if both are set.
+ if (icon != null) {
+ contentBitmap =
+ getIconBitmap(templateContext, marker, icon, CommonUtils.isDarkMode(templateContext));
+ } else if (labelString != null && !labelString.isEmpty()) {
+ contentBitmap =
+ ImageUtils.getBitmapFromString(
+ labelString,
+ hasDefaultBackground
+ ? mAppearance.mDefaultTextPaint
+ : mAppearance.mCustomBackgroundTextPaint);
+ }
+
+ if (contentBitmap != null) {
+ contentBitmap.setDensity(mAppearance.mDensityDpi);
+ }
+
+ return contentBitmap;
+ }
+
+ /**
+ * Returns the bitmap representing the content (icon of text) of the given marker, or {code null}
+ * if the marker's content is not specified.
+ */
+ @Nullable
+ public Bitmap getContentForListMarker(TemplateContext templateContext, PlaceMarker marker) {
+ CarText label = marker.getLabel();
+ String labelString = label != null ? label.toString() : null;
+ CarIcon icon = marker.getIcon();
+ Bitmap contentBitmap = null;
+
+ // The icon value takes precedence over the label if both are set.
+ if (icon != null) {
+ // We always use the light-variant tint for list marker because the card background is
+ // dark.
+ contentBitmap = getIconBitmap(templateContext, marker, icon, /* isDark= */ false);
+ } else if (labelString != null && !labelString.isEmpty()) {
+ // We use the light-variant color for the text in the list.
+ int resolvedColor =
+ CarColorUtils.resolveColor(
+ templateContext,
+ marker.getColor(),
+
+ // The background of the card is dark so use the light variant for the
+ // text color.
+ /* isDark= */ false,
+ mAppearance.mDefaultTextPaint.getColor(),
+ CarColorConstraints.UNCONSTRAINED);
+ Paint paint;
+ if (resolvedColor == mAppearance.mCustomBackgroundTextPaint.getColor()) {
+ paint = mAppearance.mCustomBackgroundTextPaint;
+ } else {
+ paint = new Paint(mAppearance.mCustomBackgroundTextPaint);
+ paint.setColor(resolvedColor);
+ }
+
+ Resources resources = templateContext.getResources();
+ Drawable bitmap =
+ new BitmapDrawable(resources, ImageUtils.getBitmapFromString(labelString, paint));
+ contentBitmap =
+ ImageUtils.getBitmapFromDrawable(
+ bitmap,
+ mAppearance.mListIconSize,
+ mAppearance.mListIconSize,
+ resources.getDisplayMetrics().densityDpi,
+ SCALE_CENTER_XY_INSIDE);
+ }
+
+ if (contentBitmap != null) {
+ contentBitmap.setDensity(mAppearance.mDensityDpi);
+ }
+
+ return contentBitmap;
+ }
+
+ @Nullable
+ private Bitmap getIconBitmap(
+ TemplateContext templateContext, PlaceMarker marker, CarIcon icon, boolean isDark) {
+ Bitmap contentBitmap;
+ boolean isImage = isMarkerImage(marker);
+ int bitmapSize = isImage ? mAppearance.mImageSize : mAppearance.mIconSize;
+ ImageViewParams imageParams =
+ ImageViewParams.builder()
+ .setDefaultTint(mAppearance.mDefaultIconTint)
+ .setForceTinting(!isImage)
+ .setIsDark(isDark)
+ .build();
+ contentBitmap =
+ ImageUtils.getBitmapFromIcon(
+ templateContext, icon, bitmapSize, bitmapSize, imageParams, SCALE_FIT_CENTER);
+
+ if (contentBitmap == null) {
+ L.e(LogTags.TEMPLATE, "Failed to get bitmap for marker: %s", marker);
+ } else if (isImage) {
+ // Apply masking to get the rounded corner effect.
+ Bitmap maskedImage = Bitmap.createBitmap(bitmapSize, bitmapSize, Config.ARGB_8888);
+ maskedImage.setDensity(mAppearance.mDensityDpi);
+
+ Canvas maskedCanvas = new Canvas(maskedImage);
+ maskedCanvas.drawBitmap(getOrCreateMarkerImageMask(mAppearance), 0, 0, null);
+ maskedCanvas.drawBitmap(contentBitmap, 0, 0, mMarkerImageMaskPaint);
+ contentBitmap = maskedImage;
+ }
+ return contentBitmap;
+ }
+
+ /** Returns an {@link Bitmap} which has been adjusted for a given background color. */
+ public Bitmap getAnchorBitmap(TemplateContext templateContext, @Nullable CarColor background) {
+ if (background == null) {
+ return mDefaultAnchorBitmap;
+ }
+
+ @ColorInt
+ int resolvedBackground =
+ CarColorUtils.resolveColor(
+ templateContext,
+ background,
+ // Use the dark-variant in day mode, and vice versa.
+ /* isDark= */ !CommonUtils.isDarkMode(templateContext),
+ mAppearance.mAnchorDefaultBackgroundColor,
+ CarColorConstraints.UNCONSTRAINED);
+ if (resolvedBackground == mAppearance.mAnchorDefaultBackgroundColor) {
+ return mDefaultAnchorBitmap;
+ }
+
+ return createAnchorBitmap(templateContext, resolvedBackground, mAppearance);
+ }
+
+ /** Draw a rounded-corner marker with a pointer at the bottom center. */
+ private void drawMarker(
+ @UnknownInitialization MarkerFactory this,
+ Canvas canvas,
+ MarkerAppearance appearance,
+ @ColorInt int backgroundColor,
+ @ColorInt int borderColor) {
+ int defaultMarkerWidth = appearance.mMarkerSize;
+ int defaultMarkerHeight = appearance.mMarkerSize + appearance.mMarkerPointerHeight;
+
+ Path markerPath =
+ canvas.getWidth() == defaultMarkerWidth
+ ? mDefaultMarkerPath
+ : createMarkerPath(canvas.getWidth(), defaultMarkerHeight, appearance);
+ if (markerPath == null) {
+ return;
+ }
+
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(backgroundColor);
+ canvas.drawPath(markerPath, paint);
+
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(borderColor);
+ paint.setStrokeWidth(appearance.mMarkerStroke);
+ paint.setStrokeCap(Cap.ROUND);
+ canvas.drawPath(markerPath, paint);
+ }
+
+ private Bitmap createDefaultMarkerBitmap(
+ @UnderInitialization MarkerFactory this, MarkerAppearance appearance) {
+ Bitmap markerBitmap =
+ Bitmap.createBitmap(
+ appearance.mMarkerSize,
+ appearance.mMarkerSize + appearance.mMarkerPointerHeight,
+ Config.ARGB_8888);
+ markerBitmap.setDensity(appearance.mDensityDpi);
+
+ Canvas canvas = new Canvas(markerBitmap);
+ drawMarker(
+ canvas,
+ appearance,
+ appearance.mMarkerDefaultBackgroundColor,
+ appearance.mMarkerDefaultBorderColor);
+
+ return markerBitmap;
+ }
+
+ private Bitmap createAnchorBitmap(
+ @UnknownInitialization MarkerFactory this,
+ Context context,
+ @ColorInt int backgroundColor,
+ MarkerAppearance appearance) {
+ Resources resources = context.getResources();
+
+ Drawable markerBackground = resources.getDrawable(R.drawable.anchor_marker);
+ markerBackground.setBounds(
+ 0, 0, markerBackground.getIntrinsicWidth(), markerBackground.getIntrinsicHeight());
+ markerBackground.setColorFilter(backgroundColor, PorterDuff.Mode.SRC_IN);
+
+ Drawable markerBorder = resources.getDrawable(R.drawable.anchor_marker_border);
+ markerBorder.setBounds(
+ 0, 0, markerBorder.getIntrinsicWidth(), markerBorder.getIntrinsicHeight());
+ markerBorder.setColorFilter(appearance.mAnchorBorderColor, PorterDuff.Mode.SRC_IN);
+
+ Drawable markerDot = resources.getDrawable(R.drawable.anchor_marker_circle);
+ markerDot.setBounds(0, 0, markerDot.getIntrinsicWidth(), markerDot.getIntrinsicHeight());
+ markerDot.setColorFilter(appearance.mAnchorDotColor, PorterDuff.Mode.SRC_IN);
+
+ Bitmap bitmap =
+ Bitmap.createBitmap(
+ markerBackground.getIntrinsicWidth(),
+ markerBackground.getIntrinsicHeight(),
+ Config.ARGB_8888);
+ bitmap.setDensity(appearance.mDensityDpi);
+
+ Canvas canvas = new Canvas(bitmap);
+ markerBackground.draw(canvas);
+ markerBorder.draw(canvas);
+ markerDot.draw(canvas);
+
+ return bitmap;
+ }
+
+ private Bitmap getOrCreateMarkerImageMask(MarkerAppearance appearance) {
+ if (mMarkerImageMask != null) {
+ return mMarkerImageMask;
+ }
+
+ mMarkerImageMask =
+ Bitmap.createBitmap(appearance.mImageSize, appearance.mImageSize, Config.ALPHA_8);
+ mMarkerImageMask.setDensity(appearance.mDensityDpi);
+
+ Canvas canvas = new Canvas(mMarkerImageMask);
+ canvas.drawRoundRect(
+ 0,
+ 0,
+ appearance.mImageSize,
+ appearance.mImageSize,
+ appearance.mImageCornerRadius,
+ appearance.mImageCornerRadius,
+ new Paint());
+
+ return mMarkerImageMask;
+ }
+
+ /**
+ * Returns the marker color that should be used based on the {@link PlaceMarker}'s configuration.
+ *
+ * <p>If the marker is of type {@link PlaceMarker#TYPE_IMAGE}, then the default color will be
+ * used. Otherwise, we resolve the color provided via the marker to what is defined in our theme.
+ */
+ @ColorInt
+ private int resolveMarkerColor(TemplateContext templateContext, PlaceMarker marker) {
+ // We do not support rendering a background color for images.
+ if (marker.getIconType() == PlaceMarker.TYPE_IMAGE) {
+ return mAppearance.mMarkerDefaultBackgroundColor;
+ }
+
+ @ColorInt
+ int resolvedColor =
+ CarColorUtils.resolveColor(
+ templateContext,
+ marker.getColor(),
+ // Use the dark-variant in day mode for better contrast with the
+ // light-colored map, and
+ // vice versa.
+ /* isDark= */ !CommonUtils.isDarkMode(templateContext),
+ mAppearance.mMarkerDefaultBackgroundColor,
+ CarColorConstraints.UNCONSTRAINED);
+ return resolvedColor;
+ }
+
+ private MarkerFactory(Context context, MarkerAppearance appearance) {
+ mAppearance = appearance;
+
+ mDefaultMarkerPath =
+ createMarkerPath(
+ appearance.mMarkerSize,
+ appearance.mMarkerSize + appearance.mMarkerPointerHeight,
+ appearance);
+ mDefaultMarkerBitmap = createDefaultMarkerBitmap(appearance);
+ mDefaultAnchorBitmap =
+ createAnchorBitmap(context, appearance.mAnchorDefaultBackgroundColor, appearance);
+ mDefaultAnchorBitmap.setDensity(appearance.mDensityDpi);
+
+ mMarkerImageMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mMarkerImageMaskPaint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ }
+
+ /**
+ * Create a {@link Path} representing a map marker that fits within the input width and height.
+ */
+ private static Path createMarkerPath(int width, int height, MarkerAppearance appearance) {
+ // Actual draw region needs to account for the stroke size.
+ // At the end, we offset the drawing by (markerStroke / 2) in both x and y to center it.
+ int drawWidth = width - appearance.mMarkerStroke;
+ int drawHeight = height - appearance.mMarkerStroke;
+ int cornerRadius = appearance.mMarkerCornerRadius;
+ int cornerDiameter = cornerRadius * 2;
+ float halfStroke = appearance.mMarkerStroke / 2f;
+
+ // Start from the pointer tip and draw clockwise.
+ float startX = drawWidth / 2f;
+ float startY = drawHeight;
+
+ // Bottom of the rectangular region of the marker.
+ float rectBottom = startY - appearance.mMarkerPointerHeight;
+ float pointerHalfWidth = appearance.mMarkerPointerWidth / 2f;
+
+ Path path = new Path();
+ RectF cornerRect = new RectF();
+
+ path.moveTo(startX, startY);
+ path.lineTo(startX - pointerHalfWidth, rectBottom);
+ path.lineTo(appearance.mMarkerCornerRadius, rectBottom);
+ cornerRect.set(0, rectBottom - cornerDiameter, cornerDiameter, rectBottom);
+ path.arcTo(cornerRect, 90, 90, false);
+
+ path.lineTo(0, cornerRadius);
+ cornerRect = new RectF(0, 0, cornerDiameter, cornerDiameter);
+ path.arcTo(cornerRect, 180, 90, false);
+
+ path.lineTo(drawWidth - cornerRadius, 0);
+ cornerRect.set(drawWidth - cornerDiameter, 0, drawWidth, cornerDiameter);
+ path.arcTo(cornerRect, 270, 90, false);
+
+ path.lineTo(drawWidth, rectBottom - cornerRadius);
+ cornerRect.set(drawWidth - cornerDiameter, rectBottom - cornerDiameter, drawWidth, rectBottom);
+ path.arcTo(cornerRect, 0, 90, false);
+
+ path.lineTo(startX + pointerHalfWidth, rectBottom);
+ path.close();
+
+ // Offset the path to accommodate the stroke so the drawing is centered within the region.
+ path.offset(halfStroke, halfStroke);
+
+ return path;
+ }
+
+ private static boolean isMarkerImage(PlaceMarker marker) {
+ return marker.getIconType() == PlaceMarker.TYPE_IMAGE;
+ }
+
+ /** Contains the attributes that define the marker's appearance. */
+ public static class MarkerAppearance {
+ @ColorInt private final int mMarkerDefaultBackgroundColor;
+ @ColorInt private final int mMarkerDefaultBorderColor;
+ @ColorInt private final int mMarkerCustomBorderColor;
+ private final int mMarkerSize;
+ private final int mMarkerPointerWidth;
+ private final int mMarkerPointerHeight;
+ private final int mMarkerStroke;
+ private final int mMarkerCornerRadius;
+ @ColorInt private final int mAnchorDefaultBackgroundColor;
+ @ColorInt private final int mAnchorBorderColor;
+ @ColorInt private final int mAnchorDotColor;
+
+ @ColorInt private final int mDefaultIconTint;
+
+ private final int mIconSize;
+ private final int mTextHorizontalPadding;
+ private final int mImageSize;
+ private final int mImageCornerRadius;
+ private final Paint mDefaultTextPaint;
+ private final Paint mCustomBackgroundTextPaint;
+
+ private final int mDensityDpi;
+ private final int mListIconSize;
+
+ /**
+ * Creates an instance of a {@link MarkerAppearance} by reading it from the styled attributes in
+ * the given context's theme.
+ */
+ public MarkerAppearance(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ // Get the marker appearance style resource id from the view's attributes.
+ TypedArray viewStyledAttributes =
+ context.obtainStyledAttributes(attrs, R.styleable.PlaceMarker, defStyleAttr, defStyleRes);
+ int resId = viewStyledAttributes.getResourceId(R.styleable.PlaceMarker_markerAppearance, -1);
+ viewStyledAttributes.recycle();
+
+ // No need to pass default values here, the style should contain all these values, which
+ // can be ensured by using a default style resource by the caller.
+ TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.MarkerAppearance);
+
+ @ColorInt
+ int defaultContentColor =
+ ta.getColor(R.styleable.MarkerAppearance_markerDefaultContentColor, -1);
+
+ // Set up the paint for the text of the marker's label.
+ mDefaultTextPaint =
+ new Paint(Paint.LINEAR_TEXT_FLAG | Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
+ mDefaultTextPaint.setTextAlign(Align.CENTER);
+ mDefaultTextPaint.setTypeface(
+ Typeface.create(
+ requireNonNull(ta.getString(R.styleable.MarkerAppearance_android_fontFamily)),
+ ta.getInt(R.styleable.MarkerAppearance_android_textStyle, -1)));
+ mDefaultTextPaint.setColor(defaultContentColor);
+ mDefaultTextPaint.setTextSize(
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_android_textSize, -1));
+
+ mCustomBackgroundTextPaint = new Paint(mDefaultTextPaint);
+ mCustomBackgroundTextPaint.setColor(
+ ta.getColor(R.styleable.MarkerAppearance_markerCustomBackgroundContentColor, -1));
+
+ // All other marker/anchor related dimensions and colors
+ mMarkerDefaultBackgroundColor =
+ ta.getInt(R.styleable.MarkerAppearance_markerDefaultBackgroundColor, -1);
+ mMarkerDefaultBorderColor =
+ ta.getInt(R.styleable.MarkerAppearance_markerDefaultBorderColor, -1);
+ mMarkerCustomBorderColor =
+ ta.getInt(R.styleable.MarkerAppearance_markerCustomBorderColor, -1);
+ mMarkerPointerWidth =
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPointerWidth, -1);
+ mMarkerPointerHeight =
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPointerHeight, -1);
+ mMarkerStroke = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerStroke, -1);
+ mMarkerCornerRadius =
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerCornerRadius, -1);
+ mAnchorDefaultBackgroundColor =
+ ta.getInt(R.styleable.MarkerAppearance_anchorDefaultBackgroundColor, -1);
+ mAnchorBorderColor = ta.getInt(R.styleable.MarkerAppearance_anchorBorderColor, -1);
+ mAnchorDotColor = ta.getInt(R.styleable.MarkerAppearance_anchorDotColor, -1);
+ mTextHorizontalPadding =
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerTextHorizontalPadding, -1);
+ mIconSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerIconSize, -1);
+ mImageSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerImageSize, -1);
+ mImageCornerRadius =
+ ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerImageCornerRadius, -1);
+
+ mDefaultIconTint = defaultContentColor;
+
+ int markerPadding = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerPadding, -1);
+ mMarkerSize = max(mIconSize, mImageSize) + mMarkerStroke + markerPadding;
+
+ mListIconSize = ta.getDimensionPixelSize(R.styleable.MarkerAppearance_markerListIconSize, -1);
+
+ ta.recycle();
+
+ mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java
new file mode 100644
index 0000000..607cd18
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PanOverlayView.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Overlay view to display on top of the map surface in the pan mode. */
+public class PanOverlayView extends FrameLayout {
+
+ public PanOverlayView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public PanOverlayView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PanOverlayView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PanOverlayView(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java
new file mode 100644
index 0000000..34d84e9
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ParkedOnlyFrameLayout.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+
+/**
+ * Custom view that hides content and shows an appropriate message when car is driving. This view
+ * will hide all children views while the driving message is being shown. Usage of this layout
+ * should use a single container layout as a child, and visibility of that child should not be
+ * modified outside of this layout. This layout does not maintain any visibility states of children
+ * views before or after drive state changes. This means that if the visibility of children views
+ * are updated directly the visibility may not be consistent after the driving message disappears.
+ */
+public class ParkedOnlyFrameLayout extends FrameLayout {
+
+ private View mDrivingMessageView;
+
+ private boolean mIsLockedOut;
+ private TemplateContext mTemplateContext;
+ private EventManager mEventManager;
+
+ public ParkedOnlyFrameLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public ParkedOnlyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ParkedOnlyFrameLayout(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ParkedOnlyFrameLayout(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ View rootView = LayoutInflater.from(getContext()).inflate(R.layout.driving_message_view, this);
+ mDrivingMessageView = rootView.findViewById(R.id.driving_message_view);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mEventManager != null) {
+ mEventManager.unsubscribeEvent(this, EventType.CONSTRAINTS);
+ }
+ super.onDetachedFromWindow();
+ }
+
+ /** Set the template context used to start listening for uxr constraints. */
+ public void setTemplateContext(TemplateContext templateContext) {
+ mTemplateContext = templateContext;
+ mEventManager = templateContext.getEventManager();
+
+ mEventManager.subscribeEvent(this, EventType.CONSTRAINTS, this::update);
+
+ CarUiTextView drivingMessageText = mDrivingMessageView.findViewById(R.id.driving_message_text);
+ drivingMessageText.setText(
+ CarUiTextUtils.fromCharSequence(
+ templateContext,
+ templateContext.getString(
+ templateContext.getHostResourceIds().getDrivingStateMessageText()),
+ drivingMessageText.getMaxLines()));
+ update();
+ }
+
+ /** Get whether the content view is being hidden, and the driving message is being shown. */
+ public boolean isLockedOut() {
+ return mIsLockedOut;
+ }
+
+ private void update() {
+ boolean isRestricted = mTemplateContext.getConstraintsProvider().isConfigRestricted();
+ if (isRestricted == mIsLockedOut) {
+ return;
+ }
+ mIsLockedOut = isRestricted;
+
+ // Hide IME if ParkedOnlyFrameLayout is visible
+ if (mIsLockedOut) {
+ mTemplateContext.getInputManager().stopInput();
+ }
+
+ // Toggle visibility of all children views; the driving message will be shown if locked out,
+ // content views are shown otherwise.
+ for (int i = 0; i < this.getChildCount(); i++) {
+ View view = getChildAt(i);
+ if (view.getId() == mDrivingMessageView.getId()) {
+ view.setVisibility(mIsLockedOut ? VISIBLE : GONE);
+ } else {
+ view.setVisibility(mIsLockedOut ? GONE : VISIBLE);
+ }
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java
new file mode 100644
index 0000000..1282365
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/PinSignInView.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.signin.PinSignInMethod;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.CarUiText;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** A view that displays {@link PinSignInMethod} UI. */
+public class PinSignInView extends FrameLayout {
+ private final int mMaxWidth;
+
+ private CarUiTextView mPinTextView;
+
+ public PinSignInView(Context context) {
+ this(context, null);
+ }
+
+ public PinSignInView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PinSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings("ResourceType")
+ public PinSignInView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateSignInMethodViewMaxWidth,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mMaxWidth = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+ if (mMaxWidth > 0 && mMaxWidth < measuredWidth) {
+ int measureMode = MeasureSpec.getMode(widthMeasureSpec);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPinTextView = findViewById(R.id.pin_text);
+ }
+ /** Returns the maximum height of mPinTextView */
+ public int getMaxLines() {
+ return mPinTextView.getMaxLines();
+ }
+
+ /** Sets the PIN text. */
+ public void setText(CarUiText pinText) {
+ mPinTextView.setText(pinText);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java
new file mode 100644
index 0000000..39d2aa7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/QRCodeSignInView.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import androidx.car.app.model.signin.QRCodeSignInMethod;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.templates.host.R;
+import com.google.zxing.WriterException;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+import com.google.zxing.qrcode.encoder.ByteMatrix;
+import com.google.zxing.qrcode.encoder.Encoder;
+import com.google.zxing.qrcode.encoder.QRCode;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A view that displays {@link QRCodeSignInMethod} UI. */
+public class QRCodeSignInView extends FrameLayout {
+ private ImageView mQRCodeView;
+
+ public QRCodeSignInView(Context context) {
+ this(context, null);
+ }
+
+ public QRCodeSignInView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public QRCodeSignInView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings("ResourceType")
+ public QRCodeSignInView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mQRCodeView = findViewById(R.id.qr_code_view);
+ }
+
+ /** Sets the qr code. */
+ public void setQRCodeSignInMethod(
+ TemplateContext templateContext, QRCodeSignInMethod qrCodeSignInMethod) {
+ setQRCode(templateContext, qrCodeSignInMethod.getUri().toString());
+ }
+
+ private void setQRCode(TemplateContext templateContext, String url) {
+ QRCode qrCode;
+ try {
+ qrCode = Encoder.encode(url, ErrorCorrectionLevel.H);
+ } catch (WriterException e) {
+ templateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(templateContext.getCarAppPackageInfo().getComponentName())
+ .setCause(e)
+ .build());
+ return;
+ }
+
+ BitmapDrawable drawable = new BitmapDrawable(getResources(), qrToBitmap(qrCode));
+ drawable.setAntiAlias(false);
+ drawable.setFilterBitmap(false);
+ mQRCodeView.setImageDrawable(drawable);
+ }
+
+ private Bitmap qrToBitmap(QRCode qrCode) {
+ ByteMatrix matrix = qrCode.getMatrix();
+ int width = matrix.getWidth();
+ int height = matrix.getHeight();
+ int[] colors = new int[width * height];
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ colors[y * width + x] = (matrix.get(x, y) != 0) ? Color.WHITE : Color.TRANSPARENT;
+ }
+ }
+
+ return Bitmap.createBitmap(colors, 0, width, width, height, Bitmap.Config.ALPHA_8);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java
new file mode 100644
index 0000000..b040bc6
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowAdapter.java
@@ -0,0 +1,1019 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.android.car.libraries.apphost.template.view.model.RowListWrapper.LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE;
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.Switch;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ForegroundCarColorSpan;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowConstraints;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.template.view.model.SelectionGroup;
+import com.android.car.libraries.apphost.view.common.ActionButtonListParams;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.RowHolder.RowListener;
+import com.android.car.ui.CarUiText;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiContentListItem.IconType;
+import com.android.car.ui.recyclerview.CarUiListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Adapter for {@link ContentView} to display {@link CarUiListItem}s. */
+public class RowAdapter extends CarUiListItemAdapter {
+ /**
+ * Start id for non-Chassis items. This value should be higher than any view type in {@link
+ * CarUiListItemAdapter}.
+ */
+ private static final int ROW_LIST_VIEW_TYPE_BASE = 1000;
+
+ /**
+ * Empty payload used with {@link #notifyItemChanged(int, Object)} to selectively disable item
+ * change animations.
+ */
+ private static final Object EMPTY_ITEM_CHANGED_PAYLOAD = new Object();
+
+ private static final int ROW_LIST_VIEW_TYPE_ACTION_BUTTON = ROW_LIST_VIEW_TYPE_BASE + 1;
+ private static final int ROW_LIST_VIEW_TYPE_SECTION_HEADER = ROW_LIST_VIEW_TYPE_BASE + 2;
+ private static final int ROW_LIST_VIEW_TYPE_ROW = ROW_LIST_VIEW_TYPE_BASE + 3;
+ private static final CarColor SELECTED_TEXT_COLOR = CarColor.BLUE;
+ private static final int TITLE_MAX_LINE_COUNT = 2;
+ private static final int ONE_BODY_MAX_LINE_COUNT = 2;
+ private static final int MULTI_BODY_MAX_LINE_COUNT = 1;
+ private static final int MAX_IMAGES_PER_TEXT_LINE = 2;
+
+ @ColorInt private final int mDefaultIconTint;
+ @Nullable private final Drawable mPlaceholderDrawable;
+ @Nullable private final Drawable mFullRowChevronDrawable;
+ @Nullable private final Drawable mHalfRowChevronDrawable;
+ @ColorInt private final int mRowBackgroundColor;
+
+ private RowListener mRowListener;
+ private List<RowHolder> mRowHolders;
+ private TemplateContext mTemplateContext;
+ private final MarkerFactory mMarkerFactory;
+ private final boolean mUseCompactLayout;
+ private final int mTitleTextSize;
+ private final int mSecondaryTextSize;
+ private final int mSectionHeaderTextSize;
+
+ static RowAdapter create(
+ Context context,
+ List<CarUiListItem> items,
+ MarkerFactory markerFactory,
+ boolean useCompactLayout) {
+ return new RowAdapter(context, items, markerFactory, useCompactLayout);
+ }
+
+ private RowAdapter(
+ Context context,
+ List<CarUiListItem> items,
+ MarkerFactory markerFactory,
+ boolean useCompactLayout) {
+ super(items, useCompactLayout);
+
+ mUseCompactLayout = useCompactLayout;
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateRowDefaultIconTint,
+ R.attr.templateRowImagePlaceholder,
+ R.attr.templateFullRowChevronIcon,
+ R.attr.templateHalfRowChevronIcon,
+ R.attr.templateRowBackgroundColor,
+ };
+
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mDefaultIconTint = ta.getColor(0, 0);
+ mPlaceholderDrawable = ta.getDrawable(1);
+ mFullRowChevronDrawable = ta.getDrawable(2);
+ mHalfRowChevronDrawable = ta.getDrawable(3);
+ mRowBackgroundColor = ta.getColor(4, 0);
+ ta.recycle();
+
+ mTitleTextSize = getTextSizeFromAttribute(context, R.attr.templateRowTitleStyle);
+ mSecondaryTextSize = getTextSizeFromAttribute(context, R.attr.templateRowSecondaryTextStyle);
+ mSectionHeaderTextSize =
+ getTextSizeFromAttribute(context, R.attr.templateRowSectionHeaderStyle);
+
+ mMarkerFactory = markerFactory;
+ }
+
+ private static int getTextSizeFromAttribute(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
+ int titleTextStyleResourceId = ta.getResourceId(0, 0);
+ ta.recycle();
+
+ ta =
+ context.obtainStyledAttributes(
+ titleTextStyleResourceId, new int[] {android.R.attr.textAppearance});
+ int titleTextAppearanceResourceId = ta.getResourceId(0, 0);
+ ta.recycle();
+
+ ta =
+ context.obtainStyledAttributes(
+ titleTextAppearanceResourceId, new int[] {android.R.attr.textSize});
+ int result = ta.getDimensionPixelSize(androidx.appcompat.R.styleable.TextAppearance_android_textSize, 0);
+ ta.recycle();
+
+ return result;
+ }
+
+ public List<RowHolder> getRowHolders() {
+ return mRowHolders;
+ }
+
+ /** Updates the rows of the adapter. */
+ @SuppressWarnings("unchecked")
+ public void setRows(
+ TemplateContext templateContext, List<RowHolder> rowHolders, RowListener rowListener) {
+ int previousItemCount = mRowHolders == null ? 0 : mRowHolders.size();
+ mTemplateContext = templateContext;
+ mRowHolders = rowHolders;
+ mRowListener = rowListener;
+
+ List<CarUiListItem> items = new ArrayList<>(rowHolders.size());
+ for (int index = 0; index < rowHolders.size(); index++) {
+ RowHolder holder = rowHolders.get(index);
+ RowWrapper rowWrapper = holder.getRowWrapper();
+ CarUiListItem item = createCarUiListItem(templateContext, rowWrapper, index);
+ if (item == null) {
+ Log.e(LogTags.TEMPLATE, "Cannot create item for the row " + rowWrapper);
+ continue;
+ }
+ items.add(item);
+ }
+
+ getItems().clear();
+ ((List<CarUiListItem>) getItems()).addAll(items);
+ if (previousItemCount == items.size()) {
+ notifyItemRangeChanged(0, items.size(), EMPTY_ITEM_CHANGED_PAYLOAD);
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+
+ /** Updates row at index {@code index} of the adapter. */
+ @SuppressWarnings("unchecked")
+ public void updateRow(int index) {
+ if (index < 0 || index >= mRowHolders.size()) {
+ Log.e(LogTags.TEMPLATE, "Index out of bound " + index);
+ return;
+ }
+ RowWrapper rowWrapper = mRowHolders.get(index).getRowWrapper();
+ CarUiListItem item = createCarUiListItem(mTemplateContext, rowWrapper, index);
+ if (item == null) {
+ Log.e(LogTags.TEMPLATE, "Cannot create item for the row " + rowWrapper);
+ return;
+ }
+ ((List<CarUiListItem>) getItems()).set(index, item);
+ // By passing a non-null payload to notifyItemChanged, we avoid ItemAnimator's onChange
+ // animation. This is needed when updating list items because otherwise the ViewHolder's
+ // contents flicker every time they are updated.
+ notifyItemChanged(index, EMPTY_ITEM_CHANGED_PAYLOAD);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == ROW_LIST_VIEW_TYPE_ACTION_BUTTON) {
+ return new ActionButtonListViewHolder(
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.action_button_list_row, parent, false));
+ } else if (viewType == ROW_LIST_VIEW_TYPE_SECTION_HEADER) {
+ return new SectionHeaderViewHolder(
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.row_section_header_view, parent, false));
+ } else if (viewType == ROW_LIST_VIEW_TYPE_ROW) {
+ if (mUseCompactLayout) {
+ return new ListItemViewHolder(
+ LayoutInflater.from(parent.getContext())
+ .inflate(
+ R.layout.half_list_row_view, /* root= */ parent, /* attachToRoot= */ false),
+ mHalfRowChevronDrawable);
+ } else {
+ return new ListItemViewHolder(
+ LayoutInflater.from(parent.getContext())
+ .inflate(
+ R.layout.full_list_row_view, /* root= */ parent, /* attachToRoot= */ false),
+ mFullRowChevronDrawable);
+ }
+ } else {
+ return super.onCreateViewHolder(parent, viewType);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (getItems().get(position) instanceof ActionButtonListItem) {
+ return ROW_LIST_VIEW_TYPE_ACTION_BUTTON;
+ } else if (getItems().get(position) instanceof SectionHeaderItem) {
+ return ROW_LIST_VIEW_TYPE_SECTION_HEADER;
+ } else if (getItems().get(position) instanceof CarUiContentListItem) {
+ return ROW_LIST_VIEW_TYPE_ROW;
+ } else {
+ return super.getItemViewType(position);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_ACTION_BUTTON) {
+ CarUiListItem item = getItems().get(position);
+ if (!(item instanceof ActionButtonListItem)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item);
+ return;
+ }
+ if (!(holder instanceof ActionButtonListViewHolder)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect view holder type for list item.");
+ return;
+ }
+
+ ((ActionButtonListViewHolder) holder).bind((ActionButtonListItem) item);
+ } else if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_SECTION_HEADER) {
+ CarUiListItem item = getItems().get(position);
+ if (!(item instanceof SectionHeaderItem)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item);
+ return;
+ }
+ if (!(holder instanceof SectionHeaderViewHolder)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect view holder type for section header item.");
+ return;
+ }
+
+ ((SectionHeaderViewHolder) holder).bind((SectionHeaderItem) item);
+ } else if (holder.getItemViewType() == ROW_LIST_VIEW_TYPE_ROW) {
+ CarUiListItem item = getItems().get(position);
+ if (!(item instanceof CarUiContentListItem)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect item view type for item " + item);
+ return;
+ }
+ if (!(holder instanceof ListItemViewHolder)) {
+ Log.e(LogTags.TEMPLATE, "Incorrect view holder type for row item " + item);
+ return;
+ }
+
+ // TODO(b/205602000): investigate switching to ConstraintLayout for this instead of
+ // calculating the margin in code.
+ RowHolder rowHolder = getRowHolders().get(position);
+ boolean hasTemplateImageBesidesRow =
+ (rowHolder.getRowWrapper().getListFlags() & LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE) != 0;
+ ((ListItemViewHolder) holder).bind((CarUiContentListItem) item, hasTemplateImageBesidesRow);
+ } else {
+ super.onBindViewHolder(holder, position);
+ }
+ }
+
+ /** Converts a {@link Row} to a {@link CarUiListItem}. */
+ @Nullable
+ public CarUiListItem createCarUiListItem(
+ TemplateContext templateContext, RowWrapper rowWrapper, int index) {
+ if (rowWrapper.getRow() instanceof Row) {
+ return createRowCarUiListItem(templateContext, rowWrapper, index);
+ } else if (ActionListUtils.isActionList(rowWrapper.getRow())) {
+ return createActionsCarUiListItem(templateContext, rowWrapper);
+ } else {
+ Log.i(LogTags.TEMPLATE, "Unknown row type ${rowWrapper.row.javaClass.name}");
+ return null;
+ }
+ }
+
+ /** Converts the {@link Row} to a header */
+ private SectionHeaderItem createCarUiHeaderListItem(TemplateContext templateContext, Row row) {
+ CarTextParams params =
+ new CarTextParams.Builder()
+ .setMaxImages(MAX_IMAGES_PER_TEXT_LINE)
+ .setImageBoundingBox(new Rect(0, 0, mSectionHeaderTextSize, mSectionHeaderTextSize))
+ .build();
+ return new SectionHeaderItem(
+ CarTextUtils.toCharSequenceOrEmpty(templateContext, row.getTitle(), params));
+ }
+
+ @Nullable
+ private CarUiListItem createActionsCarUiListItem(
+ TemplateContext templateContext, RowWrapper rowWrapper) {
+ List<Action> actions = ActionListUtils.getActionList(rowWrapper.getRow());
+ int maxActions = rowWrapper.getRowConstraints().getMaxActionsExclusive();
+
+ return new RowAdapter.ActionButtonListItem(
+ templateContext, actions, maxActions, mRowBackgroundColor);
+ }
+
+ private CarUiListItem createRowCarUiListItem(
+ TemplateContext templateContext, RowWrapper rowWrapper, int index) {
+ Row row = (Row) rowWrapper.getRow();
+
+ // If this row is a header, create a header item instead
+ if ((rowWrapper.getRowFlags() & RowWrapper.ROW_FLAG_SECTION_HEADER) != 0) {
+ return createCarUiHeaderListItem(templateContext, row);
+ }
+
+ CarUiContentListItem.Action action = createAction(rowWrapper);
+ CarUiContentListItem item = new CarUiContentListItem(action);
+ boolean colorContrastCheckPassed =
+ checkColorContrast(templateContext, row, mRowBackgroundColor);
+ updateItemText(
+ item, templateContext, rowWrapper, index, /* allowColor= */ colorContrastCheckPassed);
+
+ // Only update the item image if there is no place marker.
+ if (!updateItemPlaceMarker(item, templateContext, rowWrapper)) {
+ updateItemImage(
+ item, templateContext, rowWrapper, index, /* allowTint= */ colorContrastCheckPassed);
+ }
+ updateCheckedState(item, rowWrapper, index);
+ updateActivationState(item, rowWrapper, index);
+ updateClickListener(item, rowWrapper, index);
+
+ item.setOnCheckedChangeListener(
+ (v, checked) -> {
+ if (mRowListener != null) {
+ mRowListener.onCheckedChange(index);
+ }
+ });
+
+ return item;
+ }
+
+ /** Checks the color contrast between contents of the given row and the background color. */
+ private static boolean checkColorContrast(
+ TemplateContext templateContext, Row row, @ColorInt int backgroundColor) {
+ // Only the secondary texts can be colored, so check them
+ for (CarText carText : row.getTexts()) {
+ if (!CarTextUtils.checkColorContrast(templateContext, carText, backgroundColor)) {
+ return false;
+ }
+ }
+
+ CarIcon image = row.getImage();
+ if (image == null) {
+ return true;
+ }
+ CarColor tint = image.getTint();
+ if (tint == null) {
+ return true;
+ }
+
+ return CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor);
+ }
+
+ /**
+ * Sets the click listener if the row is actionable. An actionable row is one that has a click
+ * delegate, selection state, or toggle.
+ */
+ private void updateClickListener(CarUiContentListItem item, RowWrapper rowWrapper, int index) {
+ Row row = (Row) rowWrapper.getRow();
+
+ boolean isClickable =
+ row.getOnClickDelegate() != null
+ && rowWrapper.getRowConstraints().isOnClickListenerAllowed();
+ boolean isSelectable = rowWrapper.getSelectionGroup() != null;
+ boolean isToggle = row.getToggle() != null;
+ if (isClickable || isSelectable || isToggle) {
+ item.setOnItemClickedListener(
+ (v) -> {
+ if (mRowListener != null) {
+ mRowListener.onRowClicked(index);
+ }
+ });
+ } else {
+ item.setOnItemClickedListener(null);
+ }
+ }
+
+ /** Updates the text fields of the item using properties of the {@link RowWrapper}. */
+ private void updateItemText(
+ CarUiContentListItem item,
+ TemplateContext templateContext,
+ RowWrapper rowWrapper,
+ int index,
+ boolean allowColor) {
+ Row row = (Row) rowWrapper.getRow();
+ RowConstraints constraints = rowWrapper.getRowConstraints();
+ int listFlags = rowWrapper.getListFlags();
+
+ boolean renderTitleAsSecondaryText =
+ (listFlags & RowListWrapper.LIST_FLAGS_RENDER_TITLE_AS_SECONDARY) != 0;
+
+ // Create a copy because the row model returns unmodifiable list.
+ List<CarText> texts = new ArrayList<>(row.getTexts());
+
+ CarText title = row.getTitle();
+ if (title != null) {
+ CarTextParams titleParams =
+ new CarTextParams.Builder()
+ .setMaxImages(MAX_IMAGES_PER_TEXT_LINE)
+ .setImageBoundingBox(new Rect(0, 0, mTitleTextSize, mTitleTextSize))
+ .build();
+ CharSequence titleString =
+ CarTextUtils.toCharSequenceOrEmpty(templateContext, title, titleParams);
+ if (titleString.length() > 0) {
+ if (renderTitleAsSecondaryText) {
+ texts.add(0, title);
+ } else {
+ item.setTitle(
+ CarUiTextUtils.fromCarText(
+ templateContext, title, titleParams, TITLE_MAX_LINE_COUNT));
+ }
+ }
+ }
+
+ int lineCount = texts.size();
+ int maxLineCount = min(constraints.getMaxTextLinesPerRow(), lineCount);
+
+ if (maxLineCount < lineCount) {
+ Log.d(
+ LogTags.TEMPLATE,
+ "Number of secondary text lines " + lineCount + " over limit of " + maxLineCount);
+ }
+
+ while (!texts.isEmpty() && texts.size() > maxLineCount) {
+ texts.remove(texts.size() - 1);
+ }
+
+ // Add selected text to the body if available.
+ CarText selectedText = createSelectedText(rowWrapper, index);
+ if (selectedText != null) {
+ texts.add(selectedText);
+ }
+
+ if (!texts.isEmpty()) {
+ List<CarUiText> bodyTexts = createCarUiTextList(templateContext, texts, allowColor);
+ item.setBody(bodyTexts);
+ }
+ }
+
+ /**
+ * Creates a {@link CarText} representing {@code selectedText} for the given {@link RowWrapper}.
+ *
+ * <p>Returns {@code null} if selected text is not available or the row is not selected.
+ */
+ @Nullable
+ private CarText createSelectedText(RowWrapper rowWrapper, int index) {
+ CarText selectedText = rowWrapper.getSelectedText();
+ if (selectedText == null) {
+ return null;
+ }
+
+ SelectionGroup selectionGroup = rowWrapper.getSelectionGroup();
+ if (selectionGroup == null || !selectionGroup.isSelected(index)) {
+ return null;
+ }
+
+ SpannableString spannableSelectedText = new SpannableString(selectedText.toCharSequence());
+ int start = 0;
+ int end = spannableSelectedText.length();
+ spannableSelectedText.setSpan(
+ ForegroundCarColorSpan.create(SELECTED_TEXT_COLOR),
+ start,
+ end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return new CarText.Builder(spannableSelectedText).build();
+ }
+
+ /**
+ * Updates the place marker image of the item using properties of the {@link RowWrapper}.
+ *
+ * <p>Returns true iff a place marker was found.
+ */
+ private boolean updateItemPlaceMarker(
+ CarUiContentListItem item, TemplateContext templateContext, RowWrapper rowWrapper) {
+ Place place = rowWrapper.getMetadata().getPlace();
+ if (place == null) {
+ return false;
+ }
+
+ PlaceMarker marker = place.getMarker();
+ if (marker == null) {
+ return false;
+ }
+
+ Bitmap bitmap = mMarkerFactory.getContentForListMarker(templateContext, marker);
+ if (bitmap == null) {
+ return false;
+ }
+
+ item.setPrimaryIconType(IconType.STANDARD);
+ item.setIcon(new BitmapDrawable(templateContext.getResources(), bitmap));
+ return true;
+ }
+
+ /** Updates the image of the item using properties of the {@link RowWrapper}. */
+ private void updateItemImage(
+ CarUiContentListItem item,
+ TemplateContext templateContext,
+ RowWrapper rowWrapper,
+ int index,
+ boolean allowTint) {
+ Row row = (Row) rowWrapper.getRow();
+ CarIcon image = row.getImage();
+ if (image == null) {
+ return;
+ }
+
+ CarUiContentListItem.IconType iconType = convertImageTypeToIconType(row.getRowImageType());
+ if (iconType == null) {
+ Log.e(LogTags.TEMPLATE, "Unknown icon type for row " + row);
+ return;
+ }
+ item.setPrimaryIconType(iconType);
+ int iconSize = (int) getIconSize(iconType);
+
+ ImageViewParams imageParams =
+ ImageViewParams.builder()
+ .setPlaceholderDrawable(mPlaceholderDrawable)
+ .setDefaultTint(mDefaultIconTint)
+ .setForceTinting(row.getRowImageType() == Row.IMAGE_TYPE_ICON)
+ .setIgnoreAppTint(!allowTint)
+ .setBackgroundColor(mRowBackgroundColor)
+ .setCarIconConstraints(rowWrapper.getRowConstraints().getCarIconConstraints())
+ .build();
+ ImageUtils.setImageTargetSrc(
+ templateContext,
+ row.getImage(),
+ drawable -> {
+ item.setIcon(drawable);
+ // By passing a non-null payload to notifyItemChanged, we avoid ItemAnimator's
+ // onChange animation. This is needed when updating list items because otherwise
+ // the ViewHolder's contents flicker every time they are updated.
+ notifyItemChanged(index, EMPTY_ITEM_CHANGED_PAYLOAD);
+ },
+ imageParams,
+ iconSize,
+ iconSize);
+ }
+
+ /** Updates the checked state of the item. */
+ private void updateCheckedState(CarUiContentListItem item, RowWrapper rowWrapper, int index) {
+ SelectionGroup selectionGroup = rowWrapper.getSelectionGroup();
+ boolean isSelected = selectionGroup != null && selectionGroup.getSelectedIndex() == index;
+ boolean hasRadioButton = item.getAction() == CarUiContentListItem.Action.RADIO_BUTTON;
+
+ RowConstraints constraints = rowWrapper.getRowConstraints();
+ boolean isToggleAllowed = constraints.isToggleAllowed();
+ boolean hasToggle = item.getAction() == CarUiContentListItem.Action.SWITCH;
+ boolean isToggleChecked = rowWrapper.isToggleChecked();
+
+ item.setChecked(
+ (isSelected && hasRadioButton) || (isToggleAllowed && hasToggle && isToggleChecked));
+ }
+
+ /** Updates the activation state of the item. */
+ private void updateActivationState(CarUiContentListItem item, RowWrapper rowWrapper, int index) {
+ SelectionGroup selectionGroup = rowWrapper.getSelectionGroup();
+ boolean isSelected = selectionGroup != null && selectionGroup.getSelectedIndex() == index;
+ boolean useRadioButton = item.getAction() == CarUiContentListItem.Action.RADIO_BUTTON;
+ item.setActivated(isSelected && !useRadioButton && shouldHighlightSelectedRow(rowWrapper));
+ }
+
+ @Nullable
+ private static CarUiContentListItem.IconType convertImageTypeToIconType(int imageType) {
+ switch (imageType) {
+ case Row.IMAGE_TYPE_LARGE:
+ return IconType.CONTENT;
+ case Row.IMAGE_TYPE_SMALL:
+ case Row.IMAGE_TYPE_ICON:
+ return IconType.STANDARD;
+ default:
+ return null;
+ }
+ }
+
+ private float getIconSize(CarUiContentListItem.IconType imageType) {
+ Resources res = mTemplateContext.getResources();
+ switch (imageType) {
+ case CONTENT:
+ return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_content_icon_width);
+ case STANDARD:
+ return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_icon_size);
+ case AVATAR:
+ return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_avatar_icon_width);
+ }
+ Log.e(LogTags.TEMPLATE, "Unknown imageType: " + imageType);
+ return res.getDimension(com.android.car.ui.R.dimen.car_ui_list_item_icon_size);
+ }
+
+ /** Returns proper {@link CarUiContentListItem.Action} for a given {@link RowWrapper}. */
+ private CarUiContentListItem.Action createAction(RowWrapper rowWrapper) {
+ if (!(rowWrapper.getRow() instanceof Row)) {
+ return CarUiContentListItem.Action.NONE;
+ }
+
+ Row row = (Row) rowWrapper.getRow();
+ if (row.isBrowsable()) {
+ return CarUiContentListItem.Action.CHEVRON;
+ } else if (row.getToggle() != null) {
+ return CarUiContentListItem.Action.SWITCH;
+ } else if (rowWrapper.getSelectionGroup() != null && shouldUseRadioButtons(rowWrapper)) {
+ return CarUiContentListItem.Action.RADIO_BUTTON;
+ } else {
+ return CarUiContentListItem.Action.NONE;
+ }
+ }
+
+ /**
+ * Returns true if the flag for using radio buttons is enabled for the provided {@link
+ * RowWrapper}.
+ */
+ private boolean shouldUseRadioButtons(RowWrapper rowWrapper) {
+ return (rowWrapper.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS)
+ != 0;
+ }
+
+ /** Returns true if the flag for highlighting the currently selected row is enabled */
+ private boolean shouldHighlightSelectedRow(RowWrapper rowWrapper) {
+ return (rowWrapper.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW) != 0;
+ }
+
+ /** Creates a list of {@link CarUiText} one for each given {@link CarText}. */
+ private List<CarUiText> createCarUiTextList(
+ TemplateContext templateContext, List<CarText> carTexts, boolean allowColor) {
+ CarTextParams textParams =
+ CarTextParams.builder()
+ .setColorSpanConstraints(
+ allowColor ? CarColorConstraints.STANDARD_ONLY : CarColorConstraints.NO_COLOR)
+ .setMaxImages(MAX_IMAGES_PER_TEXT_LINE)
+ .setImageBoundingBox(new Rect(0, 0, mSecondaryTextSize, mSecondaryTextSize))
+ .setBackgroundColor(mRowBackgroundColor)
+ .build();
+ List<CarUiText> lines = new ArrayList<>();
+ int maxLineCount = carTexts.size() > 1 ? MULTI_BODY_MAX_LINE_COUNT : ONE_BODY_MAX_LINE_COUNT;
+ for (CarText carText : carTexts) {
+ lines.add(CarUiTextUtils.fromCarText(templateContext, carText, textParams, maxLineCount));
+ }
+ return lines;
+ }
+
+ /** The {@link ViewHolder} for {@link ActionButtonListItem}. */
+ static class ActionButtonListViewHolder extends RecyclerView.ViewHolder {
+ private final ActionButtonListView mActionButtonListView;
+
+ ActionButtonListViewHolder(View view) {
+ super(view);
+ mActionButtonListView = requireViewByRefId(view, R.id.action_button_list_view);
+ }
+
+ void bind(ActionButtonListItem item) {
+ mActionButtonListView.setActionList(
+ item.getTemplateContext(),
+ item.getActions(),
+ ActionButtonListParams.builder()
+ .setMaxActions(item.getMaxActions())
+ .setOemReorderingAllowed(true)
+ .setOemColorOverrideAllowed(true)
+ .setSurroundingColor(item.getSurroundingColor())
+ .build());
+ }
+ }
+
+ /** The {@link ViewHolder} for {@link SectionHeaderItem}. */
+ static class SectionHeaderViewHolder extends RecyclerView.ViewHolder {
+ SectionHeaderViewHolder(View view) {
+ super(view);
+ }
+
+ void bind(SectionHeaderItem item) {
+ CarUiTextView sectionHeaderView = (CarUiTextView) itemView;
+ sectionHeaderView.setText(item.getText());
+ }
+ }
+
+ /** View model for an {@link ActionButtonListView}. */
+ public static class ActionButtonListItem extends CarUiListItem {
+ private final TemplateContext mTemplateContext;
+ private final List<Action> mActionList;
+ private final int mMaxActions;
+ @ColorInt private final int mSurroundingColor;
+
+ ActionButtonListItem(
+ TemplateContext templateContext,
+ List<Action> actionList,
+ int maxActions,
+ @ColorInt int surroundingColor) {
+ mActionList = actionList;
+ mTemplateContext = templateContext;
+ mMaxActions = maxActions;
+ mSurroundingColor = surroundingColor;
+ }
+
+ /** Returns a list of {@link Action}s */
+ List<Action> getActions() {
+ return mActionList;
+ }
+
+ /** Returns the associated {@link TemplateContext} */
+ TemplateContext getTemplateContext() {
+ return mTemplateContext;
+ }
+
+ /** Returns the maximum number of actions allowed. */
+ int getMaxActions() {
+ return mMaxActions;
+ }
+
+ /** Returns the color of the surrounding region around the action button list. */
+ @ColorInt
+ int getSurroundingColor() {
+ return mSurroundingColor;
+ }
+ }
+
+ /** View model for a section header. */
+ public static class SectionHeaderItem extends CarUiListItem {
+ private final CharSequence mText;
+
+ SectionHeaderItem(CharSequence text) {
+ mText = text;
+ }
+
+ CharSequence getText() {
+ return mText;
+ }
+ }
+
+ /** Holds views of {@link CarUiContentListItem}. */
+ static class ListItemViewHolder extends RecyclerView.ViewHolder {
+
+ final CarUiTextView mTitle;
+ final CarUiTextView mBody;
+ final ImageView mIcon;
+ final ImageView mContentIcon;
+ final ImageView mAvatarIcon;
+ final ViewGroup mIconContainer;
+ final ViewGroup mActionContainer;
+ final Switch mSwitch;
+ final CheckBox mCheckBox;
+ final RadioButton mRadioButton;
+ final ImageView mSupplementalIcon;
+ final View mTouchInterceptor;
+ final View mReducedTouchInterceptor;
+ final View mActionContainerTouchInterceptor;
+ @Nullable final Drawable mChevronDrawable;
+ @Nullable final View mLargeImageSpacer;
+
+ ListItemViewHolder(@NonNull View itemView, @Nullable Drawable chevronDrawable) {
+ super(itemView);
+ mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title);
+ mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body);
+ mIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_icon);
+ mContentIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_content_icon);
+ mAvatarIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_avatar_icon);
+ mIconContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_icon_container);
+ mActionContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_action_container);
+ mSwitch = requireViewByRefId(itemView, R.id.car_ui_list_item_switch_widget);
+ mCheckBox = requireViewByRefId(itemView, R.id.car_ui_list_item_checkbox_widget);
+ mRadioButton = requireViewByRefId(itemView, R.id.car_ui_list_item_radio_button_widget);
+ mSupplementalIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_supplemental_icon);
+ mReducedTouchInterceptor =
+ requireViewByRefId(itemView, R.id.car_ui_list_item_reduced_touch_interceptor);
+ mTouchInterceptor = requireViewByRefId(itemView, R.id.car_ui_list_item_touch_interceptor);
+ mActionContainerTouchInterceptor =
+ requireViewByRefId(itemView, R.id.car_ui_list_item_action_container_touch_interceptor);
+ mChevronDrawable = chevronDrawable;
+ mLargeImageSpacer = itemView.findViewById(R.id.large_image_spacer);
+ }
+
+ void bind(@NonNull CarUiContentListItem item, boolean hasTemplateImageBesidesRow) {
+ CarUiText title = item.getTitle();
+ if (title != null) {
+ mTitle.setText(title);
+ mTitle.setVisibility(View.VISIBLE);
+ } else {
+ mTitle.setVisibility(View.GONE);
+ }
+
+ List<CarUiText> body = item.getBody();
+ if (body != null) {
+ mBody.setText(body);
+ mBody.setVisibility(View.VISIBLE);
+ } else {
+ mBody.setVisibility(View.GONE);
+ }
+
+ mIcon.setVisibility(View.GONE);
+ mContentIcon.setVisibility(View.GONE);
+ mAvatarIcon.setVisibility(View.GONE);
+
+ Drawable icon = item.getIcon();
+ if (icon != null) {
+ mIconContainer.setVisibility(View.VISIBLE);
+
+ switch (item.getPrimaryIconType()) {
+ case CONTENT:
+ mContentIcon.setVisibility(View.VISIBLE);
+ mContentIcon.setImageDrawable(icon);
+ break;
+ case STANDARD:
+ mIcon.setVisibility(View.VISIBLE);
+ mIcon.setImageDrawable(icon);
+ break;
+ case AVATAR:
+ mAvatarIcon.setVisibility(View.VISIBLE);
+ mAvatarIcon.setImageDrawable(icon);
+ mAvatarIcon.setClipToOutline(true);
+ break;
+ }
+ } else {
+ mIconContainer.setVisibility(View.GONE);
+ }
+
+ mSwitch.setVisibility(View.GONE);
+ mCheckBox.setVisibility(View.GONE);
+ mRadioButton.setVisibility(View.GONE);
+ mSupplementalIcon.setVisibility(View.GONE);
+
+ CarUiContentListItem.OnClickListener itemOnClickListener = item.getOnClickListener();
+
+ switch (item.getAction()) {
+ case NONE:
+ mActionContainer.setVisibility(View.GONE);
+
+ // Display ripple effects across entire item when clicked by using full-sized
+ // touch interceptor.
+ mTouchInterceptor.setVisibility(View.VISIBLE);
+ mTouchInterceptor.setOnClickListener(
+ v -> {
+ if (itemOnClickListener != null) {
+ itemOnClickListener.onClick(item);
+ }
+ });
+ mTouchInterceptor.setClickable(itemOnClickListener != null);
+ mReducedTouchInterceptor.setVisibility(View.GONE);
+ mActionContainerTouchInterceptor.setVisibility(View.GONE);
+ break;
+ case SWITCH:
+ bindCompoundButton(item, mSwitch, itemOnClickListener);
+ break;
+ case CHECK_BOX:
+ bindCompoundButton(item, mCheckBox, itemOnClickListener);
+ break;
+ case RADIO_BUTTON:
+ bindCompoundButton(item, mRadioButton, itemOnClickListener);
+ break;
+ case CHEVRON:
+ mSupplementalIcon.setVisibility(View.VISIBLE);
+ mSupplementalIcon.setImageDrawable(mChevronDrawable);
+ mActionContainer.setVisibility(View.VISIBLE);
+ mTouchInterceptor.setVisibility(View.VISIBLE);
+ mTouchInterceptor.setOnClickListener(
+ v -> {
+ if (itemOnClickListener != null) {
+ itemOnClickListener.onClick(item);
+ }
+ });
+ mTouchInterceptor.setClickable(itemOnClickListener != null);
+ mReducedTouchInterceptor.setVisibility(View.GONE);
+ mActionContainerTouchInterceptor.setVisibility(View.GONE);
+ break;
+ case ICON:
+ mSupplementalIcon.setVisibility(View.VISIBLE);
+ mSupplementalIcon.setImageDrawable(item.getSupplementalIcon());
+
+ mActionContainer.setVisibility(View.VISIBLE);
+
+ // If the icon has a click listener, use a reduced touch interceptor to create
+ // two distinct touch area; the action container and the remainder of the list
+ // item. Each touch area will have its own ripple effect. If the icon has no
+ // click listener, it shouldn't be clickable.
+ if (item.getSupplementalIconOnClickListener() == null) {
+ mTouchInterceptor.setVisibility(View.VISIBLE);
+ mTouchInterceptor.setOnClickListener(
+ v -> {
+ if (itemOnClickListener != null) {
+ itemOnClickListener.onClick(item);
+ }
+ });
+ mTouchInterceptor.setClickable(itemOnClickListener != null);
+ mReducedTouchInterceptor.setVisibility(View.GONE);
+ mActionContainerTouchInterceptor.setVisibility(View.GONE);
+ } else {
+ mReducedTouchInterceptor.setVisibility(View.VISIBLE);
+ mReducedTouchInterceptor.setOnClickListener(
+ v -> {
+ if (itemOnClickListener != null) {
+ itemOnClickListener.onClick(item);
+ }
+ });
+ mReducedTouchInterceptor.setClickable(itemOnClickListener != null);
+ mActionContainerTouchInterceptor.setVisibility(View.VISIBLE);
+ mActionContainerTouchInterceptor.setOnClickListener(
+ (container) -> {
+ CarUiContentListItem.OnClickListener listener =
+ item.getSupplementalIconOnClickListener();
+ if (listener != null) {
+ listener.onClick(item);
+ }
+ });
+ mActionContainerTouchInterceptor.setClickable(
+ item.getSupplementalIconOnClickListener() != null);
+ mTouchInterceptor.setVisibility(View.GONE);
+ }
+ break;
+ }
+
+ // Sets the right margin for the row to account for the space needed for the large image.
+ View spacer = mLargeImageSpacer;
+ if (spacer != null) {
+ spacer.setVisibility(hasTemplateImageBesidesRow ? View.VISIBLE : View.GONE);
+ }
+
+ itemView.setActivated(item.isActivated());
+ setEnabled(itemView, item.isEnabled());
+ }
+
+ void setEnabled(View view, boolean enabled) {
+ view.setEnabled(enabled);
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+
+ for (int i = 0; i < group.getChildCount(); i++) {
+ setEnabled(group.getChildAt(i), enabled);
+ }
+ }
+ }
+
+ void bindCompoundButton(
+ @NonNull CarUiContentListItem item,
+ @NonNull CompoundButton compoundButton,
+ @Nullable CarUiContentListItem.OnClickListener itemOnClickListener) {
+ compoundButton.setVisibility(View.VISIBLE);
+ compoundButton.setOnCheckedChangeListener(null);
+ compoundButton.setChecked(item.isChecked());
+ compoundButton.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> item.setChecked(isChecked));
+
+ // Clicks anywhere on the item should toggle the checkbox state. Use full touch
+ // interceptor.
+ mTouchInterceptor.setVisibility(View.VISIBLE);
+ mTouchInterceptor.setOnClickListener(
+ v -> {
+ compoundButton.toggle();
+ if (itemOnClickListener != null) {
+ itemOnClickListener.onClick(item);
+ }
+ });
+ // Compound button list items should always be clickable
+ mTouchInterceptor.setClickable(true);
+ mReducedTouchInterceptor.setVisibility(View.GONE);
+ mActionContainerTouchInterceptor.setVisibility(View.GONE);
+
+ mActionContainer.setVisibility(View.VISIBLE);
+ mActionContainer.setClickable(false);
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java
new file mode 100644
index 0000000..8fcf34c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowHolder.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_SECTION_HEADER;
+import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_SHOW_DIVIDERS;
+import static com.android.car.libraries.templates.host.view.widgets.common.ActionListUtils.isActionList;
+
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.RowConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/** A holder of a row instance with its associated metadata. */
+public class RowHolder {
+ /** Listener of events related to a {@link RowHolder} instance. */
+ public interface RowListener {
+ /**
+ * Notifies that a row has been selected.
+ *
+ * @param index index of the row in the list it belongs to.
+ */
+ void onRowClicked(int index);
+
+ /**
+ * Notifies that a row's check state has been changed.
+ *
+ * @param index index of the row in the list it belongs to.
+ */
+ void onCheckedChange(int index);
+
+ /** Notifies that a row's focus has changed. */
+ void onRowFocusChanged(int index, boolean hasFocus);
+ }
+
+ private final RowWrapper mRow;
+
+ @Override
+ public String toString() {
+ return mRow.toString();
+ }
+
+ /** Creates a {@link RowHolder} from a row object. */
+ public static RowHolder create(RowWrapper row) {
+ return new RowHolder(row);
+ }
+
+ /** Returns a list of {@link RowHolder} instances from the given rows. */
+ static ImmutableList<RowHolder> createHolders(
+ TemplateContext templateContext, List<RowWrapper> rows, RowListConstraints constraints) {
+ if (rows.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ ImmutableList.Builder<RowHolder> listBuilder = ImmutableList.builder();
+
+ int maxRowCount =
+ templateContext.getConstraintsProvider().getContentLimit(constraints.getListContentType());
+ int nonHeaderRowCount = 0;
+
+ // Cache the last seen header row, and only add it if there is a non-header row underneath
+ // it.
+ // We don't support consecutive header rows.
+ RowWrapper lastHeaderRow = null;
+ for (int i = 0; i < rows.size(); ++i) {
+ RowWrapper rowWrapper = rows.get(i);
+ Object rowObj = rowWrapper.getRow();
+
+ if (isActionList(rowObj)) {
+ // Special case for an action list which is only for the first row in PaneTemplate.
+ listBuilder.add(RowHolder.create(rowWrapper));
+ } else {
+ // Ensure we only count the actual rows against the row limit.
+ boolean isSectionHeader = (rowWrapper.getRowFlags() & ROW_FLAG_SECTION_HEADER) != 0;
+ if (!isSectionHeader) {
+ nonHeaderRowCount++;
+ if (nonHeaderRowCount > maxRowCount) {
+ L.w(
+ LogTags.TEMPLATE,
+ "Row count exceeds the supported maximum of %d, will drop the"
+ + " remaining excess rows",
+ maxRowCount);
+ break;
+ }
+
+ if (lastHeaderRow != null) {
+ listBuilder.add(RowHolder.create(lastHeaderRow));
+ lastHeaderRow = null;
+ }
+ listBuilder.add(RowHolder.create(rowWrapper));
+ } else {
+ if (lastHeaderRow != null) {
+ L.w(
+ LogTags.TEMPLATE,
+ "Consecutive header rows detected and is not supported, only the"
+ + " last one will be used");
+ }
+
+ lastHeaderRow = rowWrapper;
+ }
+ }
+ }
+
+ return listBuilder.build();
+ }
+
+ public RowConstraints getConstraints() {
+ return mRow.getRowConstraints();
+ }
+
+ public RowWrapper getRowWrapper() {
+ return mRow;
+ }
+
+ boolean isSectionHeader() {
+ return 0 != (mRow.getRowFlags() & ROW_FLAG_SECTION_HEADER);
+ }
+
+ boolean showDividers() {
+ return 0 != (mRow.getRowFlags() & ROW_FLAG_SHOW_DIVIDERS);
+ }
+
+ private RowHolder(RowWrapper row) {
+ mRow = row;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java
new file mode 100644
index 0000000..6f393df
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowListView.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.lang.Math.min;
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnCheckedChangeDelegate;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper;
+import com.android.car.libraries.apphost.template.view.model.SelectionGroup;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.MarkerFactory.MarkerAppearance;
+import com.android.car.libraries.templates.host.view.widgets.common.RowHolder.RowListener;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener;
+import com.android.car.ui.widget.CarUiTextView;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A view that can render a list of {@link androidx.car.app.model.Row} (wrapped inside a {@link
+ * RowListWrapper}.
+ */
+public class RowListView extends FrameLayout {
+ private final AdapterDataObserver mAdapterDataObserver =
+ new AdapterDataObserver() {
+ // call to update() not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ update();
+ }
+
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
+ super.onItemRangeChanged(positionStart, itemCount, payload);
+ update();
+ }
+
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ super.onItemRangeMoved(fromPosition, toPosition, itemCount);
+ update();
+ }
+
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ super.onItemRangeInserted(positionStart, itemCount);
+ update();
+ }
+ };
+
+ private MarkerFactory mMarkerFactory;
+ private RowAdapter mListAdapter;
+ private RowVisibilityObserver mRowVisibilityObserver;
+ private ViewGroup mProgressContainer;
+ private CarUiTextView mEmptyListTextView;
+ private CarUiRecyclerView mListView;
+ private RowListWrapper mRowList;
+ private boolean mIsLoading;
+ private final boolean mUseCompactRowLayout;
+
+ // This is only present in the full list view layout.
+ @Nullable private ViewGroup mLargeImageContainer;
+
+ // This is only present in the full list view layout.
+ @Nullable private CarImageView mLargeImageView;
+ private final float mLargeImageWidthRatio;
+ private final int mLargeImageMaxWidth;
+ private final float mLargeImageAspectRatio;
+ private final int mRowListAndImagePadding;
+ private final int mLargeImageEndPadding;
+ private final int mLargeImageTopMargin;
+ private boolean hasLaidOutLargeImage;
+
+ public RowListView(Context context) {
+ this(context, null);
+ }
+
+ public RowListView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RowListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ @SuppressWarnings({"ResourceType", "nullness:method.invocation", "nullness:argument"})
+ public RowListView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateRowListToLargeImageRatio,
+ R.attr.templateRowListLargeImageContainerMaxWidth,
+ R.attr.templateRowListLargeImageAspectRatio,
+ R.attr.templateRowListAndImagePadding,
+ R.attr.templateFullRowEndPadding,
+ };
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mLargeImageWidthRatio = ta.getFloat(0, 0.f);
+ int largeImageContainerMaxWidth = ta.getDimensionPixelSize(1, 0);
+ mLargeImageAspectRatio = ta.getFloat(2, 0.f);
+ mRowListAndImagePadding = ta.getDimensionPixelSize(3, 0);
+ // The end padding should be consistent with what was used for the row item's end padding.
+ mLargeImageEndPadding = ta.getDimensionPixelSize(4, 0);
+ ta.recycle();
+ mLargeImageMaxWidth =
+ largeImageContainerMaxWidth - (mRowListAndImagePadding + mLargeImageEndPadding);
+ mLargeImageTopMargin = context.getResources().getDimensionPixelSize(R.dimen.template_padding_2);
+
+ ta = context.obtainStyledAttributes(attrs, R.styleable.RowListView, defStyleAttr, defStyleRes);
+ mUseCompactRowLayout = ta.getBoolean(R.styleable.RowListView_listUseCompactRowLayout, false);
+ ta.recycle();
+
+ mMarkerFactory =
+ MarkerFactory.create(
+ context, new MarkerAppearance(context, attrs, defStyleAttr, defStyleAttr));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mProgressContainer = findViewById(R.id.progress_container);
+ mEmptyListTextView = findViewById(R.id.list_no_items_text);
+ mListView = findViewById(R.id.list_view);
+ mRowVisibilityObserver = RowVisibilityObserver.create(requireNonNull(mListView));
+ mListAdapter =
+ RowAdapter.create(getContext(), new ArrayList<>(), mMarkerFactory, mUseCompactRowLayout);
+ mListView.setAdapter(mListAdapter);
+
+ // TODO(b/210167386): setItemAnimator will deprecate for sc+. We can still use the code below to
+ // control the itemAnimator for qt and rvc
+ if (Build.VERSION.SDK_INT <= VERSION_CODES.R) {
+ ItemAnimator itemAnimatorNoDuration = new DefaultItemAnimator();
+ itemAnimatorNoDuration.setAddDuration(0);
+ itemAnimatorNoDuration.setChangeDuration(0);
+ itemAnimatorNoDuration.setMoveDuration(0);
+ itemAnimatorNoDuration.setRemoveDuration(0);
+ mListView.setItemAnimator(itemAnimatorNoDuration);
+ }
+
+ mListAdapter.registerAdapterDataObserver(mAdapterDataObserver);
+
+ ViewGroup imageViewContainer = findViewById(R.id.large_image_container);
+ if (imageViewContainer != null) {
+ mLargeImageContainer = imageViewContainer;
+ mLargeImageView = imageViewContainer.findViewById(R.id.large_image);
+
+ // Synchronize the scrolling of the list with the vertical offset of the image.
+ mListView.addOnScrollListener(
+ new OnScrollListener() {
+ @Override
+ public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) imageViewContainer.getLayoutParams();
+ layoutParams.topMargin -= dy;
+ imageViewContainer.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) {
+ // No-op.
+ }
+ });
+ }
+
+ update();
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public CarUiRecyclerView getRecyclerView() {
+ return mListView;
+ }
+
+ @Nullable
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public RowAdapter getAdapter() {
+ return mListAdapter;
+ }
+
+ void setRowList(TemplateContext templateContext, RowListWrapper rowList) {
+ mRowList = rowList;
+
+ boolean isLoading = rowList.isLoading();
+ if (mIsLoading != isLoading) {
+ // Trigger a visibility update if the loading state has changed.
+ mIsLoading = isLoading;
+ update();
+
+ if (mIsLoading) {
+ // Scroll to the top so we will show the first row when the loading finishes.
+ mListView.scrollToPosition(0);
+ }
+ }
+
+ CarText emptyListCarText = rowList.getEmptyListText();
+ CharSequence emptyText;
+ if (emptyListCarText != null && !emptyListCarText.isEmpty()) {
+ mEmptyListTextView.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, emptyListCarText, mEmptyListTextView.getMaxLines()));
+ } else {
+ emptyText =
+ templateContext.getText(
+ templateContext.getHostResourceIds().getTemplateListNoItemsText());
+ mEmptyListTextView.setText(
+ CarUiTextUtils.fromCharSequence(
+ templateContext, emptyText, mEmptyListTextView.getMaxLines()));
+ }
+
+ CarIcon paneImage = rowList.getImage();
+ ViewGroup imageViewContainer = mLargeImageContainer;
+ if (imageViewContainer != null) {
+ if (paneImage != null) {
+ ImageUtils.setImageSrc(
+ templateContext, paneImage, requireNonNull(mLargeImageView), ImageViewParams.DEFAULT);
+ }
+ }
+
+ RowListConstraints constraints = rowList.getRowListConstraints();
+ List<RowHolder> rowHolders =
+ RowHolder.createHolders(templateContext, rowList.getRowWrappers(), constraints);
+
+ mRowVisibilityObserver.setOnItemVisibilityChangedListener(
+ (startIndexInclusive, endIndexExclusive) -> {
+ OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate =
+ rowList.getOnItemVisibilityChangedDelegate();
+ if (onItemVisibilityChangedDelegate != null) {
+ templateContext
+ .getAppDispatcher()
+ .dispatchItemVisibilityChanged(
+ onItemVisibilityChangedDelegate, startIndexInclusive, endIndexExclusive);
+ }
+ publishMetadata(
+ templateContext, rowList.getRowWrappers(), startIndexInclusive, endIndexExclusive);
+ });
+
+ mListAdapter.setRows(
+ templateContext,
+ rowHolders,
+ new RowListener() {
+ @Override
+ public void onRowClicked(int index) {
+ TelemetryHandler telemetry = templateContext.getTelemetryHandler();
+ telemetry.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(TelemetryEvent.UiAction.ROW_CLICKED).setPosition(index));
+
+ onRowSelected(templateContext, index, /* clicked= */ true);
+ }
+
+ @Override
+ public void onCheckedChange(int index) {
+ TelemetryHandler telemetry = templateContext.getTelemetryHandler();
+ telemetry.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(TelemetryEvent.UiAction.ROW_CLICKED).setPosition(index));
+
+ maybeSwitchToggleState(templateContext, index);
+ }
+
+ @Override
+ public void onRowFocusChanged(int index, boolean hasFocus) {
+ RowListView.this.onRowFocused(templateContext, index, hasFocus);
+ }
+ });
+
+ if (!rowList.isRefresh()) {
+ mListView.scrollToPosition(0);
+ }
+
+ ViewUtils.logCarAppTelemetry(templateContext, UiAction.LIST_SIZE, rowHolders.size());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ // The layout of the large image container is dependent on the final size and paddings of the
+ // list items insider the RecyclerView. Here we obtain the bounds of the first item in the
+ // RecyclerView and lines up the image container based on that.
+ //
+ // We only need to do this once at the beginning to place the image at the right position.
+ // Subsequent synchronization is handled via the OnScrollListener.
+ ViewGroup imageViewContainer = mLargeImageContainer;
+ if (imageViewContainer == null || hasLaidOutLargeImage) {
+ return;
+ }
+
+ int firstActualRowIndex = -1;
+ List<? extends CarUiListItem> items = mListAdapter.getItems();
+ // Find the first item that is a CarUiContentListItem which is used for an actual Row.
+ // Action button lists and section headers use different CarUiListItem types.
+ for (int i = 0; i < items.size(); i++) {
+ CarUiListItem item = items.get(i);
+ if (item instanceof CarUiContentListItem) {
+ firstActualRowIndex = i;
+ break;
+ }
+ }
+
+ if (firstActualRowIndex == -1) {
+ return;
+ }
+
+ View itemView = mListView.getRecyclerViewChildAt(firstActualRowIndex);
+ if (itemView != null) {
+ // Get the item view bounds relative to the RowListView container, and use that
+ // to determine the offset for the image view.
+ Rect itemViewBound = new Rect();
+ itemView.getDrawingRect(itemViewBound);
+ RowListView.this.offsetDescendantRectToMyCoords(itemView, itemViewBound);
+
+ // Sets the bounding box based on desired width and aspect ratio.
+ int imageWidth =
+ min(
+ mLargeImageMaxWidth,
+ // Image width is a ratio of the total container width, accounting for the
+ // padding we want from the row and the edge of the screen.
+ (int) (mLargeImageWidthRatio * itemViewBound.width())
+ - (mRowListAndImagePadding + mLargeImageEndPadding));
+
+ FrameLayout.LayoutParams imageParams =
+ (FrameLayout.LayoutParams) imageViewContainer.getLayoutParams();
+ imageParams.topMargin = itemViewBound.top + mLargeImageTopMargin;
+ imageParams.setMarginEnd(
+ RowListView.this.getRight() - itemViewBound.right + mLargeImageEndPadding);
+ imageParams.width = imageWidth;
+ imageParams.height = (int) (imageWidth * mLargeImageAspectRatio);
+ imageViewContainer.setLayoutParams(imageParams);
+
+ hasLaidOutLargeImage = true;
+ }
+ }
+
+ private void onRowSelected(TemplateContext templateContext, int newIndex, boolean clicked) {
+ RowWrapper rowWrapper = getRowWrapperIfValid(newIndex);
+ if (rowWrapper == null) {
+ return;
+ }
+
+ if (rowWrapper.getRow() instanceof Row) {
+ Row row = (Row) rowWrapper.getRow();
+ final OnClickDelegate onClickDelegate = row.getOnClickDelegate();
+ if (onClickDelegate != null) {
+ templateContext.getAppDispatcher().dispatchClick(onClickDelegate);
+ }
+ }
+
+ SelectionGroup selectionGroup = rowWrapper.getSelectionGroup();
+
+ // If the row belongs to a selection group, change the selection in the group if necessary.
+ // This is done here in the host without having to do a round-trip to the client to change
+ // the
+ // model and re-fresh the entire list, which is much faster and more convenient for apps.
+ if (selectionGroup != null) {
+ int currentSelectionIndex = selectionGroup.getSelectedIndex();
+
+ // If the selected index changed, deselect the previously selected row, and select the
+ // new
+ // one.
+ boolean isRowPreviouslySelected = currentSelectionIndex == newIndex;
+ if (!isRowPreviouslySelected) {
+ // Store the new selection. This is important also in case the rows get re-created
+ // during recycling so that they maintain the proper state.
+ selectionGroup.setSelectedIndex(newIndex);
+
+ mListAdapter.updateRow(currentSelectionIndex);
+ mListAdapter.updateRow(newIndex);
+
+ boolean shouldScrollToSelectedRow =
+ (mRowList.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW) != 0;
+ if (shouldScrollToSelectedRow) {
+ // Post to the main thread so that the scroll happens after the UI changes for
+ // the selected state is completed.
+ post(() -> mListView.smoothScrollToPosition(newIndex));
+ }
+ }
+
+ // Dispatch the selection callbacks.
+ // Note the selection event is dispatched regardless of selection index actually
+ // changing.
+ templateContext
+ .getAppDispatcher()
+ .dispatchSelected(
+ selectionGroup.getOnSelectedDelegate(), selectionGroup.getRelativeIndex(newIndex));
+ if (isRowPreviouslySelected && clicked) {
+ Runnable runnable = mRowList.getRepeatedSelectionCallback();
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+ }
+ }
+
+ private void onRowFocused(TemplateContext templateContext, int index, boolean hasFocus) {
+ // Select the row if moving the focus should change the selection, and we have a new focus.
+ boolean focusChangeSelection =
+ (mRowList.getListFlags() & RowListWrapper.LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW) != 0;
+ if (focusChangeSelection && hasFocus) {
+ onRowSelected(templateContext, index, /* clicked= */ false);
+ }
+ }
+
+ /** Switches the toggle state of a row if it does contain a toggle. */
+ private void maybeSwitchToggleState(TemplateContext templateContext, int index) {
+ RowWrapper rowWrapper = getRowWrapperIfValid(index);
+ if (rowWrapper == null) {
+ return;
+ }
+
+ Object rowObj = rowWrapper.getRow();
+
+ // Only rows can contain toggles.
+ if (rowObj instanceof Row) {
+ Row row = (Row) rowObj;
+
+ // Does the row contain a toggle ? if so, switch its state.
+ Toggle toggle = row.getToggle();
+ if (toggle != null) {
+ rowWrapper.switchToggleState();
+ // Dispatch the checked change callback to the app.
+ OnCheckedChangeDelegate delegate = toggle.getOnCheckedChangeDelegate();
+ templateContext
+ .getAppDispatcher()
+ .dispatchCheckedChanged(delegate, rowWrapper.isToggleChecked());
+ }
+ }
+ }
+
+ private void update() {
+ boolean isLoading = mIsLoading;
+ ViewGroup largeImageContainer = mLargeImageContainer;
+ if (isLoading) {
+ mProgressContainer.setVisibility(VISIBLE);
+
+ // Mark the content views as invisible so that the size of the container remains the
+ // same while the progress bar is showing.
+ mEmptyListTextView.setVisibility(INVISIBLE);
+ mListView.setVisibility(INVISIBLE);
+
+ if (largeImageContainer != null) {
+ largeImageContainer.setVisibility(INVISIBLE);
+ }
+
+ return;
+ }
+
+ mProgressContainer.setVisibility(GONE);
+
+ // If the list is empty, hide it and display a message instead.
+ boolean isEmpty = mListAdapter.getItemCount() == 0;
+ if (isEmpty) {
+ mEmptyListTextView.setVisibility(VISIBLE);
+ mListView.setVisibility(GONE);
+
+ if (largeImageContainer != null) {
+ largeImageContainer.setVisibility(GONE);
+ }
+
+ mEmptyListTextView.setFocusable(true);
+ } else {
+ mEmptyListTextView.setVisibility(GONE);
+ mListView.setVisibility(VISIBLE);
+
+ if (largeImageContainer != null) {
+ largeImageContainer.setVisibility(mRowList.getImage() != null ? VISIBLE : GONE);
+ }
+ }
+ }
+
+ /** Publish any non-null {@link Place}s from the list of {@link RowWrapper}. */
+ private void publishMetadata(
+ TemplateContext templateContext,
+ List<RowWrapper> rowWrappers,
+ int startIndexInclusive,
+ int endIndexExclusive) {
+ if (templateContext == null) {
+ L.e(LogTags.TEMPLATE, "TemplateContext is null");
+ return;
+ }
+
+ // Return if the range is empty.
+ if (startIndexInclusive < 0) {
+ return;
+ }
+
+ if (endIndexExclusive > rowWrappers.size()) {
+ L.e(LogTags.TEMPLATE, "Index out of bound: (%d > %d)", endIndexExclusive, rowWrappers.size());
+ return;
+ }
+
+ ImmutableList.Builder<Place> builder = new ImmutableList.Builder<>();
+ for (int index = startIndexInclusive; index < endIndexExclusive; index++) {
+ RowWrapper rowWrapper = rowWrappers.get(index);
+ Place place = rowWrapper.getMetadata().getPlace();
+ if (place != null) {
+ builder.add(place);
+ }
+ }
+
+ ImmutableList<Place> places = builder.build();
+ L.v(LogTags.TEMPLATE, "Updating %d visible places", places.size());
+ requireNonNull(templateContext.getAppHostService(LocationMediator.class))
+ .setCurrentPlaces(places);
+ }
+
+ @Nullable
+ private RowWrapper getRowWrapperIfValid(int index) {
+ // The user may click on a row that is transitioning out and the index here may be invalid
+ // for the new rows being transitioned in. Ignore those cases.
+ // Theoretically this means that we may trigger a click on a new row that was
+ // not clicked on (e.g. if the user double-taps really fast on the previously row), but that
+ // seems like a low-probability scenario in real HU so we are not doing extra checks here.
+ List<RowWrapper> rowWrappers = mRowList.getRowWrappers();
+ if (index >= rowWrappers.size()) {
+ return null;
+ }
+
+ return rowWrappers.get(index);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java
new file mode 100644
index 0000000..eda9af1
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/RowVisibilityObserver.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView.OnScrollListener;
+
+/**
+ * Observes the visibility change of the given {@link RecyclerView}.
+ *
+ * <p>Since we do not own the layout manager of {@link RecyclerView}, this class can be used to
+ * listen for the visibility changes of the recycler view due to scrolling.
+ */
+public class RowVisibilityObserver {
+
+ /** Listener for the item visibility changes. */
+ interface OnItemVisibilityChangedListener {
+
+ /** Callback when item visibility changes. */
+ void sendItemVisibilityChanged(int startIndexInclusive, int endIndexExclusive);
+ }
+
+ private static final int MSG_HANDLE_VISIBLE_ROWS_CHANGE = 1;
+ private static final int HANDLE_ROW_CHANGE_DELAY_MILLIS = 150;
+ private static final int INVALID_ROW_INDEX = Integer.MIN_VALUE;
+
+ @NonNull private final CarUiRecyclerView mRecyclerView;
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), new HandlerCallback());
+ private final OnScrollListener mOnScrollListener =
+ new CarUiRecyclerView.OnScrollListener() {
+ // Suppressing error for referencing handleVisibleRowsChange() before
+ // initialization of RowVisibilityObserver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onScrollStateChanged(CarUiRecyclerView recyclerView, int newState) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ handleVisibleRowsChange();
+ }
+ }
+
+ @Override
+ public void onScrolled(CarUiRecyclerView recyclerView, int dx, int dy) {}
+ };
+
+ private final OnLayoutChangeListener mOnLayoutChangeListener =
+ new OnLayoutChangeListener() {
+ // Suppressing error for referencing handleVisibleRowsChange() before
+ // initialization of RowVisibilityObserver and mReceyclerView being null.
+ @SuppressWarnings("nullness")
+ @Override
+ public void onLayoutChange(
+ View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+ if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
+ handleVisibleRowsChange();
+ }
+ }
+ };
+
+ @Nullable private OnItemVisibilityChangedListener mListener;
+ private int mFirstVisibleRowIndex;
+ private int mLastVisibleRowIndexExclusive;
+
+ /** Returns an instance of {@link RowVisibilityObserver}. */
+ public static RowVisibilityObserver create(@NonNull CarUiRecyclerView recyclerView) {
+ return new RowVisibilityObserver(requireNonNull(recyclerView));
+ }
+
+ /** Sets an {@link OnItemVisibilityChangedListener}. */
+ public void setOnItemVisibilityChangedListener(
+ @NonNull OnItemVisibilityChangedListener listener) {
+ requireNonNull(listener);
+
+ // Remove any existing listener.
+ removeOnItemVisibilityChangedListener();
+
+ mListener = listener;
+
+ // Reset the cached start/end indices, so that the newly-set listener will be invoked even
+ // if the visible rows might not have changed.
+ mFirstVisibleRowIndex = INVALID_ROW_INDEX;
+ mLastVisibleRowIndexExclusive = INVALID_ROW_INDEX;
+
+ mRecyclerView.addOnScrollListener(mOnScrollListener);
+ mRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener);
+ }
+
+ /** Removes any existing {@link OnItemVisibilityChangedListener}. */
+ public void removeOnItemVisibilityChangedListener() {
+ if (mListener == null) {
+ return;
+ }
+
+ mRecyclerView.removeOnScrollListener(mOnScrollListener);
+ mRecyclerView.removeOnLayoutChangeListener(mOnLayoutChangeListener);
+ mListener = null;
+ }
+
+ /** Creates a {@link RowVisibilityObserver} for given {@link RecyclerView}. */
+ private RowVisibilityObserver(@NonNull CarUiRecyclerView recyclerView) {
+ mRecyclerView = requireNonNull(recyclerView);
+
+ // Start with an invalid index, so that the newly-set listener will be invoked.
+ mFirstVisibleRowIndex = INVALID_ROW_INDEX;
+ mLastVisibleRowIndexExclusive = INVALID_ROW_INDEX;
+ }
+
+
+ /** Sends a message to the handler to publish item visibility change event. */
+ private void handleVisibleRowsChange() {
+ mHandler.removeMessages(MSG_HANDLE_VISIBLE_ROWS_CHANGE);
+ Message message = mHandler.obtainMessage(MSG_HANDLE_VISIBLE_ROWS_CHANGE);
+ if (mRecyclerView.getRecyclerViewChildCount() == 0) {
+ // When a full data refresh happens in the adapter that backs the recycler view, the
+ // view reports no visible items first for a few milliseconds, and then reports the new
+ // updated items.
+ // This ephemeral state of emptiness can cause flickering for the views that listen to
+ // the published events (e.g. the map view which clears and renders pins in the map).
+ // This is a work around by adding a short delay before sending the item visibility
+ // change event.
+ // TODO(b/183989613): Possibly remove once list diffing is implemented.
+ mHandler.sendMessageDelayed(message, HANDLE_ROW_CHANGE_DELAY_MILLIS);
+ } else {
+ mHandler.sendMessage(message);
+ }
+ }
+
+ /** A {@link Handler.Callback} used to process the message queue for the visibility events. */
+ private class HandlerCallback implements Handler.Callback {
+
+ /** Publishes the item visibility changed event to the listener. */
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_HANDLE_VISIBLE_ROWS_CHANGE) {
+ int firstVisibleRowIndex = mRecyclerView.findFirstCompletelyVisibleItemPosition();
+ int lastVisibleRowIndex = mRecyclerView.findLastCompletelyVisibleItemPosition();
+ int lastVisibleRowIndexExclusive = lastVisibleRowIndex + 1;
+
+ L.d(
+ LogTags.TEMPLATE,
+ "Handling visible rows in range (%d, %d)",
+ firstVisibleRowIndex,
+ lastVisibleRowIndexExclusive);
+
+ if (firstVisibleRowIndex == mFirstVisibleRowIndex
+ && lastVisibleRowIndexExclusive == mLastVisibleRowIndexExclusive) {
+ return true;
+ }
+
+ if (mListener != null) {
+ mListener.sendItemVisibilityChanged(firstVisibleRowIndex, lastVisibleRowIndexExclusive);
+ }
+
+ mFirstVisibleRowIndex = firstVisibleRowIndex;
+ mLastVisibleRowIndexExclusive = lastVisibleRowIndexExclusive;
+
+ return true;
+ } else {
+ L.w(LogTags.TEMPLATE, "Unknown message: %s", msg);
+ }
+ return false;
+ }
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java
new file mode 100644
index 0000000..4154695
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/SearchHeaderView.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static com.google.common.base.Strings.nullToEmpty;
+
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.SearchCallbackDelegate;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.ui.core.CarUi;
+import com.android.car.ui.toolbar.SearchMode;
+import com.android.car.ui.toolbar.ToolbarController;
+
+/** A view that displays the header for the search templates. */
+public class SearchHeaderView extends AbstractHeaderView {
+ private final CarEditTextWrapper mEditableSearchBar;
+ private final EditText mSearchBar;
+
+ private SearchHeaderView(
+ TemplateContext templateContext,
+ ToolbarController toolbarController,
+ View rootView,
+ @Nullable String initialSearchText,
+ @Nullable SearchCallbackDelegate searchCallbackDelegate,
+ boolean keyboardOpened) {
+ super(templateContext, toolbarController);
+
+ InputManager mInputManager = templateContext.getInputManager();
+ mToolbarController.setSearchMode(SearchMode.SEARCH);
+ mSearchBar = rootView.requireViewById(com.android.car.ui.R.id.car_ui_toolbar_search_bar);
+ mEditableSearchBar = new CarEditTextWrapper(mSearchBar, mInputManager);
+
+ toolbarController.setSearchQuery(nullToEmpty(initialSearchText));
+
+ if (searchCallbackDelegate != null) {
+ mToolbarController.registerOnSearchListener(
+ query ->
+ templateContext
+ .getAppDispatcher()
+ .dispatchSearchTextChanged(searchCallbackDelegate, query));
+
+ toolbarController.registerOnSearchCompletedListener(
+ () -> {
+ String query = mSearchBar.getText().toString();
+ templateContext
+ .getAppDispatcher()
+ .dispatchSearchSubmitted(searchCallbackDelegate, query);
+ });
+ }
+
+ if (keyboardOpened) {
+ mInputManager.startInput(mEditableSearchBar);
+ }
+
+ // TODO(b/179220417): Handle disabling search while driving
+ }
+
+ /** Returns the searchBar of the header */
+ public EditText getSearchBar() {
+ return mSearchBar;
+ }
+
+ /** Returns the {@link InputConnection} for the search bar. */
+ public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+ return mEditableSearchBar.onCreateInputConnection(editorInfo);
+ }
+
+ /** Updates the optional button in the header. */
+ @Override
+ public void setAction(@Nullable Action action) {
+ super.setAction(action);
+ }
+
+ /** Updates the {@link ActionStrip} associated with this toolbar */
+ @Override
+ public void setActionStrip(@Nullable ActionStrip actionStrip, ActionsConstraints constraints) {
+ super.setActionStrip(actionStrip, constraints);
+ boolean hasMenuItems = actionStrip != null && !actionStrip.getActions().isEmpty();
+ mToolbarController.setShowMenuItemsWhileSearching(hasMenuItems);
+ }
+
+ /** Updates the search hint. */
+ public void setHint(@Nullable String searchHint) {
+ mToolbarController.setSearchHint(searchHint != null ? searchHint : "");
+ }
+
+ /** Installs a {@link HeaderView} around the given container view */
+ @SuppressWarnings("nullness:argument") // InsetsChangedListener is nullable.
+ public static SearchHeaderView install(
+ TemplateContext templateContext,
+ View container,
+ View rootView,
+ @Nullable String initialSearchText,
+ @Nullable SearchCallbackDelegate searchCallbackDelegate,
+ boolean keyboardOpened) {
+ ToolbarController toolbarController = CarUi.installBaseLayoutAround(container, null, true);
+ if (toolbarController == null) {
+ throw new NullPointerException("Toolbar Controller could not be created.");
+ }
+ return new SearchHeaderView(
+ templateContext,
+ toolbarController,
+ rootView,
+ initialSearchText,
+ searchCallbackDelegate,
+ keyboardOpened);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java
new file mode 100644
index 0000000..b93aea3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/StringReplacementSpan.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import android.text.style.ReplacementSpan;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * A simple class for replacement the text that this span is attached to with the given replacement.
+ */
+public class StringReplacementSpan extends ReplacementSpan {
+
+ private final String mReplacementText;
+
+ public StringReplacementSpan(String text) {
+ mReplacementText = text;
+ }
+
+ /** Returns the replacement string for replacing the attached text. */
+ @VisibleForTesting
+ public String getReplacementText() {
+ return mReplacementText;
+ }
+
+ @Override
+ public int getSize(
+ Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
+ Rect bounds = new Rect();
+ paint.getTextBounds(mReplacementText, 0, mReplacementText.length(), bounds);
+ return bounds.width();
+ }
+
+ @Override
+ public void draw(
+ Canvas canvas,
+ CharSequence text,
+ int start,
+ int end,
+ float x,
+ int top,
+ int y,
+ int bottom,
+ Paint paint) {
+ canvas.drawText(mReplacementText, x, y, paint);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java
new file mode 100644
index 0000000..a9a98d4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/ViewUtils.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.templates.host.view.widgets.common;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Utility class for view operations. */
+public class ViewUtils {
+ private ViewUtils() {}
+
+ /** A {@link TouchDelegate} that allows combining multiple {@link TouchDelegate}s into one */
+ private static class TouchDelegateComposite extends TouchDelegate {
+ private static class TouchDelegateInfo {
+ final TouchDelegate mTouchDelegate;
+ @Nullable final WeakReference<View> mTargetView;
+
+ TouchDelegateInfo(TouchDelegate touchDelegate, @Nullable View targetView) {
+ mTouchDelegate = touchDelegate;
+ mTargetView = targetView != null ? new WeakReference<>(targetView) : null;
+ }
+ }
+
+ private final List<TouchDelegateInfo> delegates = new ArrayList<>();
+
+ private static final Rect emptyRect = new Rect();
+
+ public TouchDelegateComposite(View view) {
+ super(emptyRect, view);
+ }
+
+ public void addDelegate(TouchDelegate delegate, @Nullable View targetView) {
+ if (delegate != null) {
+ delegates.add(new TouchDelegateInfo(delegate, targetView));
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean res = false;
+ float x = event.getX();
+ float y = event.getY();
+ for (TouchDelegateInfo delegateInfo : delegates) {
+ event.setLocation(x, y);
+ if (delegateInfo.mTargetView != null && delegateInfo.mTargetView.get() == null) {
+ throw new IllegalStateException("Invalid touch delegation, target view has be removed");
+ }
+ res = delegateInfo.mTouchDelegate.onTouchEvent(event) || res;
+ }
+ return res;
+ }
+ }
+
+ // Returns true if {@code child} is a descendant of {@code parent}.
+ private static boolean isDescendant(View parent, View child) {
+ View current = child;
+ while (current != null) {
+ if (current == parent) {
+ return true;
+ }
+ if (!(current.getParent() instanceof View)) {
+ return false;
+ }
+ current = (View) current.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Sets the tap target for the given view to encompass at least the area of a square of the given
+ * dimensions.
+ *
+ * <p>If the current tap area is already larger in either dimension, method will not shrink it
+ * (hence "min" tap target).
+ *
+ * <p>If the current tap area is smaller, method will expand it equally on either side to meet the
+ * minimum size.
+ *
+ * <p><b>Important: This method works by adding a {@link TouchDelegate} to the container view.</b>
+ * The caller must make sure this method is invoked only once per view. Otherwise, multiple {@link
+ * TouchDelegate} instances will be added to the container, which could cause duplicate click
+ * events.
+ *
+ * @param containerView the view where a {@link TouchDelegate} will be added.
+ * @param view the view to potentially expand the tap target for.
+ * @param tapTargetSize the dimensions of a square that will become the new minimum tap target for
+ * the given view.
+ */
+ public static void setMinTapTarget(ViewGroup containerView, View view, int tapTargetSize) {
+ containerView.post(
+ () -> {
+ // Return if the view has already been removed from the view hierarchy or has unexpected
+ // parent.
+ if (!(view.getParent() instanceof View)
+ || !isDescendant(containerView, (View) view.getParent())) {
+ L.d(LogTags.TEMPLATE, "Cannot set min tap target for view %s", view);
+ return;
+ }
+
+ Rect rect = new Rect();
+ view.getHitRect(rect);
+ containerView.offsetDescendantRectToMyCoords((View) view.getParent(), rect);
+
+ int rectHeight = rect.height();
+ if (rectHeight < tapTargetSize) {
+ int delta = (tapTargetSize - rectHeight) / 2;
+ rect.top -= delta;
+ rect.bottom += delta;
+ }
+
+ int rectWidth = rect.width();
+ if (rectWidth < tapTargetSize) {
+ int delta = (tapTargetSize - rectWidth) / 2;
+ rect.left -= delta;
+ rect.right += delta;
+ }
+
+ TouchDelegate parentTouchDelegate = containerView.getTouchDelegate();
+ TouchDelegate newDelegate = new TouchDelegate(rect, view);
+ if (parentTouchDelegate != null) {
+ if (parentTouchDelegate instanceof TouchDelegateComposite) {
+ ((TouchDelegateComposite) parentTouchDelegate).addDelegate(newDelegate, view);
+ newDelegate = parentTouchDelegate;
+ } else {
+ TouchDelegateComposite composite = new TouchDelegateComposite(view);
+ composite.addDelegate(parentTouchDelegate, null);
+ composite.addDelegate(newDelegate, view);
+ newDelegate = composite;
+ }
+ }
+ containerView.setTouchDelegate(newDelegate);
+ });
+ }
+
+ /**
+ * Enforce the minimum and maximum size limit to the given view.
+ *
+ * <p>The view width and height sizes must be equal.
+ */
+ public static void enforceViewSizeLimit(View view, int minSize, int maxSize) {
+ enforceViewSizeLimit(
+ view,
+ /* minWidth= */ minSize,
+ /* maxWidth= */ maxSize,
+ /* minHeight= */ minSize,
+ /* maxHeight= */ maxSize);
+ }
+
+ /** Enforce the minimum and maximum width and height limits to the given view. */
+ public static void enforceViewSizeLimit(
+ View view, int minWidth, int maxWidth, int minHeight, int maxHeight) {
+ LayoutParams layoutParams = view.getLayoutParams();
+ if (layoutParams == null) {
+ return;
+ }
+
+ int width = getValueInRange(layoutParams.width, minWidth, maxWidth);
+ int height = getValueInRange(layoutParams.height, minHeight, maxHeight);
+ layoutParams.width = width;
+ layoutParams.height = height;
+ view.setLayoutParams(layoutParams);
+ }
+
+ /** Logs a telemetry event with the given {@link UiAction} and {@link TemplateContext} */
+ public static void logCarAppTelemetry(TemplateContext templateContext, UiAction action) {
+ logCarAppTelemetry(
+ templateContext,
+ TelemetryEvent.newBuilder(action)
+ .setComponentName(templateContext.getCarAppPackageInfo().getComponentName()));
+ }
+
+ /**
+ * Logs a telemetry event with the given {@link UiAction}, action count and {@link
+ * TemplateContext}
+ */
+ public static void logCarAppTelemetry(
+ TemplateContext templateContext, UiAction action, int actionCount) {
+ logCarAppTelemetry(
+ templateContext,
+ TelemetryEvent.newBuilder(action)
+ .setComponentName(templateContext.getCarAppPackageInfo().getComponentName())
+ .setItemsLoadedCount(actionCount));
+ }
+
+ /**
+ * Logs a telemetry event with the given {@link TelemetryEvent.Builder} and {@link
+ * TemplateContext}
+ */
+ public static void logCarAppTelemetry(
+ TemplateContext templateContext, TelemetryEvent.Builder builder) {
+ TelemetryHandler telemetry = templateContext.getTelemetryHandler();
+ telemetry.logCarAppTelemetry(builder);
+ }
+
+ /**
+ * Returns the capped value between the min and max range.
+ *
+ * <p>If the given value is less than or equal to 0 (e.g. MATCH_CONSTRAINT (0), MATCH_PARENT (-1),
+ * or WRAP_CONTENT (-2)), the original value will be returned.
+ */
+ private static int getValueInRange(int value, int min, int max) {
+ if (value <= 0) {
+ return value;
+ }
+
+ int newValue = value;
+ newValue = min(newValue, max);
+ newValue = max(newValue, min);
+ return newValue;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml
new file mode 100644
index 0000000..5a8ca7b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_in.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="scaleX"
+ android:valueType="floatType"
+ android:valueFrom=".7f"
+ android:valueTo="1f"/>
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="scaleY"
+ android:valueType="floatType"
+ android:valueFrom=".7f"
+ android:valueTo="1f"/>
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="alpha"
+ android:valueType="floatType"
+ android:valueFrom="0f"
+ android:valueTo="1f"/>
+</set>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml
new file mode 100644
index 0000000..33f5c80
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/anim/fab_view_animation_fade_out.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="scaleX"
+ android:valueType="floatType"
+ android:valueFrom="1f"
+ android:valueTo=".7f"/>
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="scaleY"
+ android:valueType="floatType"
+ android:valueFrom="1f"
+ android:valueTo=".7f"/>
+ <objectAnimator
+ android:duration="@integer/action_strip_animation_duration_millis"
+ android:propertyName="alpha"
+ android:valueType="floatType"
+ android:valueFrom="1f"
+ android:valueTo="0f"/>
+</set>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml
new file mode 100644
index 0000000..debd0a3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="50dp"
+ android:viewportWidth="36"
+ android:viewportHeight="50">
+ <path
+ android:pathData="M28.2,32c5.2,-4.9 6.8,-8.3 6.8,-14c0,-9.4 -7.6,-17 -17,-17S1,8.6 1,18c0,5.6 1.6,9 6.7,14l1.2,1c5,4.9 6.7,8.2 7.3,14.1c0.1,1.1 0.8,1.8 1.8,1.8c1,0 1.7,-0.7 1.8,-1.8c0.5,-5.8 2.2,-9.1 7.2,-14L28.2,32z"
+ android:strokeWidth="0"
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml
new file mode 100644
index 0000000..063b6eb
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_border.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="50dp"
+ android:viewportWidth="36"
+ android:viewportHeight="50">
+ <path
+ android:pathData="M18,49.9C16.5,49.9 15.3,48.8 15.2,47.2C14.7,41.6 13.1,38.5 8.3,33.8L7,32.7C1.7,27.5 0,23.9 0,18C0,8.1 8.1,0 18,0C27.9,0 36,8.1 36,18C36,24 34.3,27.6 28.9,32.7L28.6,33L27.7,33.8C22.9,38.5 21.3,41.6 20.8,47.2C20.6,48.8 19.5,49.9 18,49.9ZM8.3,31.2L8.7,31.5L9.7,32.4C14.9,37.5 16.7,41 17.2,47.1C17.2,47.6 17.5,48 18,48C18.6,48 18.8,47.4 18.8,47.1C19.3,41.1 21.2,37.5 26.3,32.5L27.5,31.4C32.5,26.6 34,23.6 34,18.1C34,9.3 26.8,2.1 18,2.1C9.2,2.1 2,9.2 2,18C2,23.3 3.5,26.4 8.3,31.2Z"
+ android:strokeWidth="1"
+ android:fillColor="#80868B"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml
new file mode 100644
index 0000000..9953ce3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/anchor_marker_circle.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="50dp"
+ android:viewportWidth="36"
+ android:viewportHeight="50">
+ <path
+ android:pathData="M18,18m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
+ android:strokeWidth="1"
+ android:fillColor="#80868B"
+ android:fillType="nonZero"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml
new file mode 100644
index 0000000..89d860b
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/empty.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- This is a blank shape, 0x0 in size, that works around the fact that the
+ android:textSelectHandle xml property requires a drawable with a defined size. -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <size
+ android:width="0dp"
+ android:height="0dp" />
+</shape>
+
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml
new file mode 100644
index 0000000..9393c31
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_error.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector android:height="64dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FFFFFF" android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M12,2L1,21h22L12,2zM13,16h-2v2h2v-2zM13,10h-2v4h2v-4z"/>
+</vector>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml
new file mode 100644
index 0000000..79933ad
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/drawable/ic_pan_overlay.xml
@@ -0,0 +1,34 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="290dp"
+ android:height="258dp"
+ android:viewportWidth="290"
+ android:viewportHeight="258">
+ <path
+ android:pathData="M244.268,130.768C245.244,129.791 245.244,128.209 244.268,127.232L228.358,111.322C227.382,110.346 225.799,110.346 224.822,111.322C223.846,112.299 223.846,113.881 224.822,114.858L238.964,129L224.822,143.142C223.846,144.118 223.846,145.701 224.822,146.678C225.799,147.654 227.381,147.654 228.358,146.678L244.268,130.768ZM241,131.5L242.5,131.5L242.5,126.5L241,126.5L241,131.5Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M54.232,127.232C53.256,128.208 53.256,129.791 54.232,130.768L70.142,146.678C71.118,147.654 72.701,147.654 73.677,146.678C74.654,145.702 74.654,144.119 73.677,143.143L59.535,129L73.678,114.858C74.654,113.882 74.655,112.299 73.678,111.323C72.702,110.347 71.119,110.346 70.143,111.323L54.232,127.232ZM57.5,126.5L56,126.5L56,131.5L57.5,131.5L57.5,126.5Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M147.232,224.268C148.209,225.244 149.791,225.244 150.768,224.268L166.678,208.358C167.654,207.381 167.654,205.799 166.678,204.822C165.701,203.846 164.118,203.846 163.142,204.822L149,218.964L134.858,204.822C133.881,203.846 132.299,203.846 131.322,204.822C130.346,205.799 130.346,207.382 131.322,208.358L147.232,224.268ZM146.5,221L146.5,222.5L151.5,222.5L151.5,221L146.5,221Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M150.768,34.232C149.791,33.256 148.209,33.256 147.232,34.232L131.322,50.142C130.346,51.118 130.346,52.701 131.322,53.678C132.299,54.654 133.882,54.654 134.858,53.678L149,39.535L163.142,53.678C164.118,54.654 165.701,54.654 166.678,53.678C167.654,52.701 167.654,51.118 166.678,50.142L150.768,34.232ZM151.5,39L151.5,36L146.5,36L146.5,39L151.5,39Z"
+ android:fillColor="#000000"/>
+</vector>
+
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml
new file mode 100644
index 0000000..171f33a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_row.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView
+ android:id="@+id/action_button_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templatePlainContentHorizontalPadding"
+ android:layout_marginVertical="?templateActionButtonListRowVerticalSpacing"
+ android:orientation="horizontal"
+ android:gravity="center" />
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml
new file mode 100644
index 0000000..d9fdc29
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_list_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templatePlainContentHorizontalPadding">
+</com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml
new file mode 100644
index 0000000..1a8fd6d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="?templateActionButtonHeight"
+ style="?templateActionButtonStyle"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml
new file mode 100644
index 0000000..c178a8f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/action_icon"
+ android:layout_width="?templateActionIconSize"
+ android:layout_height="?templateActionIconSize"
+ app:imageMinWidth="?templateActionIconSizeMin"
+ app:imageMaxWidth="?templateActionIconSizeMax"
+ app:imageMinHeight="?templateActionIconSizeMin"
+ app:imageMaxHeight="?templateActionIconSizeMax"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml
new file mode 100644
index 0000000..7d4bdce
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_icon_text.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingStart="?templateActionIconTextStartSpacing"
+ android:paddingEnd="?templateActionIconTextEndSpacing">
+
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/action_icon"
+ android:layout_width="?templateActionIconSize"
+ android:layout_height="?templateActionIconSize"
+ app:imageMinWidth="?templateActionIconSizeMin"
+ app:imageMaxWidth="?templateActionIconSizeMax"
+ app:imageMinHeight="?templateActionIconSizeMin"
+ app:imageMaxHeight="?templateActionIconSizeMax"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+
+ <CarUiTextView
+ android:id="@+id/action_text"
+ style="?templateActionButtonTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginStart="?templateActionIconToTextSpacing"
+ android:maxEms="?templateActionButtonTextMaxEmsWithIcon"/>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml
new file mode 100644
index 0000000..c5b82d4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_button_view_text.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/action_text"
+ style="?templateActionButtonTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templateActionTextHorizontalSpacing"
+ android:layout_gravity="center"
+ android:maxEms="?templateActionButtonTextMaxEmsNoIcon"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml
new file mode 100644
index 0000000..1a75313
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/action_strip_view_floating.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+An action strip with floating buttons that anchors to the top right of the
+screen, meant to be used in conjunction with half-screen, card-style templates.
+
+IMPORTANT: parents of this view should have clipChildren set to false so that
+the shadows don't get clipped.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.ActionStripView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:visibility="gone"
+ android:clipChildren="false"
+ app:fabAppearance="?templateActionStripFabAppearance">
+
+ <LinearLayout
+ android:id="@+id/action_strip_touch_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="?templateActionStripPadding"
+ android:orientation="vertical">
+ <LinearLayout
+ android:id="@+id/action_strip_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:layout_gravity="center_vertical|end"
+ android:orientation="horizontal" />
+
+ <LinearLayout
+ android:id="@+id/action_strip_container_secondary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:layout_gravity="center_vertical|end"
+ android:orientation="horizontal" />
+ </LinearLayout>
+
+</com.android.car.libraries.templates.host.view.widgets.common.ActionStripView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml
new file mode 100644
index 0000000..848f4ba
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_container.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- The card has a minimum and maximum heights, the latter never going past
+ the screen height. -->
+<com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?templateCardContentContainerStyle"
+ android:visibility="gone"
+ android:focusable="false"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintHeight_default="wrap"
+ app:layout_constraintHeight_min="?templateCardContentContainerMinHeight"
+ android:layout_marginStart="?templateCardContentContainerStartMargin"
+ android:layout_marginTop="?templateCardContentContainerTopMargin"
+ android:layout_width="?templateCardContentContainerDefaultWidth"
+ android:layout_height="0dp"
+ tools:ignore="MissingClass">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <include layout="@layout/card_header_layout"/>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="?templateDividerThickness"
+ android:background="?templateDividerColor"/>
+ <include
+ layout="@layout/content_view"
+ android:id="@+id/content_view"/>
+ </LinearLayout>
+</com.android.car.libraries.templates.host.view.widgets.common.BleedingCardView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml
new file mode 100644
index 0000000..6540a2c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/card_header_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ tools:ignore="MergeRootFrame">
+ <com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView
+ android:id="@+id/header_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:descendantFocusability="afterDescendants"/>
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml
new file mode 100644
index 0000000..2aad758
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/clickable_span_text_container.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:descendantFocusability="afterDescendants" >
+
+ <CarUiTextView
+ android:id="@+id/clickable_span_text_view"
+ style="?templateSignInAdditionalTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true" />
+
+</com.android.car.libraries.templates.host.view.widgets.common.ClickableSpanTextContainer>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml
new file mode 100644
index 0000000..2399b86
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/content_view.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.ContentView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <!-- A container for the content views's content. -->
+ <com.android.car.ui.FocusArea
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:clipChildren="true"
+ android:layout_weight="1"/>
+
+</com.android.car.libraries.templates.host.view.widgets.common.ContentView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml
new file mode 100644
index 0000000..090ebf5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/driving_message_view.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/driving_message_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:background="@color/template_black"
+ android:visibility="gone">
+ <!-- An icon shown on top of the contents. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/driving_message_icon"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinWidth="?templateLargeImageSizeMin"
+ app:imageMaxWidth="?templateLargeImageSizeMax"
+ app:imageMinHeight="?templateLargeImageSizeMin"
+ app:imageMaxHeight="?templateLargeImageSizeMax"
+ android:src="@drawable/ic_error"
+ tools:ignore="ContentDescription" />
+
+ <!-- The title displayed below the icon. -->
+ <CarUiTextView
+ android:id="@+id/driving_message_text"
+ style="?templateMessageTitleTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="?templateMessageTitleTopSpacing"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml
new file mode 100644
index 0000000..c638c3f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_icon_text.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?templateActionIconTextStartSpacing"
+ android:layout_marginEnd="?templateActionIconTextEndSpacing">
+
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/action_icon"
+ android:layout_width="?templateActionIconSize"
+ android:layout_height="?templateActionIconSize"
+ app:imageMinWidth="?templateActionIconSizeMin"
+ app:imageMaxWidth="?templateActionIconSizeMax"
+ app:imageMinHeight="?templateActionIconSizeMin"
+ app:imageMaxHeight="?templateActionIconSizeMax"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+
+ <CarUiTextView
+ android:id="@+id/action_text"
+ style="?templateActionButtonTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginStart="?templateActionIconToTextSpacing"
+ android:maxEms="?templateFabTextMaxEmsWithIcon"/>
+</LinearLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml
new file mode 100644
index 0000000..3bc77c8
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/action_text"
+ style="?templateActionButtonTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templateActionTextHorizontalSpacing"
+ android:layout_gravity="center"
+ android:maxEms="?templateFabTextMaxEmsNoIcon"/>
+
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml
new file mode 100644
index 0000000..826c98f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/fab_view_text_no_icon.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CarUiTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/action_text"
+ style="?templateActionButtonTextStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingStart="@dimen/template_padding_5"
+ android:paddingEnd="@dimen/template_padding_5"
+ android:maxEms="?templateFabTextMaxEmsNoIcon"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml
new file mode 100644
index 0000000..6855f5c
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_no_divider_view.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.RowListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:markerAppearance="?templateListMarkerAppearance">
+
+ <FrameLayout
+ android:id="@+id/progress_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:minHeight="?templateRowMinHeight"
+ android:focusable="true"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinSize="?templateLargeImageSizeMin"
+ app:imageMaxSize="?templateLargeImageSizeMax"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <!-- A text view displaying a message when the list is empty. -->
+ <CarUiTextView
+ style="?templateRowListEmptyTextStyle"
+ android:id="@+id/list_no_items_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:clipToPadding="true"
+ app:carUiSize="large"
+ app:layoutStyle="linear"
+ app:enableDivider="false" />
+
+ <FrameLayout
+ android:id="@+id/large_image_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:clickable="false"
+ android:focusable="false"
+ android:visibility="gone">
+ <!--
+ We programmatically calculate the width and height of the wrapping
+ large_image_container, and allow the ImageView to stretch to fill the
+ container width if needed while maintaining the source's aspect ratio.
+ -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/large_image"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|center"
+ android:clickable="false"
+ android:focusable="false"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ tools:ignore="ContentDescription" />
+ </FrameLayout>
+
+</com.android.car.libraries.templates.host.view.widgets.common.RowListView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml
new file mode 100644
index 0000000..581b88d
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_row_view.xml
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:tag="carUiListItem">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ app:layout_constraintEnd_toStartOf="@id/large_image_spacer"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/car_ui_list_item_height">
+
+ <!-- The following touch interceptor views are sized to encompass the specific sub-sections of
+ the list item view to easily control the bounds of a background ripple effects. -->
+ <View
+ android:id="@+id/car_ui_list_item_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <!-- This touch interceptor does not include the action container -->
+ <View
+ android:id="@+id/car_ui_list_item_reduced_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_start_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="?templateFullRowStartPadding" />
+
+ <FrameLayout
+ android:id="@+id/car_ui_list_item_icon_container"
+ android:layout_width="@dimen/car_ui_list_item_icon_container_width"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="@+id/car_ui_list_item_start_guideline"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_icon"
+ android:layout_width="@dimen/car_ui_list_item_icon_size"
+ android:layout_height="@dimen/car_ui_list_item_icon_size"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_content_icon"
+ android:layout_width="@dimen/car_ui_list_item_content_icon_width"
+ android:layout_height="@dimen/car_ui_list_item_content_icon_height"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_avatar_icon"
+ android:background="@drawable/car_ui_list_item_avatar_icon_outline"
+ android:layout_width="@dimen/car_ui_list_item_avatar_icon_width"
+ android:layout_height="@dimen/car_ui_list_item_avatar_icon_height"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+ </FrameLayout>
+
+ <CarUiTextView
+ android:id="@+id/car_ui_list_item_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+ style="?templateRowTitleStyle"
+ android:layout_marginTop="@dimen/car_ui_padding_2"
+ app:layout_constraintBottom_toTopOf="@+id/car_ui_list_item_body"
+ app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_goneMarginBottom="@dimen/car_ui_padding_2"
+ app:layout_goneMarginStart="0dp"/>
+ <CarUiTextView
+ android:id="@+id/car_ui_list_item_body"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+ style="?templateRowSecondaryTextStyle"
+ android:layout_marginBottom="@dimen/car_ui_padding_2"
+ android:textAlignment="viewStart"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container"
+ app:layout_constraintTop_toBottomOf="@+id/car_ui_list_item_title"
+ app:layout_goneMarginTop="@dimen/car_ui_padding_2"
+ app:layout_goneMarginStart="0dp"/>
+
+ <!-- This touch interceptor is sized and positioned to encompass the action container -->
+ <View
+ android:id="@+id/car_ui_list_item_action_container_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" />
+
+ <FrameLayout
+ android:id="@+id/car_ui_list_item_action_container"
+ android:layout_width="wrap_content"
+ android:minWidth="@dimen/car_ui_list_item_icon_container_width"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <Switch
+ android:id="@+id/car_ui_list_item_switch_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <CheckBox
+ android:id="@+id/car_ui_list_item_checkbox_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <RadioButton
+ android:id="@+id/car_ui_list_item_radio_button_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_supplemental_icon"
+ android:layout_width="?templateFullRowChevronWidth"
+ android:layout_height="?templateFullRowChevronHeight"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription"/>
+ </FrameLayout>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="?templateFullRowEndPadding" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <Space
+ android:id="@+id/large_image_spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintWidth_percent="?templateRowListToLargeImageRatio"
+ app:layout_constraintWidth_default="percent"
+ app:layout_constraintWidth_max="?templateRowListLargeImageContainerMaxWidth"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml
new file mode 100644
index 0000000..2cadd94
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_list_view.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.RowListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:markerAppearance="?templateListMarkerAppearance">
+
+ <FrameLayout
+ android:id="@+id/progress_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:minHeight="?templateRowMinHeight"
+ android:focusable="true"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinSize="?templateLargeImageSizeMin"
+ app:imageMaxSize="?templateLargeImageSizeMax"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <!-- A text view displaying a message when the list is empty. -->
+ <CarUiTextView
+ style="?templateRowListEmptyTextStyle"
+ android:id="@+id/list_no_items_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:clipToPadding="true"
+ app:carUiSize="large"
+ app:layoutStyle="linear"
+ app:enableDivider="true" />
+
+ <FrameLayout
+ android:id="@+id/large_image_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:clickable="false"
+ android:focusable="false"
+ android:visibility="gone">
+ <!--
+ We programmatically calculate the width and height of the wrapping
+ large_image_container, and allow the ImageView to stretch to fill the
+ container width if needed while maintaining the source's aspect ratio.
+ -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/large_image"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|center"
+ android:clickable="false"
+ android:focusable="false"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ tools:ignore="ContentDescription" />
+ </FrameLayout>
+
+</com.android.car.libraries.templates.host.view.widgets.common.RowListView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml
new file mode 100644
index 0000000..44a8ef3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/full_screen_header_layout.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="?templateHeaderHeight"
+ android:paddingStart="@dimen/template_edge_column_margin"
+ android:paddingEnd="@dimen/template_edge_column_margin"
+ android:focusable="false"
+ tools:ignore="MergeRootFrame">
+ <com.android.car.libraries.templates.host.view.widgets.common.CardHeaderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/header_view"
+ android:layout_width="match_parent"
+ android:layout_height="?templateHeaderHeight"
+ android:focusable="false"
+ android:descendantFocusability="afterDescendants"/>
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml
new file mode 100644
index 0000000..7700102
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_item_view.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.GridItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="?templateGridItemVerticalSpacing"
+ android:paddingHorizontal="?templateGridItemHorizontalSpacing"
+ android:focusable="true"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/grid_item_image_container"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ android:layout_marginBottom="?templateGridItemImageBottomPadding">
+ <!-- The loading spinner. -->
+ <ProgressBar
+ android:id="@+id/grid_item_progress_bar"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center" />
+
+ <!-- The grid item image or icon. -->
+ <ImageView
+ android:id="@+id/grid_item_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription" />
+ </LinearLayout>
+
+ <!-- A container with the title and a secondary text line. -->
+ <LinearLayout
+ android:id="@+id/grid_item_text_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <CarUiTextView
+ android:id="@+id/grid_item_title"
+ style="?templateGridItemTitleStyle"
+ android:layout_marginBottom="?templateGridItemTextBottomPadding"
+ android:visibility="gone"
+ android:maxWidth="?templateGridItemTextContainerMaxWidth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"/>
+
+ <CarUiTextView
+ android:id="@+id/grid_item_text"
+ style="?templateGridItemTextStyle"
+ android:visibility="gone"
+ android:maxWidth="?templateGridItemTextContainerMaxWidth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"/>
+ </LinearLayout>
+</com.android.car.libraries.templates.host.view.widgets.common.GridItemView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml
new file mode 100644
index 0000000..f9ba1f5
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/grid_view.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.GridView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:clipChildren="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/progress_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:focusable="true"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateLargeImageSize"
+ android:layout_height="?templateLargeImageSize"
+ app:imageMinSize="?templateLargeImageSizeMin"
+ app:imageMaxSize="?templateLargeImageSizeMax"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <!-- A text view displaying a message when the list is empty. -->
+ <CarUiTextView
+ style="?templateGridEmptyTextStyle"
+ android:id="@+id/list_no_items_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/grid_paged_list_view"
+ style="?templateGridStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingHorizontal="?templatePlainContentHorizontalPadding" />
+</com.android.car.libraries.templates.host.view.widgets.common.GridView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml
new file mode 100644
index 0000000..e15c9e6
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_row_view.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?templateHalfRowMinHeight"
+ android:tag="carUiListItem">
+
+ <!-- The following touch interceptor views are sized to encompass the specific sub-sections of
+ the list item view to easily control the bounds of a background ripple effects. -->
+ <View
+ android:id="@+id/car_ui_list_item_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <!-- This touch interceptor does not include the action container -->
+ <View
+ android:id="@+id/car_ui_list_item_reduced_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_start_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="?templateHalfRowHorizontalPadding" />
+
+ <FrameLayout
+ android:id="@+id/car_ui_list_item_icon_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="@+id/car_ui_list_item_start_guideline"
+ app:layout_constraintTop_toTopOf="@+id/car_ui_list_item_title" >
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_icon"
+ android:layout_width="?templateHalfRowImageSize"
+ android:layout_height="?templateHalfRowImageSize"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter" />
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_content_icon"
+ android:layout_width="?templateHalfRowImageSize"
+ android:layout_height="?templateHalfRowImageSize"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter" />
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_avatar_icon"
+ android:background="@drawable/car_ui_list_item_avatar_icon_outline"
+ android:layout_width="?templateHalfRowImageSize"
+ android:layout_height="?templateHalfRowImageSize"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:scaleType="fitCenter" />
+ </FrameLayout>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_top_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="?templateHalfRowVerticalPadding" />
+
+ <CarUiTextView
+ android:id="@+id/car_ui_list_item_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?templateHalfRowImageToTextSpacing"
+ style="?templateRowTitleStyle"
+ app:layout_constraintBottom_toTopOf="@+id/car_ui_list_item_body"
+ app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container"
+ app:layout_constraintTop_toTopOf="@+id/car_ui_list_item_top_guideline"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_goneMarginStart="@dimen/car_ui_padding_0" />
+ <CarUiTextView
+ android:id="@+id/car_ui_list_item_body"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="?templateHalfRowImageToTextSpacing"
+ android:layout_marginTop="?templateHalfRowTextToTextSpacing"
+ style="?templateRowSecondaryTextStyle"
+ app:layout_constraintBottom_toBottomOf="@+id/car_ui_list_item_bottom_guideline"
+ app:layout_constraintEnd_toStartOf="@+id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toEndOf="@+id/car_ui_list_item_icon_container"
+ app:layout_constraintTop_toBottomOf="@+id/car_ui_list_item_title"
+ app:layout_goneMarginStart="@dimen/car_ui_padding_0" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_bottom_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="?templateHalfRowVerticalPadding" />
+
+ <!-- This touch interceptor is sized and positioned to encompass the action container -->
+ <View
+ android:id="@+id/car_ui_list_item_action_container_touch_interceptor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/car_ui_list_item_background"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container"
+ app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" />
+
+ <FrameLayout
+ android:id="@+id/car_ui_list_item_action_container"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <Switch
+ android:id="@+id/car_ui_list_item_switch_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <CheckBox
+ android:id="@+id/car_ui_list_item_checkbox_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <RadioButton
+ android:id="@+id/car_ui_list_item_radio_button_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false" />
+
+ <ImageView
+ android:id="@+id/car_ui_list_item_supplemental_icon"
+ android:layout_width="?templateHalfRowImageSize"
+ android:layout_height="?templateHalfRowImageSize"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter" />
+ </FrameLayout>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/car_ui_list_item_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="?templateHalfRowHorizontalPadding" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml
new file mode 100644
index 0000000..4618e1e
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/half_list_view.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.RowListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:clipChildren="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:markerAppearance="?templateListMarkerAppearance"
+ app:listUseCompactRowLayout="true">
+
+ <FrameLayout
+ android:id="@+id/progress_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:minHeight="?templateRowMinHeight"
+ android:focusable="true"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateNavCardLargeImageSize"
+ android:layout_height="?templateNavCardLargeImageSize"
+ app:imageMinSize="?templateNavCardLargeImageSizeMin"
+ app:imageMaxSize="?templateNavCardLargeImageSizeMax"
+ android:layout_gravity="center" />
+ </FrameLayout>
+
+ <!-- A text view displaying a message when the list is empty. -->
+ <CarUiTextView
+ style="?templateRowListEmptyTextStyle"
+ android:id="@+id/list_no_items_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="?templateHalfListPaddingVertical"
+ android:layout_marginBottom="?templateHalfListPaddingVertical"
+ android:layout_marginHorizontal="?templateHalfRowHorizontalPadding"
+ android:foreground="@drawable/no_content_view_focus_ring" />
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:clipToPadding="true"
+ android:paddingBottom="?templateHalfListBottomPadding"
+ app:carUiSize="small"
+ app:layoutStyle="linear"
+ app:enableDivider="true" />
+
+</com.android.car.libraries.templates.host.view.widgets.common.RowListView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml
new file mode 100644
index 0000000..3742510
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/header_view.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- The header button icon is wrapped by a frame layout to define
+ the background layer with a focus selector and ripple effects etc. -->
+ <FrameLayout
+ android:id="@+id/header_button_container"
+ android:layout_width="?templateHeaderButtonContainerSize"
+ android:layout_height="?templateHeaderButtonContainerSize"
+ android:layout_marginStart="?templateHeaderButtonStartSpacing"
+ android:addStatesFromChildren="true"
+ android:background="?templateHeaderButtonBackground"
+ android:visibility="gone"
+ android:clickable="true"
+ android:focusable="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" >
+
+ <!-- The header action icon -->
+ <ImageView
+ android:id="@+id/header_icon"
+ android:layout_width="?templateHeaderButtonIconSize"
+ android:layout_height="?templateHeaderButtonIconSize"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:maxWidth="?templateHeaderButtonContainerSize"
+ android:maxHeight="?templateHeaderButtonContainerSize"
+ tools:ignore="ContentDescription"/>
+ </FrameLayout>
+
+ <!-- The header title -->
+ <CarUiTextView
+ android:id="@+id/header_title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_marginStart="?templateHeaderTextStartSpacing"
+ android:layout_marginEnd="?templateHeaderTextEndSpacing"
+ android:layout_marginVertical="?templateHeaderTextVerticalSpacing"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAlignment="textStart"
+ android:gravity="center_vertical|start"
+ android:textAppearance="?templateHeaderTextStyle"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/header_button_container"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/refresh_button_container"
+ app:layout_goneMarginStart="?templateHeaderTextNoIconStartSpacing" />
+
+ <!-- The optional refresh icon is wrapped by a frame layout to define
+ the background layer with a focus selector and ripple effects etc. -->
+ <FrameLayout
+ android:id="@+id/refresh_button_container"
+ android:layout_width="?templateHeaderButtonContainerSize"
+ android:layout_height="?templateHeaderButtonContainerSize"
+ android:layout_marginStart="?templateHeaderTextEndSpacing"
+ android:addStatesFromChildren="true"
+ android:background="?templateHeaderButtonBackground"
+ android:visibility="gone"
+ android:clickable="true"
+ android:focusable="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" >
+
+ <!-- The refresh icon -->
+ <ImageView
+ android:id="@+id/refresh_icon"
+ android:layout_width="?templateHeaderButtonIconSize"
+ android:layout_height="?templateHeaderButtonIconSize"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:maxWidth="?templateHeaderButtonContainerSize"
+ android:maxHeight="?templateHeaderButtonContainerSize"
+ tools:ignore="ContentDescription"/>
+ </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml
new file mode 100644
index 0000000..61b800f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/input_sign_in_view.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.InputSignInView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ style="?templateSignInInputViewStyle"
+ android:orientation="vertical" >
+
+ <!--
+ Use a 0x0 drawable for textSelectHandle; null drawable crashes, so does
+ transparent color. Also set textCursorDrawable to null because this forces
+ Android to render a cursor using the text color instead of not rendering
+ one at all.
+ -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarEditText
+ android:id="@+id/input_sign_in_box"
+ style="?templateEditTextStyle"
+ android:inputType="text"
+ android:imeOptions="actionGo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="@dimen/template_padding_1"
+ android:focusable="true"
+ android:focusableInTouchMode="false"
+ android:textCursorDrawable="@null"
+ android:textSelectHandle="@drawable/empty"
+ tools:ignore="RtlHardcoded,SpUsage" />
+
+ <CarUiTextView
+ android:id="@+id/input_sign_in_error_message"
+ style="?templateSignInErrorMessageStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+</com.android.car.libraries.templates.host.view.widgets.common.InputSignInView> \ No newline at end of file
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml
new file mode 100644
index 0000000..fd77064
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/map_action_strip_view_floating.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+An action strip with floating buttons that anchors to the top right of the
+screen, meant to be used in conjunction with half-screen, card-style templates.
+
+IMPORTANT: parents of this view should have clipChildren set to false so that
+the shadows don't get clipped.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.ActionStripView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:visibility="gone"
+ android:clipChildren="false"
+ app:fabAppearance="?templateActionStripFabAppearance"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <LinearLayout
+ android:id="@+id/action_strip_touch_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="?templateActionStripPadding"
+ android:orientation="horizontal">
+ <LinearLayout
+ android:id="@+id/action_strip_container_secondary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:layout_marginEnd="?templateActionStripButtonMargin"
+ android:layout_gravity="center_horizontal|end"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:id="@+id/action_strip_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:layout_gravity="center_horizontal|end"
+ android:orientation="vertical" />
+ </LinearLayout>
+</com.android.car.libraries.templates.host.view.widgets.common.ActionStripView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml
new file mode 100644
index 0000000..31384e8
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pan_overlay.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_pan_overlay"
+ tools:ignore="ContentDescription" />
+
+</com.android.car.libraries.templates.host.view.widgets.common.PanOverlayView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml
new file mode 100644
index 0000000..663fce3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/pin_sign_in_view.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.PinSignInView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?templateSignInPinBackground"
+ android:padding="?templateSignInPinPadding"
+ android:gravity="center">
+ <CarUiTextView
+ android:id="@+id/pin_text"
+ style="?templateSignInPinTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center" />
+</com.android.car.libraries.templates.host.view.widgets.common.PinSignInView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml
new file mode 100644
index 0000000..7b9ffb3
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/qr_code_sign_in_view.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ tools:ignore="Overdraw">
+ <ImageView
+ android:id="@+id/qr_code_view"
+ android:layout_width="?templateSignInQRCodeImageWidth"
+ android:layout_height="?templateSignInQRCodeImageWidth"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:tint="?android:attr/textColorPrimary"
+ tools:ignore="ContentDescription" />
+</com.android.car.libraries.templates.host.view.widgets.common.QRCodeSignInView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml
new file mode 100644
index 0000000..462c7fa
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/row_section_header_view.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CarUiTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/row_section_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="?templateRowSectionHeaderStyle"/>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml
new file mode 100644
index 0000000..0f343ae
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/layout/sign_in_button_view.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:clickable="true"
+ android:focusable="true"
+ android:layout_width="wrap_content"
+ android:layout_height="?templateActionButtonHeight"
+ style="?templateSignInProviderSignInButtonStyle">
+
+ <!-- A container for the different optional parts of an action. -->
+ <LinearLayout
+ android:id="@+id/action_container"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml
new file mode 100644
index 0000000..5c7d401
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/attrs.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <declare-styleable name="BleedingCardView">
+ <attr name="cardRadius" format="dimension"/>
+ <attr name="cardBackgroundColor" format="color"/>
+ <attr name="cardTextColor" format="color"/>
+
+ <!-- The colors used if there is not enough contrast ratio between
+ cardBackgroundColor and cardTextColor. -->
+ <attr name="cardFallbackDarkBackgroundColor" format="color" />
+ <attr name="cardFallbackLightBackgroundColor" format="color" />
+
+ <attr name="cardBorderWidth" format="dimension"/>
+ <attr name="cardBorderColor" format="color" />
+ <attr name="cardMinWidth" format="dimension" />
+ <attr name="cardMaxWidth" format="dimension" />
+
+ <!-- The OEM-defined card width.
+ The value set by the OEM cannot be larger than cardOemMaxWidth. -->
+ <attr name="cardOemWidth" format="dimension" />
+ <attr name="cardOemMaxWidth" format="dimension" />
+
+ <!-- The card width fraction in comparison to the parent view.
+ Zero means we use the layout_width, not the fraction. -->
+ <attr name="cardWidthFraction" format="float" />
+ </declare-styleable>
+
+ <declare-styleable name="ListView">
+ <!-- The fraction of the screen width that the list will take, up to the max
+ defined by `listMaxWidth`. A negative value indicates that the list
+ does ot use an adaptive width. -->
+ <attr name="listWidthFraction" format="float"/>
+
+ <!-- The maximum width a row list will use regardless of the
+ screen width. -->
+ <attr name="listMaxWidth" format="dimension" />
+
+ <!-- The width of the scrollbar next to the list. -->
+ <attr name="listScrollbarWidth" format="float"/>
+
+ <!-- The start padding of the list without a scrollbar. -->
+ <attr name="listNoScrollBarStartPadding" format="dimension"/>
+
+ <!-- Whether to show the scrollbar divider. -->
+ <attr name="listShowScrollbarDivider" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="RowListView">
+ <!-- Whether to show the compact row layout. -->
+ <attr name="listUseCompactRowLayout" format="boolean"/>
+ </declare-styleable>
+
+ <declare-styleable name="PlaceMarker">
+ <!-- Appearance of the map markers. -->
+ <attr name="markerAppearance" format="reference" />
+ </declare-styleable>
+
+ <declare-styleable name="MarkerAppearance">
+ <!-- Colors of the POI marker. -->
+ <attr name="markerDefaultBackgroundColor" format="color" />
+
+ <!-- Color that should be used for the content if it has a default bg -->
+ <attr name="markerDefaultContentColor" format="color" />
+
+ <!-- Color that should be used for the content if it has a custom bg -->
+ <attr name="markerCustomBackgroundContentColor" format="color" />
+
+ <!-- Color that should be used for the border if it is a default bg -->
+ <attr name="markerDefaultBorderColor" format="color" />
+
+ <!-- Color that should be used for the border if it is a custom bg -->
+ <attr name="markerCustomBorderColor" format="color" />
+
+ <attr name="markerPointerWidth" format="dimension" />
+ <attr name="markerPointerHeight" format="dimension" />
+ <attr name="markerStroke" format="dimension" />
+ <attr name="markerCornerRadius" format="dimension" />
+ <attr name="markerPadding" format="dimension" />
+
+ <!-- Colors of the anchor marker. -->
+ <attr name="anchorDefaultBackgroundColor" format="color" />
+ <attr name="anchorBorderColor" format="color" />
+ <attr name="anchorDotColor" format="color" />
+
+ <!-- The following android attributes are used for the marker label style. -->
+ <attr name="android:textSize" />
+ <attr name="android:fontFamily" />
+ <attr name="android:textStyle" />
+
+ <!-- The size of the label within the marker. -->
+ <attr name="markerTextHorizontalPadding" format="dimension" />
+ <attr name="markerIconSize" format="dimension" />
+ <attr name="markerImageSize" format="dimension" />
+ <attr name="markerImageCornerRadius" format="dimension" />
+
+ <!-- The size of the marker icon in the list. -->
+ <attr name="markerListIconSize" format="dimension" />
+ </declare-styleable>
+
+ <declare-styleable name="ActionStripView">
+ <!-- Appearance of the action strip fabs. -->
+ <attr name="fabAppearance" format="reference" />
+ </declare-styleable>
+
+ <!-- Styleable for different attributes to configure what an action strip FAB should look like. -->
+ <declare-styleable name="FabAppearance">
+ <!-- The color that should be used for contents (icon+label) inside the FAB. -->
+ <attr name="fabDefaultContentColor" format="color" />
+ </declare-styleable>
+
+ <!-- Styleable for configuring the action button -->
+ <declare-styleable name="ActionButtonView">
+ <!-- Specifies the maxEms value for the action button text. Needed to customize the maxEms value for some action buttons -->
+ <attr name="textMaxEms" format="integer"/>
+ </declare-styleable>
+
+ <!-- Styleable for configuring the car image view -->
+ <declare-styleable name="CarImageView">
+ <!-- The minimum image width. -->
+ <attr name="imageMinWidth" format="dimension" />
+
+ <!-- The maximum image width. -->
+ <attr name="imageMaxWidth" format="dimension" />
+
+ <!-- The minimum image height. -->
+ <attr name="imageMinHeight" format="dimension" />
+
+ <!-- The maximum image height. -->
+ <attr name="imageMaxHeight" format="dimension" />
+ </declare-styleable>
+
+ <!-- Styleable for configuring the car progress bar -->
+ <declare-styleable name="CarProgressBar">
+ <!-- The minimum image size. -->
+ <attr name="imageMinSize" format="dimension" />
+
+ <!-- The maximum image size. -->
+ <attr name="imageMaxSize" format="dimension" />
+ </declare-styleable>
+
+ <!-- Custom error state to be used in edit boxes or other components that support this state -->
+ <declare-styleable name="ErrorState">
+ <attr name="state_error" format="boolean"/>
+ </declare-styleable>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml
new file mode 100644
index 0000000..ac16615
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <integer name="action_strip_animation_duration_millis">250</integer>
+</resources>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java
new file mode 100644
index 0000000..7558316
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionButtonListViewHelper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonListView;
+import com.android.car.libraries.templates.host.view.widgets.common.ActionButtonView;
+
+/** Test helper for the action button list view. */
+public class ActionButtonListViewHelper {
+ private static final int LAYOUT_WIDTH = 400;
+ private static final int LAYOUT_HEIGHT = 600;
+
+ private final ViewGroup mActionButtonListView;
+
+ public ActionButtonListViewHelper(ViewGroup actionButtonListView) {
+ mActionButtonListView = actionButtonListView;
+ }
+
+ /** Force a measure and layout for the action strip. */
+ public void measureAndLayout() {
+ mActionButtonListView.measure(
+ MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY));
+ mActionButtonListView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT);
+ }
+
+ /** Returns a {@link ActionButtonView} at {@code index} in the {@code mActionButtonListView} */
+ public ActionButtonView getAction(int index) {
+ return (ActionButtonView) mActionButtonListView.getChildAt(index);
+ }
+
+ /** Returns an {@link ActionButtonListView} instance of the {@code mActionButtonListView} */
+ public ActionButtonListView getActionButtonListView() {
+ return (ActionButtonListView) mActionButtonListView;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java
new file mode 100644
index 0000000..ebee699
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/ActionStripHelper.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import static android.view.View.VISIBLE;
+
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import com.android.car.libraries.templates.host.view.widgets.common.FabView;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Test helper for the action strip. */
+public class ActionStripHelper {
+ private static final int LAYOUT_WIDTH = 400;
+ private static final int LAYOUT_HEIGHT = 600;
+
+ private final ViewGroup mActionStripView;
+
+ public ActionStripHelper(ViewGroup actionStripView) {
+ mActionStripView = actionStripView;
+ }
+
+ /** Force a measure and layout for the action strip. */
+ public void measureAndLayout() {
+ mActionStripView.measure(
+ MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY));
+ mActionStripView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT);
+ }
+
+ /** Returns a {@link List} of {@link FabView}s from the action strip. */
+ @Nullable
+ public List<FabView> getFabViews() {
+ List<FabView> views = new ArrayList<>();
+ for (int i = 0; i < mActionStripView.getChildCount(); i++) {
+ FabView fabView = (FabView) mActionStripView.getChildAt(i);
+ if (fabView.getVisibility() == VISIBLE) {
+ views.add((FabView) mActionStripView.getChildAt(i));
+ }
+ }
+ return views;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java
new file mode 100644
index 0000000..0aef733
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridContentViewHelper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.GridAdapter;
+import com.android.car.libraries.templates.host.view.widgets.common.GridItemView;
+import com.android.car.libraries.templates.host.view.widgets.common.GridRowWrapper;
+import com.android.car.libraries.templates.host.view.widgets.common.GridView;
+import com.android.car.libraries.templates.host.view.widgets.common.GridWrapper;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Test helper for {@link ContentView} that has the {@link GridWrapper} content set. */
+public class GridContentViewHelper {
+ private static final int LAYOUT_WIDTH = 400;
+ private static final int LAYOUT_HEIGHT = 600;
+
+ private final ContentView mContentView;
+
+ public GridContentViewHelper(ContentView contentView) {
+ mContentView = contentView;
+ }
+
+ /** Force a measure and layout for the content view. */
+ public void measureAndLayout() {
+ CarUiRecyclerView pagedListView = getRecyclerView();
+ if (pagedListView != null) {
+ pagedListView
+ .getView()
+ .measure(
+ MeasureSpec.makeMeasureSpec(LAYOUT_WIDTH, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(LAYOUT_HEIGHT, MeasureSpec.EXACTLY));
+ pagedListView.getView().layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT);
+ }
+ }
+
+ /** Returns the {@link RecyclerView} from the content view. */
+ @Nullable
+ public CarUiRecyclerView getRecyclerView() {
+ GridView gridView = getGridView();
+ if (gridView == null) {
+ return null;
+ }
+ return (CarUiRecyclerView) gridView.findViewById(R.id.grid_paged_list_view);
+ }
+
+ /** Returns a {@link List} of {@link GridRowWrapper}s from the content view. */
+ @Nullable
+ public List<GridRowWrapper> getGridRowWrappers() {
+ CarUiRecyclerView listView = getRecyclerView();
+ if (listView != null) {
+ GridAdapter adapter = (GridAdapter) listView.getAdapter();
+ if (adapter != null) {
+ return adapter.getRowWrappers();
+ }
+ }
+ return null;
+ }
+
+ /** Returns a specified {@link GridItemView} from the content view. */
+ @Nullable
+ public GridItemView getGridItemView(int index) {
+ CarUiRecyclerView listView = getRecyclerView();
+ if (listView == null) {
+ return null;
+ }
+
+ return (GridItemView) listView.getRecyclerViewChildAt(index);
+ }
+
+ /** Returns a specified {@link GridItemViewHelper} from the content view. */
+ @Nullable
+ public GridItemViewHelper getGridItemViewHelper(int index) {
+ GridItemView gridItemView = getGridItemView(index);
+ if (gridItemView == null) {
+ return null;
+ }
+ return new GridItemViewHelper(gridItemView);
+ }
+
+ @Nullable
+ private GridView getGridView() {
+ ViewGroup container = mContentView.findViewById(R.id.container);
+ if (container == null) {
+ return null;
+ }
+ return (GridView) container.getChildAt(0);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java
new file mode 100644
index 0000000..2558b66
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/GridItemViewHelper.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import static android.view.View.VISIBLE;
+
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.GridItemView;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** Test helper for {@link GridItemView}. */
+public class GridItemViewHelper {
+ private final GridItemView mGridItemView;
+
+ public GridItemViewHelper(GridItemView gridItemView) {
+ mGridItemView = gridItemView;
+ }
+
+ /** Returns the title as a {@link String} for the {@link GridItemView}. */
+ @Nullable
+ public String getTitle() {
+ return getTextById(R.id.grid_item_title);
+ }
+
+ /** Returns the title as the raw {@link CharSequence} for the {@link GridItemView}. */
+ @Nullable
+ public CharSequence getText() {
+ return getTextById(R.id.grid_item_text);
+ }
+
+ @Nullable
+ private String getTextById(int id) {
+ CarUiTextView carUiTextView = mGridItemView.findViewById(id);
+ if (carUiTextView.getVisibility() == VISIBLE) {
+ CharSequence title = carUiTextView.getText();
+ if (title != null) {
+ return title.toString();
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the {@link ImageView} for the {@link GridItemView}. */
+ @Nullable
+ public ImageView getImage() {
+ ImageView imageView = mGridItemView.findViewById(R.id.grid_item_image);
+ if (imageView.getVisibility() == VISIBLE) {
+ return imageView;
+ }
+ return null;
+ }
+
+ /** Returns the {@link ProgressBar} for the {@link GridItemView}. */
+ @Nullable
+ public ProgressBar getLoadingView() {
+ ProgressBar loadingView = mGridItemView.findViewById(R.id.grid_item_progress_bar);
+ if (loadingView.getVisibility() == VISIBLE) {
+ return loadingView;
+ }
+ return null;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java
new file mode 100644
index 0000000..877649f
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowListContentViewHelper.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.template.view.model.RowListWrapper;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.ContentView;
+import com.android.car.libraries.templates.host.view.widgets.common.RowAdapter;
+import com.android.car.libraries.templates.host.view.widgets.common.RowHolder;
+import com.android.car.libraries.templates.host.view.widgets.common.RowListView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import java.util.List;
+
+/** Test helper for {@link ContentView} that has the {@link RowListWrapper} content set. */
+public class RowListContentViewHelper {
+ private static final int LAYOUT_WIDTH = 400;
+ private static final int LAYOUT_HEIGHT = 600;
+
+ private final ContentView mContentView;
+
+ public RowListContentViewHelper(ContentView contentView) {
+ mContentView = contentView;
+ }
+
+ /** Force a measure and layout on the given {@link ContentView}. */
+ public void measureAndLayout() {
+ measureAndLayout(LAYOUT_WIDTH, LAYOUT_HEIGHT);
+ }
+
+ /** Force a measure and layout on the given {@link ContentView} with given width and height. */
+ public void measureAndLayout(int width, int height) {
+ RowListView pagedListView = getListView();
+ if (pagedListView != null) {
+ pagedListView.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ pagedListView.layout(0, 0, width, height);
+ }
+ }
+
+ /** Returns the {@link RowListView} from the content view. */
+ @Nullable
+ public RowListView getListView() {
+ RowListView listView = getRowListView();
+ if (listView == null) {
+ return null;
+ }
+
+ CarUiRecyclerView recyclerView = listView.findViewById(R.id.list_view);
+ return (RowListView) recyclerView.getParent();
+ }
+
+ /** Returns a {@link List} of the {@link RowHolder}s in the content view. */
+ @Nullable
+ public List<RowHolder> getRowHolders() {
+ RowListView listView = getListView();
+ if (listView != null) {
+ RowAdapter adapter = listView.getAdapter();
+ if (adapter != null) {
+ return adapter.getRowHolders();
+ }
+ }
+ return null;
+ }
+
+ /** Returns the row view for a given index from the content view. */
+ @Nullable
+ public View getRowView(int index) {
+ return getListItemView(index, View.class);
+ }
+
+ /** Returns the text of the given section header from the content view. */
+ @Nullable
+ public String getSectionHeaderText(int index) {
+ View sectionHeaderView = getSectionHeaderView(index);
+ if (sectionHeaderView == null) {
+ return null;
+ }
+ TextView view = sectionHeaderView.findViewById(R.id.row_section_header);
+ return view != null ? view.getText().toString() : null;
+ }
+
+ /** Returns the {@link TextView} of the given section header from the content view. */
+ @Nullable
+ public View getSectionHeaderView(int index) {
+ return getListItemView(index, View.class);
+ }
+
+ /** Returns a {@link RowViewHelper} for the given row index from the content view. */
+ @Nullable
+ public RowViewHelper getRowViewHelper(int index) {
+ View rowView = getRowView(index);
+ if (rowView == null) {
+ return null;
+ }
+ return new RowViewHelper(rowView);
+ }
+
+ @Nullable
+ private <T> T getListItemView(int index, Class<T> clazz) {
+ RowListView listView = getListView();
+ if (listView != null) {
+ return clazz.cast(listView.getRecyclerView().getRecyclerViewChildAt(index));
+ }
+ return null;
+ }
+
+ @Nullable
+ private RowListView getRowListView() {
+ ViewGroup container = mContentView.findViewById(R.id.container);
+ if (container == null) {
+ return null;
+ }
+ return (RowListView) container.getChildAt(0);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java
new file mode 100644
index 0000000..4fb4478
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/RowViewHelper.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import static android.view.View.VISIBLE;
+
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RadioButton;
+import android.widget.Switch;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Test helper for rows of {@code RowListView}. */
+public class RowViewHelper {
+ private final View mRowView;
+
+ public RowViewHelper(View rowView) {
+ mRowView = rowView;
+ }
+
+ /** Returns the title for the row as a {@link String}. */
+ @Nullable
+ public String getTitle() {
+ CharSequence title = getTitleCharSequence();
+ return title == null ? null : title.toString();
+ }
+
+ /** Get the title for the row as the direct {@link CharSequence}. */
+ @Nullable
+ public CharSequence getTitleCharSequence() {
+ return ((CarUiTextView) mRowView.findViewById(R.id.car_ui_list_item_title)).getText();
+ }
+
+ /** Get a specified span in the title for a row. */
+ @Nullable
+ public <T> T getTitleSpanAt(int spanIndex, Class<T> clazz) {
+ CharSequence charSequence = getTitleCharSequence();
+ if (charSequence == null) {
+ return null;
+ }
+ return getSpanAt(charSequence, spanIndex, clazz);
+ }
+
+ /** Returns {@code true} if the radio button for the row is selected. */
+ public boolean isRadioButtonSelected() {
+ RadioButton radioButton = mRowView.findViewById(R.id.car_ui_list_item_radio_button_widget);
+ return radioButton.isChecked();
+ }
+
+ /** Returns the lines for the body rows. */
+ @Nullable
+ public List<String> getTextLines() {
+ CarUiTextView bodyTextView = getBodyTextView();
+ if (bodyTextView == null) {
+ return null;
+ }
+
+ String[] lines = bodyTextView.getText().toString().split("\n");
+ return Arrays.asList(lines);
+ }
+
+ /** Returns the specified span within the row text. */
+ @Nullable
+ public <T> T getTextSpanAt(int textIndex, int spanIndex, Class<T> clazz) {
+ CharSequence text = getTextAt(textIndex);
+ if (text != null) {
+ return getSpanAt(text, spanIndex, clazz);
+ }
+ return null;
+ }
+
+ /** Returns a specific the text for a particular view index within the row. */
+ @Nullable
+ public CharSequence getTextAt(int index) {
+ return getBodyTextLine(index);
+ }
+
+ /** Returns the max number of lines for a given view within the row. */
+ public int getTextMaxLinesAt(int index) {
+ CarUiTextView carUiTextView = getBodyTextView();
+ return carUiTextView == null ? -1 : carUiTextView.getMaxLines();
+ }
+
+ /** Returns the image of the caret for the row. */
+ @Nullable
+ public ImageView getCaret() {
+ return getImageView(R.id.car_ui_list_item_supplemental_icon);
+ }
+
+ /** Returns the secondary text view for the row if visible. */
+ @Nullable
+ public CarUiTextView getBodyTextView() {
+ CarUiTextView bodyTextView = mRowView.findViewById(R.id.car_ui_list_item_body);
+ if (bodyTextView.getVisibility() == VISIBLE) {
+ return bodyTextView;
+ }
+ return null;
+ }
+
+ /** Returns the radio button for the row if visible. */
+ @Nullable
+ public RadioButton getRadioButton() {
+ RadioButton radioButton = mRowView.findViewById(R.id.car_ui_list_item_radio_button_widget);
+ if (radioButton.getVisibility() == VISIBLE) {
+ return radioButton;
+ }
+ return null;
+ }
+
+ /** Returns the image for the row if visible. */
+ @Nullable
+ public ImageView getImage() {
+ return getImageView(R.id.car_ui_list_item_icon);
+ }
+
+ /** Returns the {@link Switch} view for the row. */
+ @Nullable
+ public Switch getToggle() {
+ Switch toggle = mRowView.findViewById(R.id.car_ui_list_item_switch_widget);
+ if (toggle.getVisibility() == VISIBLE) {
+ return toggle;
+ }
+ return null;
+ }
+
+ /** Returns the view containing the row elements. */
+ public View getContainer() {
+ return mRowView;
+ }
+
+ /** Returns the view that acts as a touch interceptor. */
+ public View getTouchInterceptor() {
+ return mRowView.findViewById(R.id.car_ui_list_item_touch_interceptor);
+ }
+
+ @Nullable
+ private ImageView getImageView(int id) {
+ ImageView imageView = mRowView.findViewById(id);
+ if (imageView.getVisibility() == VISIBLE) {
+ return imageView;
+ }
+ return null;
+ }
+
+ @Nullable
+ private CharSequence getBodyTextLine(int index) {
+ CarUiTextView bodyTextView = getBodyTextView();
+ if (bodyTextView == null) {
+ return null;
+ }
+ CharSequence bodyText = bodyTextView.getText();
+ CharSequence[] lines = split(bodyText, "\n");
+ return lines[index];
+ }
+
+ @Nullable
+ private <T> T getSpanAt(CharSequence charSequence, int spanIndex, Class<T> clazz) {
+ SpannedString ss = (SpannedString) charSequence;
+ T[] spans = ss.getSpans(0, charSequence.length(), clazz);
+ if (spans == null || spanIndex > spans.length - 1) {
+ return null;
+ }
+ return spans[spanIndex];
+ }
+
+ private static CharSequence[] split(CharSequence charSequence, String regex) {
+ // A short-cut for non-spanned strings.
+ if (!(charSequence instanceof Spanned)) {
+ return charSequence.toString().split(regex);
+ }
+
+ // Hereafter, emulate String.split for CharSequence.
+ ArrayList<CharSequence> sequences = new ArrayList<>();
+ Matcher matcher = Pattern.compile(regex).matcher(charSequence);
+ int nextStart = 0;
+ boolean matched = false;
+ while (matcher.find()) {
+ sequences.add(charSequence.subSequence(nextStart, matcher.start()));
+ nextStart = matcher.end();
+ matched = true;
+ }
+ if (!matched) {
+ return new CharSequence[] {charSequence};
+ }
+ sequences.add(charSequence.subSequence(nextStart, charSequence.length()));
+ return sequences.toArray(new CharSequence[0]);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java
new file mode 100644
index 0000000..7efe1f4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/common/testing/TemplateViewHelper.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.common.testing;
+
+import android.annotation.SuppressLint;
+import com.android.car.libraries.apphost.view.AbstractTemplatePresenter;
+import com.android.car.libraries.templates.host.view.TemplateView;
+
+/** Test helper for {@link TemplateView}. */
+public final class TemplateViewHelper {
+
+ private static final int LAYOUT_WIDTH = 400;
+ private static final int LAYOUT_HEIGHT = 600;
+
+ /** Force a measure and layout on the given {@link TemplateView}. */
+ @SuppressLint("RestrictedApi")
+ public static void measureAndLayout(TemplateView templateView) {
+ templateView.measure(LAYOUT_WIDTH, LAYOUT_HEIGHT);
+ templateView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT);
+
+ // Robolectric creates views without giving it size, causing the view to fail to take input
+ // focus.
+ // Set the content view size and restore focus.
+ AbstractTemplatePresenter presenter =
+ (AbstractTemplatePresenter) templateView.getCurrentPresenter();
+ if (presenter != null) {
+ presenter.restoreFocus();
+ }
+ }
+
+ private TemplateViewHelper() {}
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java
new file mode 100644
index 0000000..e262778
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/CompactStepView.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.navigation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.navigation.model.Maneuver;
+import androidx.car.app.navigation.model.Step;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils;
+import com.android.car.ui.widget.CarUiTextView;
+
+/**
+ * A view that displays a compact view of a navigation maneuver.
+ *
+ * <p>For example it could show just the maneuver description or a description and an icon.
+ */
+public class CompactStepView extends LinearLayout {
+ private ImageView mTurnSymbolView;
+ private CarUiTextView mDescriptionText;
+ @Nullable private Step mStep;
+ private int mDescriptionTextDefaultTextColor;
+
+ public CompactStepView(Context context) {
+ this(context, null);
+ }
+
+ public CompactStepView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CompactStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CompactStepView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTurnSymbolView = findViewById(R.id.compact_turn_symbol);
+ mDescriptionText = findViewById(R.id.compact_description_text);
+
+ mDescriptionTextDefaultTextColor = mDescriptionText.getCurrentTextColor();
+ }
+
+ /** Sets the color of the texts in the view. */
+ public void setTextColor(@ColorInt int textColor) {
+ mDescriptionText.setTextColor(textColor);
+ }
+
+ /** Sets the colors of the texts in he view to their default colors. */
+ public void setDefaultTextColor() {
+ mDescriptionText.setTextColor(mDescriptionTextDefaultTextColor);
+ }
+
+ /**
+ * Sets the {@link Step} to be shown.
+ *
+ * <p>Setting a {@code null} steo will cause the view to be hidden.
+ */
+ public void setStep(
+ TemplateContext templateContext,
+ @Nullable Step step,
+ CarTextParams carTextParams,
+ @ColorInt int cardBackgroundColor) {
+ L.v(LogTags.TEMPLATE, "Setting compact step view with step: %s", step);
+
+ mStep = step;
+ if (step == null) {
+ setVisibility(GONE);
+ return;
+ }
+ Maneuver maneuver = step.getManeuver();
+ CarIcon turnIcon = maneuver == null ? null : maneuver.getIcon();
+ boolean shouldShowTurnIcon =
+ ImageUtils.setImageSrc(
+ templateContext,
+ turnIcon,
+ mTurnSymbolView,
+ ImageViewParams.builder()
+ .setBackgroundColor(cardBackgroundColor)
+ .setIgnoreAppTint(
+ !CarColorUtils.checkIconTintContrast(
+ templateContext, turnIcon, cardBackgroundColor))
+ .build());
+ mTurnSymbolView.setVisibility(shouldShowTurnIcon ? VISIBLE : GONE);
+
+ CarTextParams.Builder paramsBuilder =
+ CarTextParams.builder(carTextParams).setBackgroundColor(cardBackgroundColor);
+ CarText cue = step.getCue();
+ if (cue != null) {
+ paramsBuilder.setIgnoreAppIconTint(
+ !CarTextUtils.checkColorContrast(templateContext, cue, cardBackgroundColor));
+ }
+
+ mDescriptionText.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, cue, paramsBuilder.build(), mDescriptionText.getMaxLines()));
+ setVisibility(VISIBLE);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public Step getStep() {
+ return mStep;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java
new file mode 100644
index 0000000..29c9570
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/DetailedStepView.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.navigation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.Distance;
+import androidx.car.app.navigation.model.Maneuver;
+import androidx.car.app.navigation.model.Step;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.CarTextParams;
+import com.android.car.libraries.apphost.view.common.CarTextUtils;
+import com.android.car.libraries.apphost.view.common.DistanceUtils;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils;
+import com.android.car.ui.widget.CarUiTextView;
+
+/**
+ * A view that displays a detailed navigation step.
+ *
+ * <p>This view tries to display all the elements of a {@link Step} and {@link Distance}. For
+ * example if available, it would show a turn icon, description and lanes image. It could be used
+ * with another view to show the next turn.
+ */
+public class DetailedStepView extends LinearLayout {
+ private ImageView mTurnSymbolView;
+ private CarUiTextView mDistanceText;
+ private CarUiTextView mDescriptionText;
+ private ImageView mLanesImageView;
+ private LinearLayout mTurnContainerView;
+ private FrameLayout mLanesImageContainerView;
+ private final int mNavCardPaddingVertical;
+ private final int mNavCardSmallPaddingVertical;
+ private int mDistanceTextDefaultTextColor;
+ private int mDescriptionTextDefaultTextColor;
+
+ @Nullable private Step mStep;
+ @Nullable private Distance mDistance;
+
+ public DetailedStepView(Context context) {
+ this(context, null);
+ }
+
+ public DetailedStepView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DetailedStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ @StyleableRes
+ final int[] themeAttrs = {
+ R.attr.templateNavCardPaddingVertical,
+ R.attr.templateNavCardSmallPaddingVertical
+ };
+ TypedArray ta = context.obtainStyledAttributes(themeAttrs);
+ mNavCardPaddingVertical = ta.getDimensionPixelSize(0, 0);
+ mNavCardSmallPaddingVertical = ta.getDimensionPixelSize(1, 0);
+ ta.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTurnSymbolView = findViewById(R.id.turn_symbol);
+ mDistanceText = findViewById(R.id.distance_text);
+ mDescriptionText = findViewById(R.id.description_text);
+ mLanesImageView = findViewById(R.id.lanes_image);
+ mTurnContainerView = findViewById(R.id.turn_container);
+ mLanesImageContainerView = findViewById(R.id.lanes_image_container);
+
+ mDistanceTextDefaultTextColor = mDistanceText.getCurrentTextColor();
+ mDescriptionTextDefaultTextColor = mDescriptionText.getCurrentTextColor();
+ }
+
+ /** Sets the color of the texts in the view. */
+ public void setTextColor(@ColorInt int textColor) {
+ mDistanceText.setTextColor(textColor);
+ mDescriptionText.setTextColor(textColor);
+ }
+
+ /** Sets the colors of the texts in the view to their default colors. */
+ public void setDefaultTextColor() {
+ mDistanceText.setTextColor(mDistanceTextDefaultTextColor);
+ mDescriptionText.setTextColor(mDescriptionTextDefaultTextColor);
+ }
+
+ /**
+ * Sets the {@link Step} and {@link Distance} to be shown.
+ *
+ * <p>If the {@link Step} is {@code null} then the entire view is hidden. If the {@link Distance}
+ * is null then the just the distance text is hidden and the step is still shown.
+ */
+ public void setStepAndDistance(
+ TemplateContext templateContext,
+ @Nullable Step step,
+ @Nullable Distance distance,
+ CarTextParams cueTextParams,
+ @ColorInt int cardBackgroundColor,
+ boolean hideLaneImages) {
+ L.v(
+ LogTags.TEMPLATE,
+ "Setting detailed step view with step: %s, and distance: %s",
+ step,
+ distance);
+
+ mStep = step;
+ if (step == null) {
+ setVisibility(GONE);
+ return;
+ }
+ mDistance = distance;
+ Maneuver maneuver = step.getManeuver();
+ CarIcon turnIcon = maneuver == null ? null : maneuver.getIcon();
+ ImageViewParams turnIconParams =
+ ImageViewParams.builder()
+ .setBackgroundColor(cardBackgroundColor)
+ .setIgnoreAppTint(
+ !CarColorUtils.checkIconTintContrast(
+ templateContext, turnIcon, cardBackgroundColor))
+ .build();
+ boolean shouldShowTurnIcon =
+ ImageUtils.setImageSrc(templateContext, turnIcon, mTurnSymbolView, turnIconParams);
+ mTurnSymbolView.setVisibility(shouldShowTurnIcon ? VISIBLE : GONE);
+
+ if (distance != null) {
+ mDistanceText.setText(
+ CarUiTextUtils.fromCharSequence(
+ templateContext,
+ DistanceUtils.convertDistanceToDisplayString(templateContext, distance),
+ mDistanceText.getMaxLines()));
+ mDistanceText.setVisibility(VISIBLE);
+ } else {
+ mDistanceText.setVisibility(GONE);
+ }
+
+ CarText cue = step.getCue();
+ if (cue == null || CarText.isNullOrEmpty(cue)) {
+ mDescriptionText.setVisibility(GONE);
+ } else {
+ // Ignore app icon tint if it does not pass color contrast check
+ cueTextParams =
+ CarTextParams.builder(cueTextParams)
+ .setBackgroundColor(cardBackgroundColor)
+ .setIgnoreAppIconTint(
+ !CarTextUtils.checkColorContrast(templateContext, cue, cardBackgroundColor))
+ .build();
+ mDescriptionText.setText(
+ CarUiTextUtils.fromCarText(
+ templateContext, cue, cueTextParams, mDescriptionText.getMaxLines()));
+ mDescriptionText.setVisibility(VISIBLE);
+ }
+
+ CarIcon laneImage = step.getLanesImage();
+ ImageViewParams laneImageParams =
+ ImageViewParams.builder()
+ .setBackgroundColor(cardBackgroundColor)
+ .setIgnoreAppTint(
+ !CarColorUtils.checkIconTintContrast(
+ templateContext, laneImage, cardBackgroundColor))
+ .build();
+
+ boolean shouldShowLanesImage =
+ !hideLaneImages
+ && ImageUtils.setImageSrc(templateContext, laneImage, mLanesImageView, laneImageParams);
+
+ int turnContainerBottomMargin;
+ if (shouldShowLanesImage) {
+ mLanesImageContainerView.setVisibility(VISIBLE);
+
+ // If the lane image is present, apply the small internal padding between the turn
+ // container and the lane image.
+ turnContainerBottomMargin = mNavCardSmallPaddingVertical;
+ } else {
+ mLanesImageContainerView.setVisibility(GONE);
+ turnContainerBottomMargin = mNavCardPaddingVertical;
+ }
+ LinearLayout.LayoutParams layoutParams =
+ (LinearLayout.LayoutParams) mTurnContainerView.getLayoutParams();
+ layoutParams.bottomMargin = turnContainerBottomMargin;
+ mTurnContainerView.setLayoutParams(layoutParams);
+
+ setVisibility(VISIBLE);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public Step getStep() {
+ return mStep;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public Distance getDistance() {
+ return mDistance;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java
new file mode 100644
index 0000000..1e7bccd
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/MessageView.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.navigation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarText;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.ImageUtils;
+import com.android.car.libraries.apphost.view.common.ImageViewParams;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.view.widgets.common.CarUiTextUtils;
+import com.android.car.ui.widget.CarUiTextView;
+
+/** A view that displays a message with optional image and subtext. */
+public class MessageView extends LinearLayout {
+ private ImageView mImageView;
+ private CarUiTextView mTitleView;
+ private CarUiTextView mTextView;
+ private int mTitleDefaultTextColor;
+ private int mTextDefaultTextColor;
+
+ public MessageView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MessageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @SuppressWarnings({"argument.type.incompatible"})
+ public MessageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = findViewById(R.id.message_image);
+ mTitleView = findViewById(R.id.message_title);
+ mTextView = findViewById(R.id.message_text);
+
+ mTitleDefaultTextColor = mTitleView.getCurrentTextColor();
+ mTextDefaultTextColor = mTextView.getCurrentTextColor();
+ }
+
+ /** Sets the color of the texts in the view. */
+ public void setTextColor(@ColorInt int textColor) {
+ mTitleView.setTextColor(textColor);
+ mTextView.setTextColor(textColor);
+ }
+
+ /** Sets the colors of the texts in the view to their default colors. */
+ public void setDefaultTextColor() {
+ mTitleView.setTextColor(mTitleDefaultTextColor);
+ mTextView.setTextColor(mTextDefaultTextColor);
+ }
+
+ /** Sets the title, image and text content the view. */
+ public void setMessage(
+ TemplateContext templateContext,
+ @Nullable CarIcon image,
+ CarText title,
+ @Nullable CarText text,
+ @ColorInt int cardBackgroundColor) {
+ L.v(
+ LogTags.TEMPLATE,
+ "Setting message view with message: %s secondary: %s image: %s",
+ title,
+ text,
+ image);
+
+ boolean shouldShowImage =
+ ImageUtils.setImageSrc(
+ templateContext,
+ image,
+ mImageView,
+ ImageViewParams.builder()
+ .setBackgroundColor(cardBackgroundColor)
+ .setIgnoreAppTint(
+ !CarColorUtils.checkIconTintContrast(
+ templateContext, image, cardBackgroundColor))
+ .build());
+ mImageView.setVisibility(shouldShowImage ? VISIBLE : GONE);
+
+ mTitleView.setText(
+ CarUiTextUtils.fromCarText(templateContext, title, mTitleView.getMaxLines()));
+
+ mTextView.setText(CarUiTextUtils.fromCarText(templateContext, text, mTextView.getMaxLines()));
+ mTextView.setVisibility(!CarText.isNullOrEmpty(text) ? VISIBLE : GONE);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java
new file mode 100644
index 0000000..bb07bdd
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/ProgressView.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.navigation;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.templates.host.R;
+
+/** A view that displays a progress indicator. */
+public class ProgressView extends LinearLayout {
+ private ProgressBar mProgressBar;
+
+ public ProgressView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public ProgressView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @SuppressWarnings({"argument.type.incompatible"})
+ public ProgressView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mProgressBar = findViewById(R.id.progress_indicator);
+ }
+
+ /** Sets the color of the progress indicator. */
+ public void setColor(@ColorInt int color) {
+ mProgressBar.setIndeterminateTintList(ColorStateList.valueOf(color));
+ }
+
+ /** Sets the color of the progress indicator to its default color. */
+ public void setDefaultColor() {
+ mProgressBar.setIndeterminateTintList(null);
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java
new file mode 100644
index 0000000..dd9e99a
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/TravelEstimateView.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.templates.host.view.widgets.navigation;
+
+import static android.graphics.Color.TRANSPARENT;
+
+import android.content.Context;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.DateTimeWithZone;
+import androidx.car.app.model.Distance;
+import androidx.car.app.navigation.model.TravelEstimate;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.DateTimeUtils;
+import com.android.car.libraries.apphost.view.common.DistanceUtils;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.ui.widget.CarUiTextView;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.ArrayList;
+
+/**
+ * A view that displays a travel estimate for the navigation trip.
+ *
+ * <p>This view tries to display elements from the {@link TravelEstimate} data. For example if
+ * available, it would show the estimated time of arrival and distance to destination.
+ */
+public class TravelEstimateView extends LinearLayout {
+ private static final String INTERPUNCT = "\u00b7";
+ private static final String TIME_AND_DISTANCE_SEPARATOR = " " + INTERPUNCT + " ";
+
+ private CarUiTextView mArrivalTimeText;
+ private CarUiTextView mTimeAndDistanceText;
+ @Nullable private TravelEstimate mTravelEstimate;
+
+ public TravelEstimateView(Context context) {
+ this(context, null);
+ }
+
+ public TravelEstimateView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TravelEstimateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TravelEstimateView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mArrivalTimeText = findViewById(R.id.arrival_time_text);
+ mTimeAndDistanceText = findViewById(R.id.time_and_distance_text);
+ }
+
+ /** Sets the {@link TravelEstimate} or hides the view if set to {@code null} */
+ @SuppressWarnings("NewApi") // java.time APIs are OK through de-sugaring.
+ public void setTravelEstimate(
+ TemplateContext templateContext, @Nullable TravelEstimate travelEstimate) {
+ L.v(LogTags.TEMPLATE, "Setting travel estimate view: %s", travelEstimate);
+
+ mTravelEstimate = travelEstimate;
+ if (travelEstimate == null) {
+ setVisibility(GONE);
+ return;
+ }
+
+ // Display the arrival time.
+ DateTimeWithZone arrivalTime = travelEstimate.getArrivalTimeAtDestination();
+ if (arrivalTime != null) {
+ mArrivalTimeText.setText(
+ DateTimeUtils.formatArrivalTimeString(
+ templateContext, arrivalTime, ZoneId.systemDefault()));
+ } else {
+ // This shouldn't happen since the API should enforce a non-null arrival time.
+ mArrivalTimeText.setText(new ArrayList<>());
+ }
+
+ // Display the remaining trip time.
+ // The destination travel estimate's duration should not be unknown, but if it is, use an
+ // empty
+ // string.
+ long remainingTimeSeconds = travelEstimate.getRemainingTimeSeconds();
+ String timeString =
+ remainingTimeSeconds == TravelEstimate.REMAINING_TIME_UNKNOWN
+ ? ""
+ : DateTimeUtils.formatDurationString(
+ templateContext, Duration.ofSeconds(remainingTimeSeconds));
+ Distance distance = travelEstimate.getRemainingDistance();
+ String distanceString;
+ if (distance != null) {
+ distanceString = DistanceUtils.convertDistanceToDisplayString(templateContext, distance);
+ } else {
+ distanceString = "";
+ L.w(LogTags.TEMPLATE, "Remaining distance for the travel estimate is expected but not set");
+ }
+ String timeAndDistanceString = timeString + TIME_AND_DISTANCE_SEPARATOR + distanceString;
+
+ // If we have a valid custom text color, use it.
+ SpannableString timeAndDistanceSpannable = new SpannableString(timeAndDistanceString);
+
+ @ColorInt
+ int remainingTimeColor =
+ CarColorUtils.resolveColor(
+ templateContext,
+ travelEstimate.getRemainingTimeColor(),
+ /* isDark= */ false,
+ /* defaultColor= */ TRANSPARENT,
+ CarColorConstraints.STANDARD_ONLY);
+ setStringColorSpan(remainingTimeColor, timeAndDistanceSpannable, 0, timeString.length());
+
+ @ColorInt
+ int remainingDistanceColor =
+ CarColorUtils.resolveColor(
+ templateContext,
+ travelEstimate.getRemainingDistanceColor(),
+ /* isDark= */ false,
+ /* defaultColor= */ TRANSPARENT,
+ CarColorConstraints.STANDARD_ONLY);
+ setStringColorSpan(
+ remainingDistanceColor,
+ timeAndDistanceSpannable,
+ timeString.length() + TIME_AND_DISTANCE_SEPARATOR.length(),
+ timeAndDistanceString.length());
+
+ mTimeAndDistanceText.setText(timeAndDistanceSpannable);
+ }
+
+ /** Sets a color span in the given {@link SpannableString}. */
+ private static void setStringColorSpan(
+ @ColorInt int color, SpannableString spannable, int start, int end) {
+ if (color != TRANSPARENT) {
+ spannable.setSpan(
+ new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ public TravelEstimate getTravelEstimate() {
+ return mTravelEstimate;
+ }
+}
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml
new file mode 100644
index 0000000..27f04d4
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/compact_step_view.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="?templateNavCardSmallPaddingVertical"
+ android:paddingHorizontal="?templateNavCardPaddingHorizontal"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <!-- An image showing the turn icon on the left of the view. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/compact_turn_symbol"
+ android:layout_width="?templateNavCardSmallImageSize"
+ android:layout_height="?templateNavCardSmallImageSize"
+ app:imageMinWidth="?templateNavCardSmallImageSizeMin"
+ app:imageMaxWidth="?templateNavCardSmallImageSizeMax"
+ app:imageMinHeight="?templateNavCardSmallImageSizeMin"
+ app:imageMaxHeight="?templateNavCardSmallImageSizeMax"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:layout_gravity="center"
+ tools:ignore="ContentDescription" />
+
+ <!-- A text view displaying the description of the step, e.g. "Boggle St". -->
+ <CarUiTextView
+ android:id="@+id/compact_description_text"
+ style="?templateRoutingCompactDescriptionStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/template_steps_card_image_to_text_spacing_vertical"
+ android:layout_gravity="start|center" />
+</com.android.car.libraries.templates.host.view.widgets.navigation.CompactStepView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml
new file mode 100644
index 0000000..c35eab7
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/detailed_step_view.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <!-- A container for the turn icon, the distance, and the description.
+ We use this container so that we can apply the right margins to this
+ content. -->
+ <LinearLayout
+ android:id="@+id/turn_container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templateNavCardPaddingHorizontal"
+ android:layout_marginVertical="?templateNavCardPaddingVertical">
+
+ <!-- The top row showing the turn icon and the distance to it. -->
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+
+ <!-- An image showing the turn icon on the top left of the view. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/turn_symbol"
+ android:layout_width="?templateNavCardLargeImageSize"
+ android:layout_height="?templateNavCardLargeImageSize"
+ app:imageMinWidth="?templateNavCardLargeImageSizeMin"
+ app:imageMaxWidth="?templateNavCardLargeImageSizeMax"
+ app:imageMinHeight="?templateNavCardLargeImageSizeMin"
+ app:imageMaxHeight="?templateNavCardLargeImageSizeMax"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ tools:ignore="ContentDescription"
+ android:layout_marginEnd="?templateRoutingStepsCardIconToDistanceSpacingHorizontal" />
+
+ <!-- A text view next to the turn image on top showing the distance to the
+ next step. -->
+ <CarUiTextView
+ android:id="@+id/distance_text"
+ style="?templateRoutingDistanceStyle"
+ android:layout_gravity="center_vertical|start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+ <!-- A text view displaying the description of the step, e.g. "Turn right
+ at Morning Roll Ave S". -->
+ <CarUiTextView
+ android:id="@+id/description_text"
+ style="?templateRoutingDescriptionStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateNavCardSmallPaddingVertical"/>
+ </LinearLayout>
+
+ <!-- The image that displays the lanes (e.g. a series of arrows laid
+ horizontally. -->
+ <FrameLayout
+ android:id="@+id/lanes_image_container"
+ android:background="?templateRoutingLanesImageBackgroundColor"
+ android:layout_width="match_parent"
+ android:layout_height="?templateRoutingLanesImageContainerHeight"
+ android:paddingVertical="?templateRoutingLanesImageContainerVerticalPadding"
+ android:paddingHorizontal="?templateRoutingLanesImageContainerHorizontalPadding">
+ <ImageView
+ android:id="@+id/lanes_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ tools:ignore="ContentDescription" />
+ </FrameLayout>
+
+</com.android.car.libraries.templates.host.view.widgets.navigation.DetailedStepView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml
new file mode 100644
index 0000000..eaf1701
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/message_view.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.navigation.MessageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:paddingHorizontal="?templateNavCardPaddingHorizontal"
+ android:paddingVertical="?templateNavCardPaddingVertical">
+
+ <!-- An image showing the destination image. -->
+ <com.android.car.libraries.templates.host.view.widgets.common.CarImageView
+ android:id="@+id/message_image"
+ android:layout_width="?templateNavCardLargeImageSize"
+ android:layout_height="?templateNavCardLargeImageSize"
+ app:imageMinWidth="?templateNavCardLargeImageSizeMin"
+ app:imageMaxWidth="?templateNavCardLargeImageSizeMax"
+ app:imageMinHeight="?templateNavCardLargeImageSizeMin"
+ app:imageMaxHeight="?templateNavCardLargeImageSizeMax"
+ android:layout_gravity="start"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+ <!-- A title on top of the card, e.g. the name of the location the
+ user arrived at. -->
+ <CarUiTextView
+ android:id="@+id/message_title"
+ style="?templateRoutingMessagePrimaryStyle"
+ android:layout_gravity="start"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateRoutingMessageInnerPaddingVertical" />
+
+ <!-- A text view displaying the address of the location. -->
+ <CarUiTextView
+ android:id="@+id/message_text"
+ style="?templateRoutingMessageSecondaryStyle"
+ android:layout_gravity="start"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="?templateRoutingMessageInnerPaddingVertical" />
+</com.android.car.libraries.templates.host.view.widgets.navigation.MessageView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml
new file mode 100644
index 0000000..1aa46e2
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/progress_view.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="?templateNavCardPaddingHorizontal"
+ android:layout_marginVertical="?templateNavCardPaddingVertical"
+ android:layout_gravity="center"
+ android:orientation="vertical"
+ android:visibility="gone">
+ <com.android.car.libraries.templates.host.view.widgets.common.CarProgressBar
+ android:id="@+id/progress_indicator"
+ style="?templateLoadingSpinnerStyle"
+ android:layout_width="?templateNavCardLargeImageSize"
+ android:layout_height="?templateNavCardLargeImageSize"
+ app:imageMinSize="?templateNavCardLargeImageSizeMin"
+ app:imageMaxSize="?templateNavCardLargeImageSizeMax"
+ android:layout_gravity="center_horizontal" />
+
+</com.android.car.libraries.templates.host.view.widgets.navigation.ProgressView>
diff --git a/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml
new file mode 100644
index 0000000..88ae5de
--- /dev/null
+++ b/Host/app/renderer/src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res/layout/travel_estimate_view.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="start|center_vertical">
+
+ <!-- The first row showing the arrival time. -->
+ <CarUiTextView
+ android:id="@+id/arrival_time_text"
+ style="?templateRoutingTravelEstimateStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <!-- The first row showing the remaining time and distance. -->
+ <CarUiTextView
+ android:id="@+id/time_and_distance_text"
+ style="?templateRoutingTravelEstimateStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+</com.android.car.libraries.templates.host.view.widgets.navigation.TravelEstimateView>