summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCalvin Huang <calhuang@google.com>2022-03-29 13:26:27 -0700
committerCalvin Huang <calhuang@google.com>2022-04-04 11:25:35 -0700
commite6263ed070cd17e1988f41f2d712bce06ff0fbb1 (patch)
treef2dad3fcc8f3ef692285aa08480ba8328e97ccbb
parent295b2970abbb4ab4babf2cf7abbeace354532428 (diff)
downloadTemplates-e6263ed070cd17e1988f41f2d712bce06ff0fbb1.tar.gz
Publish AOSP Templates Host v1.0
Fix: 194232491 Test: ./gradlew :app:installDebug and showcase Change-Id: I9b06dc77ec1ff10ac507013463458299fcc0e698
-rw-r--r--.gitignore7
-rw-r--r--Host/app/apphost/build.gradle44
-rw-r--r--Host/app/apphost/consumer-rules.pro0
-rw-r--r--Host/app/apphost/proguard-rules.pro21
-rw-r--r--Host/app/apphost/src/main/AndroidManifest.xml33
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java103
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java401
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java46
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java30
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java47
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java221
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java51
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java22
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java230
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java20
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java35
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java30
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java27
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java181
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java42
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java379
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java203
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java34
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java165
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java54
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java71
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java25
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java157
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java139
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java215
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java55
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java25
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java75
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java293
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java56
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java125
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java34
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java27
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java555
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java46
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java72
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java44
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java53
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java35
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java332
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java70
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java31
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java23
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java23
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java23
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java513
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java130
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java57
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java88
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java45
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java34
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java50
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java68
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java65
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java65
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java61
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java37
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java161
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java78
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java86
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java50
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java176
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java170
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java31
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java33
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java25
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java52
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java130
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml5
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java385
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java143
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java625
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java28
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java113
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java113
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java98
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java56
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java59
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java35
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java62
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java532
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java51
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java37
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java165
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java76
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java248
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java47
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java33
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java635
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java75
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java71
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java41
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java95
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java72
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java644
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java183
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java271
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java104
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java166
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java382
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java466
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java219
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceProvider.java103
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceViewContainer.java146
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverter.java38
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverterRegistry.java68
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenter.java145
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterFactory.java36
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterRegistry.java75
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateTransitionManager.java27
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ActionButtonListParams.java144
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextParams.java237
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextUtils.java484
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DateTimeUtils.java128
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DistanceUtils.java99
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageUtils.java648
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageViewParams.java258
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/widget/map/AbstractMapViewContainer.java48
-rw-r--r--Host/app/build.gradle63
-rw-r--r--Host/app/proguard-rules.pro21
-rw-r--r--Host/app/renderer/build.gradle86
-rw-r--r--Host/app/renderer/consumer-rules.pro0
-rw-r--r--Host/app/renderer/libs/libnavigation_state_kt_proto_lite.ktproto.jarbin0 -> 77233 bytes
-rw-r--r--Host/app/renderer/libs/libnavigation_state_proto-lite.jarbin0 -> 105501 bytes
-rw-r--r--Host/app/renderer/proguard-rules.pro21
-rw-r--r--Host/app/renderer/src/main/AndroidManifest.xml44
-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
-rw-r--r--Host/app/renderer/src/main/res/layout/fragment_blank.xml14
-rw-r--r--Host/app/renderer/src/main/res/values/strings.xml4
-rw-r--r--Host/app/src/main/AndroidManifest.xml74
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/BlankFragment.kt59
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/TemplatesHostApplication.java23
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/HostResourceIdsImpl.java245
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/MapViewContainerModule.java35
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerModule.java34
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerStub.java44
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/ThemeManagerImpl.java43
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/config/AndroidManifest.xml21
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/config/FeaturesConfigStub.java41
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/config/HostApiLevelConfigStub.java33
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/config/StubModule.java46
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/config/UxreConfigStub.java46
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/di/res/values/strings.xml117
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_background.xml30
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_ripple.xml29
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_background.xml25
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_foreground.xml24
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_ic_pan_button.xml28
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_ic_refresh_button.xml25
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/drawable/ic_android.xml74
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/layout/fragment_blank.xml14
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values-night/colors.xml27
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values-night/colors_overlayable.xml24
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/attrs.xml32
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/bools_overlayable.xml38
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/colors.xml87
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/colors_overlayable.xml72
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/dimens_overlayable.xml116
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/drawable.xml23
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/drawable_overlayable.xml36
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/integers.xml29
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/integers_overlayable.xml42
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/overlayable.xml306
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/strings.xml21
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/res/values/styles_overlayable.xml87
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapStub.java230
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStub.java65
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStubContainer.java362
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_stub_layout.xml28
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub.xml22
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub_container_layout.xml29
-rw-r--r--Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/values/dimens.xml24
-rw-r--r--Host/build.gradle18
-rw-r--r--Host/gradle.properties21
-rw-r--r--Host/gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--Host/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xHost/gradlew185
-rw-r--r--Host/gradlew.bat89
-rw-r--r--Host/local.properties2
-rw-r--r--Host/settings.gradle24
-rw-r--r--OWNERS3
458 files changed, 46224 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4d7af7d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+Host/build/**
+Host/.gradle/**
+Host/.idea/**
+Host/app/build/**
+Host/app/apphost/build/**
+Host/app/renderer/build/**
+
diff --git a/Host/app/apphost/build.gradle b/Host/app/apphost/build.gradle
new file mode 100644
index 0000000..26bb402
--- /dev/null
+++ b/Host/app/apphost/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ minSdk 29
+ targetSdk 31
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation "androidx.car.app:app:1.2.0-beta02"
+ compileOnly 'com.google.auto.value:auto-value-annotations:1.9'
+ annotationProcessor 'com.google.auto.value:auto-value:1.9'
+ implementation group: 'com.google.errorprone', name: 'error_prone_annotations', version: '2.11.0'
+ implementation group: 'org.checkerframework', name: 'checker-qual', version: '3.21.1'
+ implementation('com.google.guava:guava:31.0.1-jre')
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-core:4.3.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/Host/app/apphost/consumer-rules.pro b/Host/app/apphost/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Host/app/apphost/consumer-rules.pro
diff --git a/Host/app/apphost/proguard-rules.pro b/Host/app/apphost/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Host/app/apphost/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/Host/app/apphost/src/main/AndroidManifest.xml b/Host/app/apphost/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..97e8f2b
--- /dev/null
+++ b/Host/app/apphost/src/main/AndroidManifest.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.
+-->
+<manifest package="com.android.car.libraries.apphost"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Declares permissions meant for the Car App Library (3p apps) -->
+ <!-- Permission that apps can use to get access to a canvas surface. -->
+ <permission
+ android:name="androidx.car.app.ACCESS_SURFACE"
+ android:protectionLevel="normal"/>
+ <!-- Permission that apps can use to get access to the navigation templates. -->
+ <permission
+ android:name="androidx.car.app.NAVIGATION_TEMPLATES"
+ android:protectionLevel="normal"/>
+ <!-- Permission that apps can use to get access to templates that show a map. -->
+ <permission
+ android:name="androidx.car.app.MAP_TEMPLATES"
+ android:protectionLevel="normal"/>
+</manifest>
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java
new file mode 100644
index 0000000..752227f
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java
@@ -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.apphost;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_UNBOUND;
+
+import android.content.Intent;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.google.common.base.Preconditions;
+import java.io.PrintWriter;
+
+/**
+ * Abstract base class for {@link Host}s which implements some of the common host service
+ * functionality.
+ */
+public abstract class AbstractHost implements Host {
+ protected TemplateContext mTemplateContext;
+ private boolean mIsValid = true;
+ private final String mName;
+
+ @SuppressWarnings("nullness")
+ protected AbstractHost(TemplateContext templateContext, String name) {
+ mTemplateContext = templateContext;
+ mName = name;
+ addEventSubscriptions();
+ }
+
+ @Override
+ public void setTemplateContext(TemplateContext templateContext) {
+ removeEventSubscriptions();
+ mTemplateContext = templateContext;
+ addEventSubscriptions();
+ }
+
+ @Override
+ public void invalidateHost() {
+ mIsValid = false;
+ }
+
+ @Override
+ public void onCarAppBound() {}
+
+ @Override
+ public void onNewIntentDispatched() {}
+
+ @Override
+ public void onBindToApp(Intent intent) {}
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {}
+
+ /** Called when the app is disconnected. */
+ public void onDisconnectedEvent() {}
+
+ /** Called when the app is unbound. */
+ public void onUnboundEvent() {}
+
+ /** Asserts that the service is valid. */
+ protected void assertIsValid() {
+ Preconditions.checkState(mIsValid, "Accessed a host service after it became invalidated");
+ }
+
+ /** Runs the {@code runnable} iff the host is valid. */
+ protected void runIfValid(String methodName, Runnable runnable) {
+ if (isValid()) {
+ runnable.run();
+ } else {
+ L.w(mName, "Accessed %s after host became invalidated", methodName);
+ }
+ }
+
+ /** Returns whether the host is valid. */
+ protected boolean isValid() {
+ return mIsValid;
+ }
+
+ private void addEventSubscriptions() {
+ mTemplateContext
+ .getEventManager()
+ .subscribeEvent(this, APP_DISCONNECTED, this::onDisconnectedEvent);
+ mTemplateContext.getEventManager().subscribeEvent(this, APP_UNBOUND, this::onUnboundEvent);
+ }
+
+ private void removeEventSubscriptions() {
+ mTemplateContext.getEventManager().unsubscribeEvent(this, APP_DISCONNECTED);
+ mTemplateContext.getEventManager().unsubscribeEvent(this, APP_UNBOUND);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java
new file mode 100644
index 0000000..aa557b0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarContext;
+import androidx.car.app.ICarApp;
+import androidx.car.app.ICarHost;
+import androidx.car.app.versioning.CarAppApiLevels;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.IntentUtils;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.StringUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+import com.android.car.libraries.apphost.internal.CarAppBindingCallback;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+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.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import com.google.common.base.Preconditions;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Host responsible for binding to and maintaining the lifecycle of a single app. */
+public class CarHost implements LifecycleOwner, StatusReporter {
+ // Suppress under-initialization checker warning for passing this to the LifecycleRegistry's
+ // ctor.
+ @SuppressWarnings("nullness")
+ private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+
+ private final ICarHost.Stub mCarHostStub = new CarHostStubImpl();
+ private final CarAppBinding mCarAppBinding;
+ private final TelemetryHandler mTelemetryHandler;
+
+ // Key is a @CarAppService.
+ private final HashMap<String, Host> mHostServices = new HashMap<>();
+
+ private TemplateContext mTemplateContext;
+ private long mLastStartTimeMillis = -1;
+ private boolean mIsValid = true;
+ private boolean mIsAppBound = false;
+
+ /** Creates a {@link CarHost}. */
+ public static CarHost create(TemplateContext templateContext) {
+ return new CarHost(templateContext);
+ }
+
+ /**
+ * Binds to the app managed by this {@link CarHost} instance.
+ *
+ * @param intent the intent used to start the app.
+ */
+ public void bindToApp(Intent intent) {
+ assertIsValid();
+
+ for (Host host : mHostServices.values()) {
+ host.onBindToApp(intent);
+ }
+
+ // Remove the custom extras we put in the intent, if any.
+ IntentUtils.removeInternalIntentExtras(
+ intent, mTemplateContext.getCarHostConfig().getHostIntentExtrasToRemove());
+
+ mCarAppBinding.bind(intent);
+ mTelemetryHandler.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(UiAction.APP_START, mCarAppBinding.getAppName()));
+ }
+
+ /** Unbinds from the app previously bound to with {@link #bindToApp}. */
+ public void unbindFromApp() {
+ mCarAppBinding.unbind();
+ }
+
+ /**
+ * Registers a {@link Host} with this host and returns the {@link CarHost}. This call is
+ * idempotent for the same {@code type}.
+ *
+ * @param type one of the CarServiceType as defined in {@link CarContext}
+ * @param factory factory for creating the {@link Host} corresponding to the service type
+ */
+ public Host registerHostService(String type, HostFactory factory) {
+ assertIsValid();
+ Host host = mHostServices.get(type);
+ if (host == null) {
+ host = factory.createHost(mCarAppBinding);
+ mHostServices.put(type, host);
+ }
+ return host;
+ }
+
+ /** Updates the {@link TemplateContext} when the template has destroyed an recreated. */
+ public void setTemplateContext(TemplateContext templateContext) {
+ // Since we are updating the TemplateContext, unsubscribe the event listener from the
+ // previous one.
+ mTemplateContext.getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+
+ mTemplateContext = templateContext;
+
+ mTemplateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+ mTemplateContext
+ .getEventManager()
+ .subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::onConfigurationChanged);
+ mCarAppBinding.setTemplateContext(templateContext);
+
+ for (Host host : mHostServices.values()) {
+ host.setTemplateContext(templateContext);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mCarAppBinding.toString();
+ }
+
+ /**
+ * Returns the {@link Host} that is registered for the given {@code type}.
+ *
+ * @param type one of the CarServiceType as defined in {@link CarContext}
+ * @throws IllegalStateException if there are no services registered for the given {@code type}
+ */
+ public Host getHostOrThrow(String type) {
+ assertIsValid();
+ Host host = mHostServices.get(type);
+ if (host == null) {
+ throw new IllegalStateException("No host service registered for: " + type);
+ }
+ return host;
+ }
+
+ /** Dispatches the given lifecycle event to the app managed by this {host}. */
+ public void dispatchAppLifecycleEvent(Event event) {
+ Log.d(LogTags.APP_HOST, "AppLifecycleEvent: " + event);
+ assertIsValid();
+ mLifecycleRegistry.handleLifecycleEvent(event);
+ }
+
+ /** Invalidates the {@link CarHost} so that any subsequent call on any of the APIs will fail. */
+ public void invalidate() {
+ mIsValid = false;
+ for (Host host : mHostServices.values()) {
+ host.invalidateHost();
+ }
+
+ mLifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+ }
+
+ /** Returns the {@link CarAppBinding} instance used to bind to the app. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public CarAppBinding getCarAppBinding() {
+ return mCarAppBinding;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public TemplateContext getTemplateContext() {
+ return mTemplateContext;
+ }
+
+ /** Runs the logic necessary after the {@link CarHost} has successfully bound to the app. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public void onAppBound() {
+ // Don't assert whether the object is valid here as this is an asynchronous API and could be
+ // called after being invalidated we don't want to cause a crash after the previous
+ // shutdown.
+ if (!mIsValid) {
+ return;
+ }
+
+ mIsAppBound = true;
+ mTemplateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+ for (Host host : mHostServices.values()) {
+ host.onCarAppBound();
+ }
+
+ // Binding is asynchronous, so when it completes, the lifecycle events may not have
+ // propagated. When lifecycle events happen before binding is complete, the lifecycle
+ // methods are dropped on the floor. Due to this, we will send lifecycle methods that
+ // may have happened since the bind began.
+ State currentState = mLifecycleRegistry.getCurrentState();
+ if (currentState.isAtLeast(State.STARTED)) {
+ mCarAppBinding.dispatchAppLifecycleEvent(Event.ON_START);
+ if (currentState.isAtLeast(State.RESUMED)) {
+ mCarAppBinding.dispatchAppLifecycleEvent(Event.ON_RESUME);
+ }
+ }
+ }
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {
+ pw.printf("- state: %s\n", mLifecycleRegistry.getCurrentState());
+ pw.printf("- is valid: %b\n", mIsValid);
+
+ if (mLastStartTimeMillis >= 0) {
+ long durationMillis =
+ mTemplateContext.getSystemClockWrapper().elapsedRealtime() - mLastStartTimeMillis;
+ pw.printf("- duration: %s\n", StringUtils.formatDuration(durationMillis));
+ }
+
+ mCarAppBinding.reportStatus(pw, piiHandling);
+ mTemplateContext.reportStatus(pw);
+
+ for (Map.Entry<String, Host> entry : mHostServices.entrySet()) {
+ pw.printf("\nHost service: %s\n", entry.getKey());
+ entry.getValue().reportStatus(pw, piiHandling);
+ }
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ // Don't assert whether the object is valid here, since callers may use the lifecycle to
+ // know.
+ return mLifecycleRegistry;
+ }
+
+ /**
+ * Returns the stub for the {@link ICarHost} binder that apps use to communicate with this host.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public ICarHost.Stub getCarHostStub() {
+ return mCarHostStub;
+ }
+
+ void onNewIntentDispatched() {
+ // Don't assert whether the object is valid here as this is an asynchronous API and could be
+ // called after being invalidated we don't want to cause a crash after the previous
+ // shutdown.
+ if (!mIsValid) {
+ return;
+ }
+
+ for (Host host : mHostServices.values()) {
+ host.onNewIntentDispatched();
+ }
+ }
+
+ private void onConfigurationChanged() {
+ if (mCarAppBinding.isBound()) {
+ mCarAppBinding.dispatch(
+ CarContext.CAR_SERVICE,
+ NamedAppServiceCall.create(
+ CarAppApi.ON_CONFIGURATION_CHANGED,
+ (ICarApp carApp, ANRToken anrToken) ->
+ carApp.onConfigurationChanged(
+ mTemplateContext.getResources().getConfiguration(),
+ new OnDoneCallbackStub(mTemplateContext, anrToken))));
+ }
+ }
+
+ @SuppressWarnings("nullness")
+ private CarHost(TemplateContext templateContext) {
+ mTemplateContext = templateContext;
+ mCarAppBinding =
+ CarAppBinding.create(
+ templateContext,
+ mCarHostStub,
+ new CarAppBindingCallback() {
+ @Override
+ public void onCarAppBound() {
+ onAppBound();
+ }
+
+ @Override
+ public void onNewIntentDispatched() {
+ CarHost.this.onNewIntentDispatched();
+ }
+
+ @Override
+ public void onCarAppUnbound() {
+ mIsAppBound = false;
+ templateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+ templateContext.getEventManager().dispatchEvent(EventType.APP_UNBOUND);
+ }
+ });
+
+ templateContext
+ .getEventManager()
+ .subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::onConfigurationChanged);
+
+ mTelemetryHandler = templateContext.getTelemetryHandler();
+
+ mLifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE);
+
+ mLifecycleRegistry.addObserver(
+ new DefaultLifecycleObserver() {
+ @Override
+ public void onStart(LifecycleOwner lifecycleOwner) {
+ mLastStartTimeMillis = templateContext.getSystemClockWrapper().elapsedRealtime();
+ dispatch(Event.ON_START);
+ }
+
+ @Override
+ public void onResume(LifecycleOwner lifecycleOwner) {
+ dispatch(Event.ON_RESUME);
+ }
+
+ @Override
+ public void onPause(LifecycleOwner lifecycleOwner) {
+ dispatch(Event.ON_PAUSE);
+ }
+
+ @Override
+ public void onStop(LifecycleOwner lifecycleOwner) {
+ dispatch(Event.ON_STOP);
+ long durationMillis =
+ templateContext.getSystemClockWrapper().elapsedRealtime() - mLastStartTimeMillis;
+ if (mLastStartTimeMillis < 0 || durationMillis < 0) {
+ L.w(
+ LogTags.APP_HOST,
+ "Negative duration %d or negative last start time %d",
+ durationMillis,
+ mLastStartTimeMillis);
+ return;
+ }
+ mTelemetryHandler.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(UiAction.APP_RUNTIME, mCarAppBinding.getAppName())
+ .setDurationMs(durationMillis));
+ mLastStartTimeMillis = -1;
+ }
+
+ private void dispatch(Event event) {
+ if (mCarAppBinding.isBound()) {
+ mCarAppBinding.dispatchAppLifecycleEvent(event);
+ }
+ }
+ });
+ }
+
+ private void assertIsValid() {
+ Preconditions.checkState(mIsValid, "Accessed the car host after it became invalidated");
+ }
+
+ private final class CarHostStubImpl extends ICarHost.Stub {
+ @Override
+ public void startCarApp(Intent intent) {
+ mTemplateContext.getCarAppManager().startCarApp(intent);
+ }
+
+ @Override
+ public void finish() {
+ mTemplateContext.getCarAppManager().finishCarApp();
+ }
+
+ @Override
+ public IBinder getHost(String type) {
+ assertIsValid();
+ Host service = mHostServices.get(type);
+ if (CarContext.NAVIGATION_SERVICE.equals(type)
+ && !mTemplateContext.getCarAppPackageInfo().isNavigationApp()) {
+ throw new IllegalArgumentException(
+ "Attempted to retrieve the navigation service, but the app is not a"
+ + " navigation app");
+ } else if (CarContext.CONSTRAINT_SERVICE.equals(type)
+ && mTemplateContext.getCarHostConfig().getNegotiatedApi() < CarAppApiLevels.LEVEL_2) {
+ throw new IllegalArgumentException(
+ "Attempted to retrieve the constraint service, but the host's API level is"
+ + " less than "
+ + CarAppApiLevels.LEVEL_2);
+ } else if (CarContext.HARDWARE_SERVICE.equals(type)
+ && mTemplateContext.getCarHostConfig().getNegotiatedApi() < CarAppApiLevels.LEVEL_3) {
+ throw new IllegalArgumentException(
+ "Attempted to retrieve the hardware service, but the host's API level is"
+ + " less than "
+ + CarAppApiLevels.LEVEL_3);
+ }
+
+ if (service != null) {
+ return service.getBinder();
+ }
+
+ throw new IllegalArgumentException("Unknown host service type:" + type);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java
new file mode 100644
index 0000000..b76f074
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost;
+
+import android.content.Intent;
+import android.os.IBinder;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.StatusReporter;
+
+/**
+ * A host that manages app specific behaviors such as managing template APIs, navigation APIs, etc.
+ *
+ * <p>This should be registered with {@link CarHost}.
+ */
+public interface Host extends StatusReporter {
+ /** Invalidates the {@link Host} so that any subsequent call on any of the APIs will fail. */
+ void invalidateHost();
+
+ /** Informs the {@link Host} that an {@link Intent} has been received to bind to the app. */
+ void onBindToApp(Intent intent);
+
+ /** Indicates that the {@link CarHost} is now bound to the app. */
+ void onCarAppBound();
+
+ /** Indicates that a {@code onNewIntent} call has been dispatched to the app. */
+ void onNewIntentDispatched();
+
+ /** Returns the binder interface that the app can use to talk to this host. */
+ IBinder getBinder();
+
+ /** Sets the updated {@link TemplateContext} in this host instance. */
+ void setTemplateContext(TemplateContext templateContext);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java
new file mode 100644
index 0000000..f0cb3bb
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.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.apphost;
+
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+
+/** A factory of {@link Host} instances. */
+public interface HostFactory {
+ /**
+ * Creates a {@link Host} instance.
+ *
+ * @param appBinding the binding to use to dispatch calls to the client. This is upper bounded to
+ * {@link Object} and down-casted later to avoid making {@link CarAppBinding} public, while
+ * allowing round-tripping it outside of the package
+ */
+ Host createHost(Object appBinding);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java
new file mode 100644
index 0000000..1219bad
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java
@@ -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.apphost;
+
+import android.content.ComponentName;
+import android.os.IInterface;
+import androidx.annotation.AnyThread;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+
+/**
+ * A one-way dispatcher of calls to the client app.
+ *
+ * @param <ServiceT> The type of service to dispatch calls for.
+ */
+public abstract class ManagerDispatcher<ServiceT extends IInterface> {
+ private final String mManagerType;
+ private final CarAppBinding mAppBinding;
+
+ public ComponentName getAppName() {
+ return mAppBinding.getAppName();
+ }
+
+ protected ManagerDispatcher(String managerType, Object appBinding) {
+ mManagerType = managerType;
+ mAppBinding = (CarAppBinding) appBinding;
+ }
+
+ /** Dispatches the {@code call} to the appropriate app service. */
+ @AnyThread
+ protected void dispatch(NamedAppServiceCall<ServiceT> call) {
+ mAppBinding.dispatch(mManagerType, call);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java
new file mode 100644
index 0000000..e4d0584
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java
@@ -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.apphost;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarContext;
+import androidx.car.app.model.CarLocation;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import java.util.List;
+
+/**
+ * A helper class to convert template navigation {@link Intent}s to/from legacy format.
+ *
+ * <p>Legacy apps are navigation apps that are non template (gmm, waze and kakao).
+ *
+ * <p>Legacy apps currently support a "https://maps.google.com/maps" uri, which we are not going to
+ * force all nav apps to support.
+ *
+ * <p>There are other navigation uris that some legacy apps support, such as "google.navigation:" or
+ * "google.maps:", but not all of them do.
+ *
+ * <p>The format for the uri for new navigation apps is described at {@link CarContext#startCarApp}.
+ */
+public final class NavigationIntentConverter {
+ public static final String GEO_QUERY_PREFIX = "geo";
+
+ private static final String LEGACY_NAVIGATION_INTENT_DATA_PREFIX =
+ "https://maps.google.com/maps?nav=1&q=";
+
+ private static final String NAV_PREFIX = "google.navigation";
+ private static final String MAPS_PREFIX = "google.maps";
+
+ private static final String HTTP_MAPS_URL_PREFIX = "http://maps.google.com";
+ private static final String HTTPS_MAPS_URL_PREFIX = "https://maps.google.com";
+ private static final String HTTPS_ASSISTANT_MAPS_URL_PREFIX = "https://assistant-maps.google.com";
+
+ private static final String TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX =
+ GEO_QUERY_PREFIX + ":";
+ private static final String TEMPLATE_NAVIGATION_INTENT_DATA_PREFIX =
+ TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX + "0,0?q=";
+
+ private static final String SEARCH_QUERY_PARAMETER = "q";
+ private static final String SEARCH_QUERY_PARAMETER_SPLITTER = SEARCH_QUERY_PARAMETER + "=";
+ private static final String ADDRESS_QUERY_PARAMETER = "daddr";
+ private static final String ADDRESS_QUERY_PARAMETER_SPLITTER = ADDRESS_QUERY_PARAMETER + "=";
+
+ /**
+ * Converts the given {@code navIntent} to one that is supported by legacy apps.
+ *
+ * <p>This method <strong>will update</strong> the {@link Intent} provided.
+ *
+ * @see CarContext#startCarApp for format documentation
+ */
+ public static void toLegacyNavIntent(Intent navIntent) {
+ L.d(LogTags.APP_HOST, "Converting to legacy nav intent %s", navIntent);
+
+ navIntent.setAction(Intent.ACTION_VIEW);
+
+ Uri navUri = Preconditions.checkNotNull(navIntent.getData());
+
+ // Cleanup by removing spaces.
+ CarLocation location = getCarLocation(navUri);
+
+ if (location != null) {
+ navIntent.setData(
+ Uri.parse(
+ LEGACY_NAVIGATION_INTENT_DATA_PREFIX
+ + location.getLatitude()
+ + ","
+ + location.getLongitude()));
+ } else {
+ String query = getQueryString(navUri);
+ if (query == null) {
+ throw new IllegalArgumentException("Navigation intent is not properly formed");
+ }
+ navIntent.setData(
+ Uri.parse(LEGACY_NAVIGATION_INTENT_DATA_PREFIX + query.replaceAll("\\s", "+")));
+ }
+ L.d(LogTags.APP_HOST, "Converted to legacy nav intent %s", navIntent);
+ }
+
+ /** Verifies if the given {@link Intent} is for navigation with a legacy navigation app. */
+ public static boolean isLegacyNavIntent(Intent intent) {
+ Uri uri = intent.getData();
+
+ if (uri == null) {
+ return false;
+ }
+
+ String scheme = uri.getScheme();
+ String dataString = intent.getDataString();
+ return GEO_QUERY_PREFIX.equals(scheme)
+ || NAV_PREFIX.equals(scheme)
+ || MAPS_PREFIX.equals(scheme)
+ || Strings.nullToEmpty(dataString).startsWith(HTTP_MAPS_URL_PREFIX) // NOLINT
+ || Strings.nullToEmpty(dataString).startsWith(HTTPS_MAPS_URL_PREFIX) // NOLINT
+ || Strings.nullToEmpty(dataString).startsWith(HTTPS_ASSISTANT_MAPS_URL_PREFIX); // NOLINT
+ }
+
+ /**
+ * Converts the given {@code legacyIntent} to one that is supported by template navigation apps.
+ *
+ * <p>This method <strong>will update</strong> the {@link Intent} provided.
+ *
+ * @see CarContext#startCarApp for the template navigation {@link Intent} format
+ */
+ public static void fromLegacyNavIntent(Intent legacyIntent) {
+ L.d(LogTags.APP_HOST, "Converting from legacy nav intent %s", legacyIntent);
+ Preconditions.checkArgument(isLegacyNavIntent(legacyIntent));
+
+ legacyIntent.setAction(CarContext.ACTION_NAVIGATE);
+
+ Uri uri = Preconditions.checkNotNull(legacyIntent.getData());
+
+ CarLocation location = getCarLocation(uri);
+
+ if (location != null) {
+ legacyIntent.setData(
+ Uri.parse(
+ TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX
+ + location.getLatitude()
+ + ","
+ + location.getLongitude()));
+ } else {
+ String query = getQueryString(uri);
+ if (query == null) {
+ throw new IllegalArgumentException("Navigation intent is not properly formed");
+ }
+ legacyIntent.setData(
+ Uri.parse(TEMPLATE_NAVIGATION_INTENT_DATA_PREFIX + query.replaceAll("\\s", "+")));
+ }
+ L.d(LogTags.APP_HOST, "Converted from legacy nav intent %s", legacyIntent);
+ }
+
+ /**
+ * Returns the latitude, longitude from the {@link Uri}, or {@code null} if none exists.
+ *
+ * <p>e.g. if Uri string is "geo:123.45,98.09", return value will be a {@link CarLocation} with
+ * 123.45 latitude and 98.09 longitude.
+ *
+ * <p>e.g. if Uri string is "https://maps.google.com/maps?q=123.45,98.09&nav=1", return value will
+ * be a {@link CarLocation} with 123.45 latitude and 98.09 longitude.
+ */
+ @Nullable
+ public static CarLocation getCarLocation(Uri uri) {
+ String possibleLatLng = getQueryString(uri);
+ if (possibleLatLng == null) {
+ // If not after a q=, uri is valid as geo:12.34,34.56
+ possibleLatLng = uri.getEncodedSchemeSpecificPart();
+ }
+
+ List<String> latLngParts = Splitter.on(',').splitToList(possibleLatLng);
+ if (latLngParts.size() == 2) {
+ try {
+ // Ensure both parts are doubles.
+ return CarLocation.create(
+ Double.parseDouble(latLngParts.get(0)), Double.parseDouble(latLngParts.get(1)));
+ } catch (NumberFormatException e) {
+ // Values are not Doubles.
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the actual query from the {@link Uri}, or {@code null} if none exists.
+ *
+ * <p>The query will be after "q=" or "daddr=".
+ *
+ * <p>e.g. if Uri string is "geo:0,0?q=124+Foo+St", return value will be "124+Foo+St".
+ *
+ * <p>e.g. if Uri string is "https://maps.google.com/maps?daddr=123+main+st&nav=1", return value
+ * will be "123+main+st".
+ */
+ @Nullable
+ public static String getQueryString(Uri uri) {
+ if (uri.isHierarchical()) {
+ List<String> query = uri.getQueryParameters(SEARCH_QUERY_PARAMETER);
+
+ if (query.isEmpty()) {
+ // No q= parameter, check if there is a daddr= parameter.
+ query = uri.getQueryParameters(ADDRESS_QUERY_PARAMETER);
+ }
+ return Iterables.getFirst(query, null);
+ }
+
+ String schemeSpecificPart = uri.getEncodedSchemeSpecificPart();
+ List<String> parts =
+ Splitter.on(SEARCH_QUERY_PARAMETER_SPLITTER).splitToList(schemeSpecificPart);
+
+ if (parts.size() < 2) {
+ // Did not find "q=".
+ parts = Splitter.on(ADDRESS_QUERY_PARAMETER_SPLITTER).splitToList(schemeSpecificPart);
+ }
+
+ // If we have a valid split on "q=" or "daddr=", split on "&" to only get the one parameter.
+ return parts.size() < 2 ? null : Splitter.on("&").splitToList(parts.get(1)).get(0);
+ }
+
+ private NavigationIntentConverter() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java
new file mode 100644
index 0000000..6ffca5b
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.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.apphost.common;
+
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Handles checking if an app does not respond in a timely manner. */
+public interface ANRHandler {
+ /** Time to wait for ANR check. */
+ int ANR_TIMEOUT_MS = 5000;
+
+ /**
+ * Performs the call and checks for application not responding.
+ *
+ * <p>The ANR check will happen in {@link #ANR_TIMEOUT_MS} milliseconds after calling {@link
+ * ANRCheckingCall#call}.
+ */
+ void callWithANRCheck(CarAppApi carAppApi, ANRCheckingCall call);
+
+ /** Token for dismissing the ANR check. */
+ interface ANRToken {
+ /** Requests dismissal of the ANR check. */
+ void dismiss();
+
+ /** Returns the {@link CarAppApi} that this token is for. */
+ CarAppApi getCarAppApi();
+ }
+
+ /** A call that checks for ANR and receives a token to use for dismissing the ANR check. */
+ interface ANRCheckingCall {
+ /**
+ * Performs the call.
+ *
+ * @param anrToken the token to use for dismissing the ANR check when the app calls back
+ */
+ void call(ANRToken anrToken);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java
new file mode 100644
index 0000000..4a44b25
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+/** Defines which type of incompatibility this exception is for. */
+public enum ApiIncompatibilityType {
+ APP_TOO_OLD,
+ HOST_TOO_OLD;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java
new file mode 100644
index 0000000..a3a4b8e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+/** Container class for letting the rest of the host knows whether the app is bound. */
+public final class AppBindingStateProvider {
+
+ private boolean mIsAppBound = false;
+
+ /** Returns whether the app is bound. */
+ public boolean isAppBound() {
+ return mIsAppBound;
+ }
+
+ /** Updates the app binding state to the input value. */
+ public void updateAppBindingState(boolean isAppBound) {
+ mIsAppBound = isAppBound;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java
new file mode 100644
index 0000000..25d9ee3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.graphics.Rect;
+import android.os.RemoteException;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.InputCallbackDelegate;
+import androidx.car.app.model.OnCheckedChangeDelegate;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnContentRefreshDelegate;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.SearchCallbackDelegate;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/**
+ * Class to set up safe remote callbacks to apps.
+ *
+ * <p>App interfaces to client are {@code oneway} so the calling thread does not block waiting for a
+ * response. (see go/aidl-best-practices for more information).
+ */
+public interface AppDispatcher {
+ /**
+ * Dispatches a {@link ISurfaceCallback#onSurfaceAvailable} to the provided listener with the
+ * provided container.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchSurfaceAvailable(
+ ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onSurfaceDestroyed} to the provided listener with the
+ * provided container.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchSurfaceDestroyed(
+ ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onVisibleAreaChanged} to the provided listener with the
+ * provided area.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchVisibleAreaChanged(ISurfaceCallback surfaceListener, Rect visibleArea);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onStableAreaChanged} to the provided listener with the
+ * provided area.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchStableAreaChanged(ISurfaceCallback surfaceListener, Rect stableArea);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onScroll} to the provided listener with the provided
+ * scroll distance.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchOnSurfaceScroll(ISurfaceCallback surfaceListener, float distanceX, float distanceY);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onFling} to the provided listener with the provided fling
+ * velocity.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchOnSurfaceFling(ISurfaceCallback surfaceListener, float velocityX, float velocityY);
+
+ /**
+ * Dispatches a {@link ISurfaceCallback#onScale} to the provided listener with the provided focal
+ * point and scale factor.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchOnSurfaceScale(
+ ISurfaceCallback surfaceListener, float focusX, float focusY, float scaleFactor);
+
+ /**
+ * Dispatches a {@link SearchCallbackDelegate#sendSearchTextChanged} to the provided listener with
+ * the provided search text.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchSearchTextChanged(SearchCallbackDelegate searchCallbackDelegate, String searchText);
+
+ /**
+ * Dispatches a {@link SearchCallbackDelegate#sendSearchSubmitted} to the provided listener with
+ * the provided search text.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchSearchSubmitted(SearchCallbackDelegate searchCallbackDelegate, String searchText);
+
+ /**
+ * Dispatches an {@link InputCallbackDelegate#sendInputTextChanged} to the provided listener with
+ * the provided input text.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchInputTextChanged(InputCallbackDelegate inputCallbackDelegate, String inputText);
+
+ /**
+ * Dispatches an {@link InputCallbackDelegate#sendInputSubmitted} to the provided listener with
+ * the provided input text.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchInputSubmitted(InputCallbackDelegate inputCallbackDelegate, String inputText);
+
+ /**
+ * Dispatches a {@link OnItemVisibilityChangedDelegate#sendItemVisibilityChanged} to the provided
+ * listener.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchItemVisibilityChanged(
+ OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate,
+ int startIndexInclusive,
+ int endIndexExclusive);
+
+ /**
+ * Dispatches a {@link OnSelectedDelegate#sendSelected} to the provided listener.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchSelected(OnSelectedDelegate onSelectedDelegate, int index);
+
+ /**
+ * Dispatches a {@link OnCheckedChangeDelegate#sendCheckedChange} to the provided listener.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchCheckedChanged(OnCheckedChangeDelegate onCheckedChangeDelegate, boolean isChecked);
+
+ /**
+ * Dispatches a {@link PanModeDelegate#sendPanModeChanged(boolean, OnDoneCallback)} to the
+ * provided listener.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchPanModeChanged(PanModeDelegate panModeDelegate, boolean isChecked);
+
+ /**
+ * Dispatches a {@link OnClickDelegate#sendClick} to the provided listener.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchClick(OnClickDelegate onClickDelegate);
+
+ /**
+ * Dispatches a {@link OnContentRefreshDelegate#sendContentRefreshRequested} event.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+ */
+ void dispatchContentRefreshRequest(OnContentRefreshDelegate onContentRefreshDelegate);
+
+ /**
+ * Performs the IPC.
+ *
+ * <p>The calls are oneway. Given this any exception thrown by the client will not reach us, they
+ * will be in their own process. (see go/aidl-best-practices for more information).
+ *
+ * <p>This method will handle app exceptions (described below) as well as {@link BundlerException}
+ * which would be thrown if the host fails to bundle an object before sending it over (should
+ * never happen).
+ *
+ * <h1>App Exceptions</h1>
+ *
+ * <p>Here are the possible exceptions thrown by the app, and when they may happen.
+ *
+ * <dl>
+ * <dt>{@link RemoteException}
+ * <dd>This exception is thrown when the binder is dead (i.e. the app crashed).
+ * <dt>{@link RuntimeException}
+ * <dd>The should not happen in regular scenario. The only cases where may happen are if the app
+ * is running in the same process as the host, or if the IPC was wrongly configured to not
+ * be {@code oneway}.
+ * </dl>
+ *
+ * <p>The following are the types of {@link RuntimeException} that the binder let's through. See
+ * https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2061-2094
+ *
+ * <ul>
+ * <li>{@link SecurityException}
+ * <li>{@link android.os.BadParcelableException}
+ * <li>{@link IllegalArgumentException}
+ * <li>{@link NullPointerException}
+ * <li>{@link IllegalStateException}
+ * <li>{@link android.os.NetworkOnMainThreadException}
+ * <li>{@link UnsupportedOperationException}
+ * <li>{@link android.os.ServiceSpecificException}
+ * <li>{@link RuntimeException} - for any other exceptions.
+ * </ul>
+ */
+ void dispatch(OneWayIPC ipc, CarAppApi carAppApi);
+
+ /**
+ * Performs the IPC allowing caller to define behavior for handling any exceptions.
+ *
+ * @see #dispatch(OneWayIPC, CarAppApi)
+ */
+ void dispatch(OneWayIPC ipc, ExceptionHandler exceptionHandler, CarAppApi carAppApi);
+
+ /** Will handle exceptions received while performing a {@link OneWayIPC}. */
+ interface ExceptionHandler {
+ void handle(CarAppError carAppError);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java
new file mode 100644
index 0000000..83d69a0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+/** Defines a service that can be retrieved from a {@link TemplateContext} */
+public interface AppHostService {}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java
new file mode 100644
index 0000000..94ec4c8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+/** Interface that allows loading application icons */
+public interface AppIconLoader {
+
+ /**
+ * Returns a rounded app icon for the given {@link ComponentName}, or a default icon if the given
+ * {@link ComponentName} doesn't match an installed application.
+ *
+ * <p>Implementations must ensure method is thread-safe.
+ */
+ @NonNull
+ Drawable getRoundAppIcon(@NonNull Context context, @NonNull ComponentName componentName);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java
new file mode 100644
index 0000000..4d95823
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.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.apphost.common;
+
+import android.os.RemoteException;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+
+/**
+ * Defines a call to make to an app service.
+ *
+ * @param <ServiceT> the service to receive the call
+ */
+public interface AppServiceCall<ServiceT> {
+ /** Dispatches the call. */
+ void dispatch(ServiceT appService, ANRToken anrToken) throws RemoteException;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java
new file mode 100644
index 0000000..3b6e46e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.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.apphost.common;
+
+/** Interface for handling back button press. */
+public interface BackPressedHandler {
+
+ /**
+ * Forwards a back pressed event to the car app's {@link
+ * androidx.car.app.IAppManager#onBackPressed}.
+ */
+ void onBackPressed();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java
new file mode 100644
index 0000000..c297120
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.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.apphost.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import androidx.annotation.ColorInt;
+
+/** A container class for a car app's primary and secondary colors. */
+public class CarAppColors {
+ @ColorInt public final int primaryColor;
+ @ColorInt public final int primaryDarkColor;
+ @ColorInt public final int secondaryColor;
+ @ColorInt public final int secondaryDarkColor;
+
+ /** Constructs an instance of {@link CarAppColors}. */
+ public CarAppColors(
+ int primaryColor, int primaryDarkColor, int secondaryColor, int secondaryDarkColor) {
+ this.primaryColor = primaryColor;
+ this.primaryDarkColor = primaryDarkColor;
+ this.secondaryColor = secondaryColor;
+ this.secondaryDarkColor = secondaryDarkColor;
+ }
+
+ /** Returns a default {@link CarAppColors} to use, based on the host's default colors. */
+ public static CarAppColors getDefault(Context context, HostResourceIds hostResourceIds) {
+ Resources resources = context.getResources();
+ return new CarAppColors(
+ resources.getColor(hostResourceIds.getDefaultPrimaryColor()),
+ resources.getColor(hostResourceIds.getDefaultPrimaryDarkColor()),
+ resources.getColor(hostResourceIds.getDefaultSecondaryColor()),
+ resources.getColor(hostResourceIds.getDefaultSecondaryDarkColor()));
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java
new file mode 100644
index 0000000..edb7355
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.ComponentName;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A class that encapsulates an error message that occurs for an app. */
+public class CarAppError {
+ /** Error type. Each type corresponds to an specific message to be displayed to the user */
+ public enum Type {
+ /** The client application is not responding in timely fashion */
+ ANR_TIMEOUT,
+
+ /** The user has requested to wait for the application to respond */
+ ANR_WAITING,
+
+ /** The client is using a version of the SDK that is not compatible with this host */
+ INCOMPATIBLE_CLIENT_VERSION,
+
+ /** The client does not have a required permission */
+ MISSING_PERMISSION,
+ }
+
+ private final ComponentName mAppName;
+ @Nullable private final Type mType;
+ @Nullable private final Throwable mCause;
+ @Nullable private final String mDebugMessage;
+ @Nullable private final Runnable mExtraAction;
+ private final boolean mLogVerbose;
+
+ /** Returns a {@link Builder} for the given {@code appName}. */
+ public static Builder builder(ComponentName appName) {
+ return new Builder(appName);
+ }
+
+ /** Returns the {@link ComponentName} representing an app. */
+ public ComponentName getAppName() {
+ return mAppName;
+ }
+
+ /**
+ * Returns the error type or {@code null} to show a generic error message.
+ *
+ * @see Builder#setType
+ */
+ @Nullable
+ public Type getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the debug message for displaying in the DHU or any head unit on debug builds.
+ *
+ * @see Builder#setDebugMessage
+ */
+ @Nullable
+ public String getDebugMessage() {
+ return mDebugMessage;
+ }
+
+ /**
+ * Returns the debug message for displaying in the DHU or any head unit on debug builds.
+ *
+ * @see Builder#setCause
+ */
+ @Nullable
+ public Throwable getCause() {
+ return mCause;
+ }
+
+ /**
+ * Returns the {@code action} for the error screen shown to the user, on top of the exit which is
+ * default.
+ *
+ * @see Builder#setExtraAction
+ */
+ @Nullable
+ public Runnable getExtraAction() {
+ return mExtraAction;
+ }
+
+ /**
+ * Returns whether to log this {@link CarAppError} as a verbose log.
+ *
+ * <p>The default is to log as error, but can be overridden via {@link Builder#setLogVerbose}
+ */
+ public boolean logVerbose() {
+ return mLogVerbose;
+ }
+
+ @Override
+ public String toString() {
+ return "[app: "
+ + mAppName
+ + ", type: "
+ + mType
+ + ", cause: "
+ + (mCause != null
+ ? mCause.getClass().getCanonicalName() + ": " + mCause.getMessage()
+ : null)
+ + ", debug msg: "
+ + mDebugMessage
+ + "]";
+ }
+
+ private CarAppError(Builder builder) {
+ mAppName = builder.mAppName;
+ mType = builder.mType;
+ mCause = builder.mCause;
+ mDebugMessage = builder.mDebugMessage;
+ mExtraAction = builder.mExtraAction;
+ mLogVerbose = builder.mLogVerbose;
+ }
+
+ /** A builder for {@link CarAppError}. */
+ public static class Builder {
+ private final ComponentName mAppName;
+ @Nullable private Type mType;
+ @Nullable private Throwable mCause;
+ @Nullable private String mDebugMessage;
+ @Nullable private Runnable mExtraAction;
+ public boolean mLogVerbose;
+
+ private Builder(ComponentName appName) {
+ mAppName = appName;
+ }
+
+ /** Sets the error type, or {@code null} to show a generic error message. */
+ public Builder setType(Type type) {
+ mType = type;
+ return this;
+ }
+
+ /** Sets the exception for displaying in the DHU or any head unit on debug builds. */
+ public Builder setCause(Throwable cause) {
+ mCause = cause;
+ return this;
+ }
+
+ /** Sets the debug message for displaying in the DHU or any head unit on debug builds. */
+ public Builder setDebugMessage(String debugMessage) {
+ mDebugMessage = debugMessage;
+ return this;
+ }
+
+ /**
+ * Adds the {@code action} to the error screen shown to the user, on top of the exit which is
+ * default.
+ */
+ public Builder setExtraAction(Runnable extraAction) {
+ mExtraAction = extraAction;
+ return this;
+ }
+
+ /** Sets whether to log the {@link CarAppError} as verbose only. */
+ public Builder setLogVerbose(boolean logVerbose) {
+ mLogVerbose = logVerbose;
+ return this;
+ }
+
+ /** Constructs the {@link CarAppError} instance. */
+ public CarAppError build() {
+ return new CarAppError(this);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java
new file mode 100644
index 0000000..f06342e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.Intent;
+
+/** Controls the ability to start a new car app, as well as finish the current car app. */
+public interface CarAppManager {
+ /**
+ * Starts a car app on the car screen.
+ *
+ * @see androidx.car.app.CarContext#startCarApp
+ */
+ void startCarApp(Intent intent);
+
+ /** Unbinds from the car app, and goes to the app launcher if the app is currently foreground. */
+ void finishCarApp();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java
new file mode 100644
index 0000000..24b0772
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java
@@ -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.apphost.common;
+
+import android.content.ComponentName;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+/** Provides package information of a car app. */
+public interface CarAppPackageInfo {
+ /** Package and service name of the 3p car app. */
+ @NonNull
+ ComponentName getComponentName();
+
+ /**
+ * Returns the primary and secondary colors of the app as defined in the metadata entry for the
+ * app service, or default app theme if the metadata entry is not specified.
+ */
+ @NonNull
+ CarAppColors getAppColors();
+
+ /** Returns whether this app info is for a navigation app. */
+ boolean isNavigationApp();
+
+ /** Returns a round app icon for the given car app. */
+ @NonNull
+ Drawable getRoundAppIcon();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java
new file mode 100644
index 0000000..bc272f8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import static androidx.car.app.model.CarColor.TYPE_BLUE;
+import static androidx.car.app.model.CarColor.TYPE_CUSTOM;
+import static androidx.car.app.model.CarColor.TYPE_DEFAULT;
+import static androidx.car.app.model.CarColor.TYPE_GREEN;
+import static androidx.car.app.model.CarColor.TYPE_PRIMARY;
+import static androidx.car.app.model.CarColor.TYPE_RED;
+import static androidx.car.app.model.CarColor.TYPE_SECONDARY;
+import static androidx.car.app.model.CarColor.TYPE_YELLOW;
+import static androidx.core.graphics.ColorUtils.calculateContrast;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_PRIMARY;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_PRIMARY_DARK;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_SECONDARY;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_SECONDARY_DARK;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.util.Pair;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+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;
+
+/** Utilities for handling {@link CarColor} instances. */
+public class CarColorUtils {
+
+ private static final double MINIMUM_COLOR_CONTRAST = 4.5;
+
+ /**
+ * Resolves a standard color to a {@link ColorInt}.
+ *
+ * @return the resolved color or {@code defaultColor} if the input {@code carColor} is {@code
+ * null}, does not meet the constraints, or of the type {@link CarColor#DEFAULT}
+ */
+ @ColorInt
+ public static int resolveColor(
+ TemplateContext templateContext,
+ @Nullable CarColor carColor,
+ boolean isDark,
+ @ColorInt int defaultColor,
+ CarColorConstraints constraints) {
+ return resolveColor(
+ templateContext, carColor, isDark, defaultColor, constraints, Color.TRANSPARENT);
+ }
+
+ /**
+ * Resolves a standard color to a {@link ColorInt}.
+ *
+ * <p>If {@code backgroundColor} is set to {@link Color#TRANSPARENT}, the {@code carColor} will
+ * not be checked for the minimum color contrast.
+ *
+ * @return the resolved color or {@code defaultColor} if the input {@code carColor} is {@code
+ * null}, does not meet the constraints or minimum color contrast, or of the type {@link
+ * CarColor#DEFAULT}
+ */
+ @ColorInt
+ public static int resolveColor(
+ TemplateContext templateContext,
+ @Nullable CarColor carColor,
+ boolean isDark,
+ @ColorInt int defaultColor,
+ CarColorConstraints constraints,
+ @ColorInt int backgroundColor) {
+ if (carColor == null) {
+ return defaultColor;
+ }
+ try {
+ constraints.validateOrThrow(carColor);
+ } catch (IllegalArgumentException e) {
+ L.e(LogTags.TEMPLATE, e, "Validation failed for color %s, will use default", carColor);
+ return defaultColor;
+ }
+
+ CarAppPackageInfo info = templateContext.getCarAppPackageInfo();
+ CarAppColors carAppColors = info.getAppColors();
+ HostResourceIds hostResourceIds = templateContext.getHostResourceIds();
+ return resolveColor(
+ templateContext,
+ isDark,
+ carColor,
+ carAppColors,
+ hostResourceIds,
+ defaultColor,
+ backgroundColor);
+ }
+
+ /** Resolves a standard color to a {@link ColorInt}. */
+ @ColorInt
+ public static int resolveColor(
+ Context context,
+ boolean isDark,
+ @Nullable CarColor carColor,
+ CarAppColors carAppColors,
+ HostResourceIds resIds,
+ @ColorInt int defaultColor,
+ @ColorInt int backgroundColor) {
+ if (carColor == null) {
+ return defaultColor;
+ }
+ int type = carColor.getType();
+ Resources resources = context.getResources();
+ switch (type) {
+ case TYPE_DEFAULT:
+ return defaultColor;
+ case TYPE_PRIMARY:
+ return getContrastCheckedColor(
+ carAppColors.primaryColor,
+ carAppColors.primaryDarkColor,
+ backgroundColor,
+ defaultColor,
+ isDark);
+ case TYPE_SECONDARY:
+ return getContrastCheckedColor(
+ carAppColors.secondaryColor,
+ carAppColors.secondaryDarkColor,
+ backgroundColor,
+ defaultColor,
+ isDark);
+ case TYPE_RED:
+ return resources.getColor(isDark ? resIds.getRedDarkColor() : resIds.getRedColor());
+ case TYPE_GREEN:
+ return resources.getColor(isDark ? resIds.getGreenDarkColor() : resIds.getGreenColor());
+ case TYPE_BLUE:
+ return resources.getColor(isDark ? resIds.getBlueDarkColor() : resIds.getBlueColor());
+ case TYPE_YELLOW:
+ return resources.getColor(isDark ? resIds.getYellowDarkColor() : resIds.getYellowColor());
+ case TYPE_CUSTOM:
+ return getContrastCheckedColor(
+ carColor.getColor(), carColor.getColorDark(), backgroundColor, defaultColor, isDark);
+ default:
+ L.e(LogTags.TEMPLATE, "Failed to resolve standard color id: %d", type);
+ return defaultColor;
+ }
+ }
+
+ /**
+ * Returns the {@link CarAppColors} from the given app name if all primary and secondary colors
+ * are present in the app's manifest, otherwise returns {@link CarAppColors#getDefault(Context,
+ * HostResourceIds)}.
+ */
+ public static CarAppColors resolveAppColor(
+ @NonNull Context context,
+ @NonNull ComponentName appName,
+ @NonNull HostResourceIds hostResourceIds) {
+ String packageName = appName.getPackageName();
+ CarAppColors defaultColors = CarAppColors.getDefault(context, hostResourceIds);
+
+ int themeId = ColorUtils.loadThemeId(context, appName);
+ if (themeId == 0) {
+ L.w(LogTags.TEMPLATE, "Cannot get the app theme from %s", packageName);
+ return defaultColors;
+ }
+
+ Context packageContext = ColorUtils.getPackageContext(context, packageName);
+ if (packageContext == null) {
+ L.w(LogTags.TEMPLATE, "Cannot get the app context from %s", packageName);
+ return defaultColors;
+ }
+ packageContext.setTheme(themeId);
+
+ Resources.Theme theme = packageContext.getTheme();
+ Pair<Integer, Integer> primaryColorVariants =
+ ColorUtils.getColorVariants(
+ theme,
+ packageName,
+ KEY_COLOR_PRIMARY,
+ KEY_COLOR_PRIMARY_DARK,
+ defaultColors.primaryColor,
+ defaultColors.primaryDarkColor);
+ Pair<Integer, Integer> secondaryColorVariants =
+ ColorUtils.getColorVariants(
+ theme,
+ packageName,
+ KEY_COLOR_SECONDARY,
+ KEY_COLOR_SECONDARY_DARK,
+ defaultColors.secondaryColor,
+ defaultColors.secondaryDarkColor);
+
+ return new CarAppColors(
+ primaryColorVariants.first,
+ primaryColorVariants.second,
+ secondaryColorVariants.first,
+ secondaryColorVariants.second);
+ }
+
+ /**
+ * Darkens the given color by a percentage of its brightness.
+ *
+ * @param originalColor the color to change the brightness of
+ * @param percentage the percentage to decrement the brightness for, in the [0..1] range. For
+ * example, a value of 0.5 will make the color 50% less bright
+ */
+ @ColorInt
+ public static int darkenColor(@ColorInt int originalColor, float percentage) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(originalColor, hsv);
+ hsv[2] *= 1.f - percentage;
+ return Color.HSVToColor(hsv);
+ }
+
+ /**
+ * Blends two colors using a SRC Porter-duff operator.
+ *
+ * <p>See <a href="http://ssp.impulsetrain.com/porterduff.html">Porter-Duff Compositing and Blend
+ * Modes</a>
+ *
+ * <p>NOTE: this function ignores the alpha channel of the destination, and returns a fully opaque
+ * color.
+ */
+ @ColorInt
+ public static int blendColorsSrc(@ColorInt int source, @ColorInt int destination) {
+ // Each color component is calculated like so:
+ // output_color = (1 - alpha(source)) * destination + alpha_source * source
+ float alpha = Color.alpha(source) / 255.f;
+ return Color.argb(
+ 255,
+ clampComponent(alpha * Color.red(source) + (1 - alpha) * Color.red(destination)),
+ clampComponent(alpha * Color.green(source) + (1 - alpha) * Color.green(destination)),
+ clampComponent(alpha * Color.blue(source) + (1 - alpha) * Color.blue(destination)));
+ }
+
+ /**
+ * Checks whether the given colors provide an acceptable contrast ratio.
+ *
+ * <p>See <a href="https://material.io/design/usability/accessibility.html#color-and-contrast">
+ * Color and Contrast</a>
+ *
+ * <p>If {@code backgroundColor} is {@link Color#TRANSPARENT}, any {@code foregroundColor} will
+ * pass the check.
+ *
+ * @param foregroundColor the foreground color for which the contrast should be checked.
+ * @param backgroundColor the background color for which the contrast should be checked.
+ * @return true if placing the foreground color over the background color results in an acceptable
+ * contrast.
+ */
+ public static boolean hasMinimumColorContrast(
+ @ColorInt int foregroundColor, @ColorInt int backgroundColor) {
+ if (backgroundColor == Color.TRANSPARENT) {
+ return true;
+ }
+
+ return calculateContrast(foregroundColor, backgroundColor) > MINIMUM_COLOR_CONTRAST;
+ }
+
+ /**
+ * Check if any variant in the given {@code foregroundCarColor} has enough color contrast against
+ * the given {@code backgroundColor}.
+ */
+ public static boolean checkColorContrast(
+ TemplateContext templateContext, CarColor foregroundCarColor, @ColorInt int backgroundColor) {
+ if (backgroundColor == Color.TRANSPARENT) {
+ return true;
+ }
+
+ if (CarColor.DEFAULT.equals(foregroundCarColor)) {
+ return true;
+ }
+
+ CarColor foregroundColor = convertToCustom(templateContext, foregroundCarColor);
+ boolean checkPasses =
+ hasMinimumColorContrast(foregroundColor.getColor(), backgroundColor)
+ || hasMinimumColorContrast(foregroundColor.getColorDark(), backgroundColor);
+ if (!checkPasses) {
+ L.w(
+ LogTags.TEMPLATE,
+ "Color contrast check failed, foreground car color: %s, background color: %d",
+ foregroundCarColor,
+ backgroundColor);
+ templateContext.getColorContrastCheckState().setCheckPassed(false);
+ }
+ return checkPasses;
+ }
+
+ /**
+ * Returns whether the icon's tint passes the color contrast check against the given background
+ * color.
+ */
+ public static boolean checkIconTintContrast(
+ TemplateContext templateContext, @Nullable CarIcon icon, @ColorInt int backgroundColor) {
+ boolean passes = true;
+ if (icon != null) {
+ CarColor iconTint = icon.getTint();
+ if (iconTint != null) {
+ passes = checkColorContrast(templateContext, iconTint, backgroundColor);
+ }
+ }
+ return passes;
+ }
+
+ /**
+ * Convert the given {@code carColor} into a {@link CarColor} of type {@link
+ * CarColor#TYPE_CUSTOM}.
+ */
+ private static CarColor convertToCustom(TemplateContext templateContext, CarColor carColor) {
+ if (carColor.getType() == TYPE_CUSTOM) {
+ return carColor;
+ }
+
+ @ColorInt
+ int color =
+ resolveColor(
+ templateContext,
+ carColor,
+ /* isDark= */ false,
+ Color.TRANSPARENT,
+ CarColorConstraints.UNCONSTRAINED);
+ @ColorInt
+ int colorDark =
+ resolveColor(
+ templateContext,
+ carColor,
+ /* isDark= */ true,
+ Color.TRANSPARENT,
+ CarColorConstraints.UNCONSTRAINED);
+ return CarColor.createCustom(color, colorDark);
+ }
+
+ /**
+ * Between the given {@code color} and {@code colorDark}, returns the color that has enough color
+ * contrast against the given {@code backgroundColor}.
+ *
+ * <p>If none of the given colors passes the check, returns {@code defaultColor}.
+ *
+ * <p>If {@code isDark} is {@code true}, {@code colorDark} will be checked first, otherwise {@code
+ * color} will be checked first. The first color passes the check will be returned.
+ */
+ @ColorInt
+ private static int getContrastCheckedColor(
+ @ColorInt int color,
+ @ColorInt int colorDark,
+ @ColorInt int backgroundColor,
+ @ColorInt int defaultColor,
+ boolean isDark) {
+ int[] colors = new int[2];
+ if (isDark) {
+ colors[0] = colorDark;
+ colors[1] = color;
+ } else {
+ colors[0] = color;
+ colors[1] = colorDark;
+ }
+
+ for (@ColorInt int col : colors) {
+ if (hasMinimumColorContrast(col, backgroundColor)) {
+ return col;
+ }
+ }
+ return defaultColor;
+ }
+
+ private static int clampComponent(float color) {
+ return (int) Math.max(0, Math.min(255, color));
+ }
+
+ private CarColorUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java
new file mode 100644
index 0000000..8bd4ac6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import static androidx.annotation.VisibleForTesting.PROTECTED;
+import static java.lang.Math.min;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.AppInfo;
+import androidx.car.app.versioning.CarAppApiLevels;
+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 java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/** Configuration options from the car host. */
+public abstract class CarHostConfig implements StatusReporter {
+
+ /** Represent the OEMs' preference on ordering the primary action */
+ @IntDef(
+ value = {
+ PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET,
+ PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT,
+ PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PrimaryActionOrdering {}
+
+ /** Indicates that OEMs choose to not re-ordering the actions */
+ public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET = 0;
+
+ /** Indicates that OEMs choose to put the primary action on the left */
+ public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT = 1;
+
+ /** Indicates that OEMs choose to put the primary action on the right */
+ public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT = 2;
+
+ private final ComponentName mAppName;
+ // Default to oldest as the min communication until updated via a call to negotiateApi.
+ // The oldest is the default lowest common denominator for communication.
+ private int mNegotiatedApi = CarAppApiLevels.getOldest();
+ // Last received app info, used for debugging purposes. This is the information the above
+ // negotiated API level is based on.
+ @Nullable private AppInfo mAppInfo = null;
+
+ public CarHostConfig(ComponentName appName) {
+ mAppName = appName;
+ }
+
+ /**
+ * Returns how many seconds after the user leaves an app, should the system wait before unbinding
+ * from it.
+ */
+ public abstract int getAppUnbindSeconds();
+
+ /** Returns a list of intent extras to be stripped before binding to the client app. */
+ public abstract List<String> getHostIntentExtrasToRemove();
+
+ /** Returns whether the provided intent should be treated as a new task flow. */
+ public abstract boolean isNewTaskFlowIntent(Intent intent);
+
+ /**
+ * Updates the API level for communication between the host and the connecting app.
+ *
+ * @return the negotiated api
+ * @throws IncompatibleApiException if the app's supported API range does not work with the host's
+ * API range
+ */
+ public int updateNegotiatedApi(AppInfo appInfo) throws IncompatibleApiException {
+ mAppInfo = appInfo;
+ int appMinApi = mAppInfo.getMinCarAppApiLevel();
+ int appMaxApi = mAppInfo.getLatestCarAppApiLevel();
+ int hostMinApi = getHostMinApi();
+ int hostMaxApi = getHostMaxApi();
+
+ L.i(
+ LogTags.APP_HOST,
+ "App: [%s] app info: [%s] Host min api: [%d] Host max api: [%d]",
+ mAppName.flattenToShortString(),
+ mAppInfo,
+ hostMinApi,
+ hostMaxApi);
+
+ if (appMinApi > hostMaxApi) {
+ throw new IncompatibleApiException(
+ ApiIncompatibilityType.HOST_TOO_OLD,
+ "App required min API level ["
+ + appMinApi
+ + "] is higher than the host's max API level ["
+ + hostMaxApi
+ + "]");
+ } else if (hostMinApi > appMaxApi) {
+ throw new IncompatibleApiException(
+ ApiIncompatibilityType.APP_TOO_OLD,
+ "Host required min API level ["
+ + hostMinApi
+ + "] is higher than the app's max API level ["
+ + appMaxApi
+ + "]");
+ }
+
+ mNegotiatedApi = min(appMaxApi, hostMaxApi);
+ L.d(
+ LogTags.APP_HOST,
+ "App: [%s], Host negotiated api: [%d]",
+ mAppName.flattenToShortString(),
+ mNegotiatedApi);
+
+ return mNegotiatedApi;
+ }
+
+ /** Returns the {@link AppInfo} that was last set, or {@code null} otherwise. */
+ @Nullable
+ public AppInfo getAppInfo() {
+ return mAppInfo;
+ }
+
+ /**
+ * Returns the API that was negotiated between the host and the connecting app. The host should
+ * use this value to determine if a feature for a particular API is supported for the app.
+ */
+ public int getNegotiatedApi() {
+ return mNegotiatedApi;
+ }
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {
+ pw.printf(
+ "- host min api: %d, host max api: %d, negotiated api: %s\n",
+ getHostMinApi(), getHostMaxApi(), mNegotiatedApi);
+ pw.printf(
+ "- app min api: %s, app target api: %s\n",
+ mAppInfo != null ? mAppInfo.getMinCarAppApiLevel() : "-",
+ mAppInfo != null ? mAppInfo.getLatestCarAppApiLevel() : "-");
+ pw.printf(
+ "- sdk version: %s\n", mAppInfo != null ? mAppInfo.getLibraryDisplayVersion() : "n/a");
+ }
+
+ /**
+ * Returns the host minimum API supported for the app.
+ *
+ * <p>Depending on the connecting app, the host may be configured to use a higher API level than
+ * the lowest level that the host is capable of supporting.
+ */
+ @VisibleForTesting(otherwise = PROTECTED)
+ public abstract int getHostMinApi();
+
+ /**
+ * Returns the host maximum API supported for the app.
+ *
+ * <p>Depending on the connecting app, the host may be configured to use a lower API level than
+ * the highest level that the host is capable of supporting.
+ */
+ @VisibleForTesting(otherwise = PROTECTED)
+ public abstract int getHostMaxApi();
+
+ /** Returns whether oem choose to ignore app provided colors on buttons on select templates. */
+ public abstract boolean isButtonColorOverriddenByOEM();
+
+ /**
+ * Returns the primary action order
+ *
+ * <p>Depending on the OEMs config, the primary action can be placed on the right or left,
+ * regardless of the config from connection app.
+ *
+ * @see PrimaryActionOrdering
+ */
+ @PrimaryActionOrdering
+ public abstract int getPrimaryActionOrder();
+
+ /** Returns true if the host supports cluster activity */
+ public abstract boolean isClusterEnabled();
+
+ /** Returns whether the host supports pan and zoom features in the navigation template */
+ public abstract boolean isNavPanZoomEnabled();
+
+ /** Returns whether the host supports pan and zoom features in POI and route preview templates */
+ public abstract boolean isPoiRoutePreviewPanZoomEnabled();
+
+ /** Returns whether the host supports pan and zoom features in POI and route preview templates */
+ public abstract boolean isPoiContentRefreshEnabled();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java
new file mode 100644
index 0000000..ff5b0e2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java
@@ -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.apphost.common;
+
+/**
+ * Manages the state of color contrast checks in template apps.
+ *
+ * <p>This class tracks the state for a single template in a single app.
+ */
+public interface ColorContrastCheckState {
+ /** Sets whether the color contrast check passed in the current template. */
+ void setCheckPassed(boolean passed);
+
+ /** Returns whether the color contrast check passed in the current template. */
+ boolean getCheckPassed();
+
+ /** Returns whether the host checks color contrast. */
+ // TODO(b/208683313): Remove once color contrast check is enabled in AAP
+ boolean checksContrast();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java
new file mode 100644
index 0000000..6356664
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.util.Pair;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/** Utility class to load a car app's primary and secondary colors. */
+public final class ColorUtils {
+ private static final String KEY_THEME = "androidx.car.app.theme";
+
+ // LINT.IfChange(car_colors)
+ public static final String KEY_COLOR_PRIMARY = "carColorPrimary";
+ public static final String KEY_COLOR_PRIMARY_DARK = "carColorPrimaryDark";
+ public static final String KEY_COLOR_SECONDARY = "carColorSecondary";
+ public static final String KEY_COLOR_SECONDARY_DARK = "carColorSecondaryDark";
+ // LINT.ThenChange()
+
+ private ColorUtils() {}
+
+ /** Returns a {@link Context} set up for the given package. */
+ @Nullable
+ public static Context getPackageContext(Context context, String packageName) {
+ Context packageContext;
+ try {
+ packageContext = context.createPackageContext(packageName, /* flags= */ 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ L.e(LogTags.APP_HOST, e, "Package %s does not exist", packageName);
+ return null;
+ }
+ return packageContext;
+ }
+
+ /**
+ * Returns the ID of the theme to use for the app described by the given component name.
+ *
+ * <p>This theme id is used to load custom primary and secondary colors from the remote app.
+ *
+ * @see com.google.android.libraries.car.app.model.CarColor
+ */
+ @StyleRes
+ public static int loadThemeId(Context context, ComponentName componentName) {
+ int theme = 0;
+ ServiceInfo serviceInfo = getServiceInfo(context, componentName);
+ if (serviceInfo != null && serviceInfo.metaData != null) {
+ theme = serviceInfo.metaData.getInt(KEY_THEME);
+ }
+
+ // If theme is not specified in service information, fallback to KEY_THEME in application
+ // info.
+ if (theme == 0) {
+ ApplicationInfo applicationInfo = getApplicationInfo(context, componentName);
+ if (applicationInfo != null) {
+ if (applicationInfo.metaData != null) {
+ theme = applicationInfo.metaData.getInt(KEY_THEME);
+ }
+ // If no override provided in service and application info, fallback to default app
+ // theme.
+ if (theme == 0) {
+ theme = applicationInfo.theme;
+ }
+ }
+ }
+
+ return theme;
+ }
+
+ /**
+ * Returns the color values for the given light and dark variants.
+ *
+ * <p>If a variant is not specified in the theme, default values are returned for both variants.
+ */
+ public static Pair<Integer, Integer> getColorVariants(
+ Resources.Theme appTheme,
+ String packageName,
+ String colorKey,
+ String darkColorKey,
+ @ColorInt int defaultColor,
+ @ColorInt int defaultDarkColor) {
+ Resources appResources = appTheme.getResources();
+ int colorId = appResources.getIdentifier(colorKey, "attr", packageName);
+ int darkColorId = appResources.getIdentifier(darkColorKey, "attr", packageName);
+
+ // If light or dark variant is not specified, return default variants.
+ if (colorId == Resources.ID_NULL || darkColorId == Resources.ID_NULL) {
+ return new Pair<>(defaultColor, defaultDarkColor);
+ }
+
+ @ColorInt int color = getColor(colorId, /* defaultColor= */ Color.TRANSPARENT, appTheme);
+ @ColorInt
+ int darkColor = getColor(darkColorId, /* defaultColor= */ Color.TRANSPARENT, appTheme);
+
+ // Even if the resource ID exists for a variant, it may not have a value. If so, use default
+ // variants.
+ if (color == Color.TRANSPARENT || darkColor == Color.TRANSPARENT) {
+ return new Pair<>(defaultColor, defaultDarkColor);
+ }
+ return new Pair<>(color, darkColor);
+ }
+
+ /** Returns the color specified by the given resource id from the given app theme. */
+ @ColorInt
+ private static int getColor(int resId, @ColorInt int defaultColor, Resources.Theme appTheme) {
+ @ColorInt int color = defaultColor;
+ if (resId != Resources.ID_NULL) {
+ int[] attr = {resId};
+ TypedArray ta = appTheme.obtainStyledAttributes(attr);
+ color = ta.getColor(0, defaultColor);
+ ta.recycle();
+ }
+ return color;
+ }
+
+ @Nullable
+ private static ServiceInfo getServiceInfo(Context context, ComponentName componentName) {
+ try {
+ return context
+ .getPackageManager()
+ .getServiceInfo(componentName, PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ L.e(LogTags.APP_HOST, e, "Component %s doesn't exist", componentName);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static ApplicationInfo getApplicationInfo(Context context, ComponentName componentName) {
+ try {
+ return context
+ .getPackageManager()
+ .getApplicationInfo(componentName.getPackageName(), PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ L.e(LogTags.APP_HOST, e, "Package %s doesn't exist", componentName.getPackageName());
+ }
+
+ return null;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java
new file mode 100644
index 0000000..8733cf7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.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.apphost.common;
+
+import android.content.res.Configuration;
+import android.widget.Toast;
+import androidx.car.app.model.OnClickDelegate;
+
+/** Holds static util methods for common usage in the host. */
+public final class CommonUtils {
+
+ /**
+ * Checks if {@code onClickDelegate} is a parked only action and the car is driving, then shows a
+ * toast and returns. Otherwise dispatches the {@code onClick} to the client.
+ */
+ public static void dispatchClick(
+ TemplateContext templateContext, OnClickDelegate onClickDelegate) {
+ if (onClickDelegate.isParkedOnly()
+ && templateContext.getConstraintsProvider().isConfigRestricted()) {
+ templateContext
+ .getToastController()
+ .showToast(
+ templateContext
+ .getResources()
+ .getString(templateContext.getHostResourceIds().getParkedOnlyActionText()),
+ Toast.LENGTH_SHORT);
+ return;
+ }
+ templateContext.getAppDispatcher().dispatchClick(onClickDelegate);
+ }
+
+ /** Returns {@code true} if the host is in dark mode, {@code false} otherwise. */
+ public static boolean isDarkMode(TemplateContext templateContext) {
+ Configuration configuration = templateContext.getResources().getConfiguration();
+ return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+ }
+
+ private CommonUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java
new file mode 100644
index 0000000..44b02d2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import androidx.annotation.MainThread;
+import androidx.car.app.model.TemplateWrapper;
+
+/**
+ * The interface for forwarding custom debug overlay information to the host fragment or activity.
+ */
+@MainThread
+public interface DebugOverlayHandler {
+ /**
+ * Returns {@code true} if the debug overlay is active.
+ *
+ * <p>The caller can use the active state to determine whether to process debug overlay
+ * information or not.
+ */
+ boolean isActive();
+
+ /**
+ * Sets debug overlay as active/inactive if parameter is {@code true}/{@code false} respectively.
+ */
+ void setActive(boolean active);
+
+ /** Clears all existing debug overlay. */
+ void clearAllEntries();
+
+ /**
+ * Removes the debug overlay entry associated with the input {@code debugKey}.
+ *
+ * <p>If the {@code debugKey} is not associated with any existing entry, this call is a no-op.
+ */
+ void removeDebugOverlayEntry(String debugKey);
+
+ /**
+ * Updates the debug overlay entry associated with a given {@code debugKey}.
+ *
+ * <p>This would override any previous debug text for the same key.
+ */
+ void updateDebugOverlayEntry(String debugKey, String debugOverlayText);
+
+ /** Returns text to render for debug overlay. */
+ CharSequence getDebugOverlayText();
+
+ /** Resets debug overlay with new information from {@link TemplateWrapper} */
+ void resetTemplateDebugOverlay(TemplateWrapper templateWrapper);
+
+ /** Set {@link Observer} for this {@link DebugOverlayHandler} */
+ void setObserver(Observer observer);
+
+ /**
+ * The interface that lets an object observe changes to the {@link DebugOverlayHandler}'s entries.
+ */
+ interface Observer {
+ void entriesUpdated();
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java
new file mode 100644
index 0000000..fc30645
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.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.apphost.common;
+
+/**
+ * Handles error cases, allowing classes that do not handle ui to be able to display an error screen
+ * to the user.
+ */
+public interface ErrorHandler {
+ /** Displays the given an error screen to the user. */
+ void showError(CarAppError error);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java
new file mode 100644
index 0000000..51aafee
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import static androidx.car.app.model.CarIcon.ERROR;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/**
+ * Formats {@link CarAppError} into {@link MessageTemplate} to allow displaying the error to the
+ * user.
+ */
+public class ErrorMessageTemplateBuilder {
+ private final Context mContext;
+ private final HostResourceIds mHostResourceIds;
+ private final CarAppError mError;
+ private final ComponentName mAppName;
+ private final OnClickListener mMainActionOnClickListener;
+
+ private String mAppLabel;
+
+ /** Constructor of an {@link ErrorMessageTemplateBuilder} */
+ @SuppressWarnings("nullness")
+ public ErrorMessageTemplateBuilder(
+ @NonNull Context context,
+ @NonNull CarAppError error,
+ @NonNull HostResourceIds instance,
+ @NonNull OnClickListener listener) {
+
+ if (context == null || error == null || instance == null || listener == null) {
+ throw new NullPointerException();
+ }
+
+ mContext = context;
+ mError = error;
+ mAppName = error.getAppName();
+ mHostResourceIds = instance;
+ mMainActionOnClickListener = listener;
+ }
+
+ /** Returns an {@link ErrorMessageTemplateBuilder} with {@link String} appLabel */
+ @NonNull
+ public ErrorMessageTemplateBuilder setAppLabel(String appLabel) {
+ mAppLabel = appLabel;
+ return this;
+ }
+
+ /** Returns a {@link MessageTemplate} with error message */
+ public MessageTemplate build() {
+ if (mAppLabel == null) {
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo applicationInfo = null;
+ try {
+ applicationInfo = pm.getApplicationInfo(mAppName.getPackageName(), 0);
+ } catch (NameNotFoundException e) {
+ L.e(LogTags.TEMPLATE, e, "Could not find the application info");
+ }
+ mAppLabel =
+ applicationInfo == null
+ ? mAppName.getPackageName()
+ : pm.getApplicationLabel(applicationInfo).toString();
+ }
+ String errorMessage = getErrorMessage(mAppLabel, mError);
+ if (errorMessage == null) {
+ errorMessage = mContext.getString(mHostResourceIds.getClientErrorText(), mAppLabel);
+ }
+
+ // TODO(b/179320446): Note that we use a whitespace as the title to not show anything in
+ // the header. We will have to update this to some internal-only template once the
+ // whitespace string no longer supperted.
+ MessageTemplate.Builder messageTemplateBuilder =
+ new MessageTemplate.Builder(errorMessage).setTitle(" ").setIcon(ERROR);
+
+ Throwable cause = mError.getCause();
+ if (cause != null) {
+ messageTemplateBuilder.setDebugMessage(cause);
+ }
+
+ String debugMessage = mError.getDebugMessage();
+ if (debugMessage != null) {
+ messageTemplateBuilder.setDebugMessage(debugMessage);
+ }
+
+ messageTemplateBuilder.addAction(
+ new Action.Builder()
+ .setTitle(mContext.getString(mHostResourceIds.getExitText()))
+ .setOnClickListener(mMainActionOnClickListener)
+ .build());
+
+ Action extraAction = getExtraAction(mError.getType(), mError.getExtraAction());
+ if (extraAction != null) {
+ messageTemplateBuilder.addAction(extraAction);
+ }
+
+ return messageTemplateBuilder.build();
+ }
+
+ @Nullable
+ private String getErrorMessage(String appLabel, @Nullable CarAppError error) {
+ CarAppError.Type type = error == null ? null : error.getType();
+ if (error == null || type == null) {
+ return null;
+ }
+ switch (type) {
+ case ANR_TIMEOUT:
+ return mContext.getString(mHostResourceIds.getAnrMessage(), appLabel);
+ case ANR_WAITING:
+ return mContext.getString(mHostResourceIds.getAnrWaiting());
+ case INCOMPATIBLE_CLIENT_VERSION:
+ ApiIncompatibilityType apiIncompatibilityType = ApiIncompatibilityType.HOST_TOO_OLD;
+ Throwable exception = error.getCause();
+ if (exception instanceof IncompatibleApiException) {
+ apiIncompatibilityType = ((IncompatibleApiException) exception).getIncompatibilityType();
+ }
+ return mContext.getString(
+ mHostResourceIds.getAppApiIncompatibleText(apiIncompatibilityType), appLabel);
+ case MISSING_PERMISSION:
+ return mContext.getString(mHostResourceIds.getMissingPermissionText(), appLabel);
+ }
+ throw new IllegalArgumentException("Unknown error type: " + type);
+ }
+
+ @Nullable
+ private Action getExtraAction(@Nullable CarAppError.Type type, @Nullable Runnable extraAction) {
+ if (type != CarAppError.Type.ANR_TIMEOUT || extraAction == null) {
+ return null;
+ }
+ return new Action.Builder()
+ .setTitle(mContext.getString(mHostResourceIds.getAnrWait()))
+ .setOnClickListener(extraAction::run)
+ .build();
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java
new file mode 100644
index 0000000..b9a78f5
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.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.apphost.common;
+
+import android.content.res.Resources;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.WeakHashMap;
+
+/** Handles event dispatch and subscription. */
+public class EventManager {
+ /** The type of events. */
+ public enum EventType {
+ /** Unknown event type */
+ UNKNOWN,
+
+ /** Signifies that the visible area of the view has changed. */
+ SURFACE_VISIBLE_AREA,
+
+ /** Signifies that the stable area of the view has changed. */
+ SURFACE_STABLE_AREA,
+
+ /**
+ * Signifies that one of the descendants of the template view hierarchy has been interacted
+ * with.
+ */
+ TEMPLATE_TOUCHED_OR_FOCUSED,
+
+ /** Signifies that the focus state of the window that contains the template view has changed. */
+ WINDOW_FOCUS_CHANGED,
+
+ /** Signifies that the Car UX Restrictions constraints on the template view have changed. */
+ CONSTRAINTS,
+
+ /**
+ * Signifies that the configuration of the view has changed.
+ *
+ * <p>The most up-to-date configuration can be retrieved via {@link
+ * Resources#getConfiguration()}
+ */
+ CONFIGURATION_CHANGED,
+
+ /** Signifies that the app is now unbound. */
+ APP_UNBOUND,
+
+ /** Signifies that the app has disconnected and will be rebound. */
+ APP_DISCONNECTED,
+
+ /**
+ * Signifies that the current list of places has changed.
+ *
+ * <p>This is used by the PlaceListMapTemplate to synchronize places between the list and the
+ * map views.
+ */
+ PLACE_LIST,
+
+ /** Signifies that WindowInsets has changed. */
+ WINDOW_INSETS,
+ }
+
+ // A weak-referenced map is used here so that subscribers do not have to explicitly unsubscribe
+ // themselves.
+ private final WeakHashMap<Object, List<Dependency>> mDependencyMap = new WeakHashMap<>();
+
+ /**
+ * Subscribes to an {@link EventType} and trigger the given {@link Runnable} when the event is
+ * fired.
+ *
+ * <p>The input weakReference instance should be used to associate and clean up the {@link
+ * Runnable} so that the event subscriber will automatically unsubscribe itself when the
+ * weak-referenced object is GC'd. However, if earlier un-subscription is preferred, {@link
+ * #unsubscribeEvent} can be called instead.
+ */
+ public void subscribeEvent(Object weakReference, EventType eventType, Runnable runnable) {
+ List<Dependency> objectDependencies = mDependencyMap.get(weakReference);
+ if (objectDependencies == null) {
+ objectDependencies = new ArrayList<>();
+ mDependencyMap.put(weakReference, objectDependencies);
+ }
+ objectDependencies.add(new Dependency(eventType, runnable));
+ }
+
+ /** Unsubscribes the given object (weakReference) to a certain {@link EventType}. */
+ public void unsubscribeEvent(Object weakReference, EventType eventType) {
+ List<Dependency> objectDependencies = mDependencyMap.get(weakReference);
+ if (objectDependencies != null) {
+ Iterator<Dependency> itr = objectDependencies.iterator();
+ while (itr.hasNext()) {
+ Dependency dependency = itr.next();
+ if (dependency.mEventType == eventType) {
+ itr.remove();
+ }
+ }
+ }
+ }
+
+ /** Dispatches the given {@link EventType} so subscriber can react to it. */
+ public void dispatchEvent(EventType eventType) {
+ // TODO(b/163634344): Avoid creating a temp collection. This is needed to prevent concurrent
+ // modifications that could happen if something subscribe to an event while
+ // listening/handling
+ // an existing event.
+ Collection<List<Dependency>> dependencySet = new ArrayList<>(mDependencyMap.values());
+ for (List<Dependency> dependencies : dependencySet) {
+ for (Dependency dependency : dependencies) {
+ if (dependency.mEventType == eventType) {
+ dependency.mRunnable.run();
+ }
+ }
+ }
+ }
+
+ /** An internal container for associating an {@link EventType} with a {@link Runnable}. */
+ private static class Dependency {
+ private final EventType mEventType;
+ private final Runnable mRunnable;
+
+ Dependency(EventType eventType, Runnable runnable) {
+ mEventType = eventType;
+ mRunnable = runnable;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java
new file mode 100644
index 0000000..4bc2210
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+/**
+ * Host-dependent resource identifiers.
+ *
+ * <p>Given that each host will have its own set of resources, this interface abstracts out the
+ * exact resource needed in each case.
+ */
+public interface HostResourceIds {
+
+ /** Returns the resource ID of drawable for the alert icon. */
+ @DrawableRes
+ int getAlertIconDrawable();
+
+ /** Returns the resource ID of the drawable for the error icon. */
+ @DrawableRes
+ int getErrorIconDrawable();
+
+ /** Returns the resource ID of the drawable for the error icon. */
+ @DrawableRes
+ int getBackIconDrawable();
+
+ /** Returns the resource ID of the drawable for the pan icon. */
+ @DrawableRes
+ int getPanIconDrawable();
+
+ /** Returns the resource ID of drawable for the refresh icon. */
+ @DrawableRes
+ int getRefreshIconDrawable();
+
+ /** Returns the resource ID of the standard red color. */
+ @ColorRes
+ int getRedColor();
+
+ /** Returns the resource ID of the standard red color's dark variant. */
+ @ColorRes
+ int getRedDarkColor();
+
+ /** Returns the resource ID of the standard green color. */
+ @ColorRes
+ int getGreenColor();
+
+ /** Returns the resource ID of the standard red color's dark variant. */
+ @ColorRes
+ int getGreenDarkColor();
+
+ /** Returns the resource ID of the standard blue color. */
+ @ColorRes
+ int getBlueColor();
+
+ /** Returns the resource ID of the standard red color's dark variant. */
+ @ColorRes
+ int getBlueDarkColor();
+
+ /** Returns the resource ID of the standard yellow color. */
+ @ColorRes
+ int getYellowColor();
+
+ /** Returns the resource ID of the standard red color's dark variant. */
+ @ColorRes
+ int getYellowDarkColor();
+
+ /**
+ * Returns the resource ID of the default color to use for the standard primary color, unless
+ * specified by the app.
+ */
+ @ColorRes
+ int getDefaultPrimaryColor();
+
+ /**
+ * Returns the resource ID of the default color to use for the standard primary color, unless
+ * specified by the app, in its dark variant.
+ */
+ @ColorRes
+ int getDefaultPrimaryDarkColor();
+
+ /**
+ * Returns the resource ID of the default color to use for the standard secondary color, unless
+ * specified by the app.
+ */
+ @ColorRes
+ int getDefaultSecondaryColor();
+
+ /**
+ * Returns the resource ID of the default color to use for the standard secondary color, unless
+ * specified by the app, in its dark variant.
+ */
+ @ColorRes
+ int getDefaultSecondaryDarkColor();
+
+ /** Returns the resource ID of the string used to format a distance in meters. */
+ @StringRes
+ int getDistanceInMetersStringFormat();
+
+ /** Returns the resource ID of the string used to format a distance in kilometers. */
+ @StringRes
+ int getDistanceInKilometersStringFormat();
+
+ /** Returns the resource ID of the string used to format a distance in feet. */
+ @StringRes
+ int getDistanceInFeetStringFormat();
+
+ /** Returns the resource ID of the string used to format a distance in miles. */
+ @StringRes
+ int getDistanceInMilesStringFormat();
+
+ /** Returns the resource ID of the string used to format a distance in yards. */
+ @StringRes
+ int getDistanceInYardsStringFormat();
+
+ /** Returns the resource ID of the string used to format a time with a time zone string. */
+ @StringRes
+ int getTimeAtDestinationWithTimeZoneStringFormat();
+
+ /** Returns the resource ID of the string used to format a duration in days. */
+ @StringRes
+ int getDurationInDaysStringFormat();
+
+ /** Returns the resource ID of the string used to format a duration in days and hours. */
+ @StringRes
+ int getDurationInDaysAndHoursStringFormat();
+
+ /** Returns the resource ID of the string used to format a duration in hours. */
+ @StringRes
+ int getDurationInHoursStringFormat();
+
+ /** Returns the resource ID of the string used to format a duration in hours and minutes. */
+ @StringRes
+ int getDurationInHoursAndMinutesStringFormat();
+
+ /** Returns the resource ID of the string used to format a duration in minutes. */
+ @StringRes
+ int getDurationInMinutesStringFormat();
+
+ /** Returns the resource ID of the error message for client app exception */
+ @StringRes
+ int getAnrMessage();
+
+ /** Returns the resource ID of the button text for waiting for ANR */
+ @StringRes
+ int getAnrWait();
+
+ /** Returns the resource ID of the error message for waiting for application to respond */
+ @StringRes
+ int getAnrWaiting();
+
+ /**
+ * Returns the resource ID of the error message for client version check failure of the given
+ * {@link ApiIncompatibilityType}
+ */
+ @StringRes
+ int getAppApiIncompatibleText(@NonNull ApiIncompatibilityType apiIncompatibilityType);
+
+ /** Returns the resource ID of the error message for client app exception */
+ @StringRes
+ int getClientErrorText();
+
+ /**
+ * Returns the resource ID of the error message for the application not having required permission
+ */
+ @StringRes
+ int getMissingPermissionText();
+
+ /** Returns the resource ID of the error message for client app exception */
+ @StringRes
+ int getExitText();
+
+ /**
+ * Returns the resource ID of the toast message for user selecting action that can only be
+ * selected when parked
+ */
+ @StringRes
+ int getParkedOnlyActionText();
+
+ /** Returns the resource ID of the search hint */
+ @StringRes
+ int getSearchHintText();
+
+ /** Returns the resource ID of the disabled search hint */
+ @StringRes
+ int getSearchHintDisabledText();
+
+ /** Returns the resource ID of the message for driving state */
+ @StringRes
+ int getDrivingStateMessageText();
+
+ /** Returns the resource ID of the message for no item for the current list */
+ @StringRes
+ int getTemplateListNoItemsText();
+
+ /** Returns the resource ID of the message for disabled action in long message template */
+ @StringRes
+ int getLongMessageTemplateDisabledActionText();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java
new file mode 100644
index 0000000..e0629f6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+/** An exception for API incompatibility between the host and the connecting app. */
+public final class IncompatibleApiException extends Exception {
+
+ private final ApiIncompatibilityType mApiIncompatibilityType;
+
+ public IncompatibleApiException(ApiIncompatibilityType apiIncompatibilityType, String message) {
+ super(message);
+ mApiIncompatibilityType = apiIncompatibilityType;
+ }
+
+ public ApiIncompatibilityType getIncompatibilityType() {
+ return mApiIncompatibilityType;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java
new file mode 100644
index 0000000..d45618e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import java.util.List;
+
+/** Holds static util methods for host Intent manipulations. */
+public final class IntentUtils {
+ private IntentUtils() {}
+
+ private static final String EXTRA_ORIGINAL_INTENT_KEY =
+ "com.android.car.libraries.apphost.common.ORIGINAL_INTENT";
+
+ /** Embeds {@code originalIntent} inside {@code wrappingIntent} for later extraction. */
+ public static void embedOriginalIntent(
+ @NonNull Intent wrappingIntent, @NonNull Intent originalIntent) {
+ wrappingIntent.putExtra(EXTRA_ORIGINAL_INTENT_KEY, originalIntent);
+ }
+
+ /**
+ * Tries to extract the embedded "original" intent. Gearhead doesn't set this, so it won't always
+ * be there.
+ */
+ @NonNull
+ public static Intent extractOriginalIntent(@NonNull Intent binderIntent) {
+ Intent originalIntent = binderIntent.getParcelableExtra(EXTRA_ORIGINAL_INTENT_KEY);
+ return originalIntent != null ? originalIntent : binderIntent;
+ }
+
+ /**
+ * Removes any extras that we pass around internally as metadata, preventing them from being
+ * exposed to the client apps.
+ */
+ public static void removeInternalIntentExtras(Intent intent, List<String> extrasToRemove) {
+ for (String extra : extrasToRemove) {
+ intent.removeExtra(extra);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java
new file mode 100644
index 0000000..90aeda8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.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.apphost.common;
+
+/** An exception denoting that the car host was accessed after it has become invalidated */
+public class InvalidatedCarHostException extends IllegalStateException {
+ /** Constructs a {@link InvalidatedCarHostException} instance. */
+ public InvalidatedCarHostException(String message) {
+ super(message);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java
new file mode 100644
index 0000000..931d5f1
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.location.Location;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.Place;
+import java.util.List;
+
+/**
+ * A mediator for communicating {@link Place}s, and related information, from and to different
+ * components in the UI hierarchy.
+ */
+public interface LocationMediator extends AppHostService {
+ /**
+ * Listener for notifying location changes by the app.
+ *
+ * <p>We do not go through the EventManager because we need to keep track of the listeners that
+ * are registered so we know when to start and stop requesting location updates from the app.
+ */
+ interface AppLocationListener {
+ void onAppLocationChanged(Location location);
+ }
+
+ /** Returns the current set of places of interest, or an empty list if there are none. */
+ List<Place> getCurrentPlaces();
+
+ /** Set a new list of places. */
+ void setCurrentPlaces(List<Place> places);
+
+ /** Returns the point when the camera was last anchored, or {@code null} if there was none. */
+ @Nullable
+ CarLocation getCameraAnchor();
+
+ /** Set the center point of where the camera is anchored, or {@code null} if it is unknown. */
+ void setCameraAnchor(@Nullable CarLocation cameraAnchor);
+
+ /**
+ * Add a listener for getting app location updates.
+ *
+ * <p>Note that using this on {@link androidx.car.app.versioning.CarAppApiLevel} 3 or lower would
+ * have no effect.
+ */
+ void addAppLocationListener(AppLocationListener listener);
+
+ /**
+ * Removes the listener which stops it from receiving app location updates.
+ *
+ * <p>Note that using this on {@link androidx.car.app.versioning.CarAppApiLevel} 3 or lower would
+ * have no effect.
+ */
+ void removeAppLocationListener(AppLocationListener listener);
+
+ /**
+ * Sets the {@link Location} as provided by the app.
+ *
+ * <p>This will notify the {@link AppLocationListener} that have been registered.
+ */
+ void setAppLocation(Location location);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java
new file mode 100644
index 0000000..fabef46
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.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.apphost.common;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/** Gesture manager that handles gestures in map-based template presenters. */
+public class MapGestureManager {
+ /** The minimum span value for the scale event. */
+ private static final int MIN_SCALE_SPAN_DP = 10;
+
+ private final ScaleGestureDetector mScaleGestureDetector;
+ private final GestureDetector mGestureDetector;
+ private final MapOnGestureListener mGestureListener;
+
+ public MapGestureManager(TemplateContext templateContext, long touchUpdateThresholdMillis) {
+ Handler touchHandler = new Handler(Looper.getMainLooper());
+ mGestureListener = new MapOnGestureListener(templateContext, touchUpdateThresholdMillis);
+ mScaleGestureDetector =
+ new ScaleGestureDetector(
+ templateContext, mGestureListener, touchHandler, MIN_SCALE_SPAN_DP);
+ mGestureDetector = new GestureDetector(templateContext, mGestureListener, touchHandler);
+ }
+
+ /** Handles the gesture from the given motion event. */
+ public void handleGesture(MotionEvent event) {
+ mScaleGestureDetector.onTouchEvent(event);
+ if (!mScaleGestureDetector.isInProgress()) {
+ mGestureDetector.onTouchEvent(event);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java
new file mode 100644
index 0000000..ef13bad
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import com.android.car.libraries.apphost.common.ScaleGestureDetector.OnScaleGestureListener;
+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 java.text.DecimalFormat;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Gesture listener in map-based template presenters.
+ *
+ * <p>The following events are rate-limited to reduce the delay between touch gestures and the app
+ * response:
+ *
+ * <ul>
+ * <li>{@link #onScroll(MotionEvent, MotionEvent, float, float)}
+ * <li>{@link #onScale(ScaleGestureDetector)}
+ * </ul>
+ */
+public class MapOnGestureListener extends SimpleOnGestureListener
+ implements OnScaleGestureListener {
+ /** Maximum number of debug overlay texts to display. */
+ private static final int MAX_DEBUG_OVERLAY_LINES = 3;
+
+ /** The scale factor to send to the app when the user double taps on the screen. */
+ private static final float DOUBLE_TAP_ZOOM_FACTOR = 2f;
+
+ private final DecimalFormat mDecimalFormat = new DecimalFormat("#.##");
+
+ private final Deque<String> mDebugOverlayTexts = new ArrayDeque<>();
+
+ private final TemplateContext mTemplateContext;
+
+ /** The time threshold between touch events. */
+ private final long mTouchUpdateThresholdMillis;
+
+ /** The last time that a scroll touch event happened. */
+ private long mScrollLastTouchTimeMillis;
+
+ /** The last time that a scale touch event happened. */
+ private long mScaleLastTouchTimeMillis;
+
+ /** The scroll distance in the X axis since the last distance update to the car app. */
+ private float mCumulativeDistanceX;
+
+ /** The scroll distance in the Y axis since the last distance update to the car app. */
+ private float mCumulativeDistanceY;
+
+ /**
+ * A flag that indicates that the scale gesture just ended.
+ *
+ * <p>This flag is used to work around the issue where a fling gesture is detected when a scale
+ * event ends.
+ */
+ private boolean mScaleJustEnded;
+
+ /** A flag that indicates that user is currently scrolling. */
+ private boolean mIsScrolling;
+
+ public MapOnGestureListener(TemplateContext templateContext, long touchUpdateThresholdMillis) {
+ this.mTemplateContext = templateContext;
+ this.mTouchUpdateThresholdMillis = touchUpdateThresholdMillis;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ L.d(LogTags.TEMPLATE, "Down touch event detected");
+ // Reset the flag that indicates that a sequence of scroll events may be starting from this
+ // point.
+ mIsScrolling = false;
+
+ mCumulativeDistanceX = 0;
+ mCumulativeDistanceY = 0;
+ return super.onDown(e);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ long touchTimeMillis = SystemClock.uptimeMillis();
+
+ // If this is the first scroll event in a series of gestures, log a telemetry event.
+ // This avoids triggering more than one event per sequence from finger touching down to
+ // finger lifted off the screen.
+ if (!mIsScrolling) {
+ // Since this is essentially the beginning of the scroll gesture, we need to check if
+ // SurfaceCallbackHandler allows the scroll to begin (e.g. checking against whether the
+ // user is already interacting with the screen too often).
+ SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+ if (!handler.canStartNewGesture()) {
+ mCumulativeDistanceX = 0;
+ mCumulativeDistanceY = 0;
+ return true;
+ }
+
+ mIsScrolling = true;
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.PAN));
+ }
+
+ mCumulativeDistanceX += distanceX;
+ mCumulativeDistanceY += distanceY;
+
+ if (touchTimeMillis - mScrollLastTouchTimeMillis > mTouchUpdateThresholdMillis) {
+ mTemplateContext
+ .getSurfaceCallbackHandler()
+ .onScroll(mCumulativeDistanceX, mCumulativeDistanceY);
+ mScrollLastTouchTimeMillis = touchTimeMillis;
+
+ // Reset the cumulative distance.
+ mCumulativeDistanceX = 0;
+ mCumulativeDistanceY = 0;
+
+ if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+ updateDebugOverlay(
+ "scroll distance [X: "
+ + mDecimalFormat.format(distanceX)
+ + ", Y: "
+ + mDecimalFormat.format(distanceY)
+ + "]");
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ // Do not send fling events when the scale event just ended. This works around the issue
+ // where a fling gesture is detected when a scale event ends.
+ if (!mScaleJustEnded) {
+ // Note that unlike the scroll, scale and double-tap events, onFling happens at the end of
+ // scroll events, so we do not check against SurfaceCallbackHandler#canStartNewGesture.
+ mTemplateContext.getSurfaceCallbackHandler().onFling(velocityX, velocityY);
+
+ if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+ updateDebugOverlay(
+ "fling velocity [X: "
+ + mDecimalFormat.format(velocityX)
+ + ", Y: "
+ + mDecimalFormat.format(velocityY)
+ + "]");
+ }
+
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.FLING));
+ } else {
+ mScaleJustEnded = false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+ if (!handler.canStartNewGesture()) {
+ return false;
+ }
+
+ float x = e.getX();
+ float y = e.getY();
+
+ // We cannot reliably map the touch pad position to the screen position.
+ // If the double tap happened in a touch pad, zoom into the center of the surface.
+ if (e.getSource() == InputDevice.SOURCE_TOUCHPAD) {
+ Rect visibleArea = mTemplateContext.getSurfaceInfoProvider().getVisibleArea();
+ if (visibleArea != null) {
+ x = visibleArea.centerX();
+ y = visibleArea.centerY();
+ } else {
+ // If we do not know the visible area, send negative focal point values to indicate
+ // that it is unavailable.
+ x = -1;
+ y = -1;
+ }
+ }
+
+ handler.onScale(x, y, DOUBLE_TAP_ZOOM_FACTOR);
+
+ if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+ updateDebugOverlay(
+ "scale focus [X: "
+ + mDecimalFormat.format(x)
+ + ", Y: "
+ + mDecimalFormat.format(y)
+ + "], factor ["
+ + DOUBLE_TAP_ZOOM_FACTOR
+ + "]");
+ }
+
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.ZOOM));
+
+ return true;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ long touchTimeMillis = SystemClock.uptimeMillis();
+ boolean shouldSendScaleEvent =
+ touchTimeMillis - mScaleLastTouchTimeMillis > mTouchUpdateThresholdMillis;
+ if (shouldSendScaleEvent) {
+ handleScale(detector);
+ mScaleLastTouchTimeMillis = touchTimeMillis;
+ }
+
+ // If we return false here, the detector will continue accumulating the scale factor until
+ // the next time we return true.
+ return shouldSendScaleEvent;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ // We need to check if SurfaceCallbackHandler allows the scaling gesture to begin (e.g. checking
+ // against whether the user is already interacting with the screen too often). Returning false
+ // here if needed to tell the detector to ignore the rest of the gesture.
+ SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+ return handler.canStartNewGesture();
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ handleScale(detector);
+ mScaleJustEnded = true;
+
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.ZOOM));
+ }
+
+ private void handleScale(ScaleGestureDetector detector) {
+ // The focus values are only meaningful when the motion is in progress
+ if (detector.isInProgress()) {
+ float focusX = detector.getFocusX();
+ float focusY = detector.getFocusY();
+ float scaleFactor = detector.getScaleFactor();
+ mTemplateContext.getSurfaceCallbackHandler().onScale(focusX, focusY, scaleFactor);
+
+ if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+ updateDebugOverlay(
+ "scale focus [X: "
+ + mDecimalFormat.format(focusX)
+ + ", Y: "
+ + mDecimalFormat.format(focusY)
+ + "], factor ["
+ + mDecimalFormat.format(scaleFactor)
+ + "]");
+ }
+ }
+ }
+
+ private void updateDebugOverlay(String debugText) {
+ if (mDebugOverlayTexts.size() >= MAX_DEBUG_OVERLAY_LINES) {
+ mDebugOverlayTexts.removeFirst();
+ }
+ mDebugOverlayTexts.addLast(debugText);
+
+ StringBuilder sb = new StringBuilder();
+ for (String text : mDebugOverlayTexts) {
+ sb.append(text);
+ sb.append("\n");
+ }
+
+ // Remove the last newline.
+ sb.setLength(sb.length() - 1);
+ mTemplateContext.getDebugOverlayHandler().updateDebugOverlayEntry("Gesture", sb.toString());
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java
new file mode 100644
index 0000000..17ef88e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.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.apphost.common;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.Place;
+import androidx.lifecycle.LifecycleRegistry;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Represents a layout that wraps a map view. */
+public interface MapViewContainer {
+ /**
+ * Returns the {@link LifecycleRegistry} instance that can be used by a parent of the container to
+ * drive the lifecycle events of the map view wrapped by it.
+ */
+ @NonNull
+ LifecycleRegistry getLifecycleRegistry();
+
+ /**
+ * Sets whether current location is enabled.
+ *
+ * @param enable true if the map should show the current location
+ */
+ void setCurrentLocationEnabled(boolean enable);
+
+ /** Sets the map anchor. The camera will be adjusted to include the anchor marker if necessary. */
+ void setAnchor(@Nullable Place anchor);
+
+ /**
+ * Sets the places to display in the map. The camera will be moved to the region that contains all
+ * the places.
+ */
+ void setPlaces(List<Place> places);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java
new file mode 100644
index 0000000..dbce0f8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.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.apphost.common;
+
+import android.os.RemoteException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/**
+ * A {@link AppServiceCall} decorated with a name, useful for logging.
+ *
+ * @param <ServiceT> the type of service to make the call for.
+ */
+public class NamedAppServiceCall<ServiceT> implements AppServiceCall<ServiceT> {
+ private final AppServiceCall<ServiceT> mCall;
+ private final CarAppApi mCarAppApi;
+
+ /** Creates an instance of a {@link NamedAppServiceCall} for the given API. */
+ public static <ServiceT> NamedAppServiceCall<ServiceT> create(
+ CarAppApi carAppApi, AppServiceCall<ServiceT> call) {
+ return new NamedAppServiceCall<>(carAppApi, call);
+ }
+
+ /** Returns the API this call is made for. */
+ public CarAppApi getCarAppApi() {
+ return mCarAppApi;
+ }
+
+ @Override
+ public void dispatch(ServiceT appService, ANRHandler.ANRToken anrToken) throws RemoteException {
+ mCall.dispatch(appService, anrToken);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + mCarAppApi.name() + "]";
+ }
+
+ private NamedAppServiceCall(CarAppApi carAppApi, AppServiceCall<ServiceT> call) {
+ mCall = call;
+ mCarAppApi = carAppApi;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java
new file mode 100644
index 0000000..a7084d0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.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.apphost.common;
+
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+
+import android.content.ComponentName;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+/**
+ * Default {@link IOnDoneCallback} that will log telemetry for API success and failure, handle ANR,
+ * as well as release the blocking thread, by setting a {@code null} on the blocking response for
+ * any api that blocks for this callback.
+ */
+public class OnDoneCallbackStub extends IOnDoneCallback.Stub implements OnDoneCallback {
+ private final ErrorHandler mErrorHandler;
+ private final ComponentName mAppName;
+ private final ANRHandler.ANRToken mANRToken;
+ private final TelemetryHandler mTelemetryHandler;
+ private final AppBindingStateProvider mAppBindingStateProvider;
+
+ /**
+ * Constructs an {@link OnDoneCallbackStub} that will release the given {@link
+ * ANRHandler.ANRToken} when {@link #onSuccess} or {@link #onFailure} is called.
+ */
+ public OnDoneCallbackStub(TemplateContext templateContext, ANRHandler.ANRToken anrToken) {
+ this(
+ templateContext.getErrorHandler(),
+ templateContext.getCarAppPackageInfo().getComponentName(),
+ anrToken,
+ templateContext.getTelemetryHandler(),
+ templateContext.getAppBindingStateProvider());
+ }
+
+ /**
+ * Constructs an {@link OnDoneCallbackStub} that will release the given {@link
+ * ANRHandler.ANRToken} when {@link #onSuccess} or {@link #onFailure} is called.
+ */
+ public OnDoneCallbackStub(
+ ErrorHandler errorHandler,
+ ComponentName appName,
+ ANRHandler.ANRToken anrToken,
+ TelemetryHandler telemetryHandler,
+ AppBindingStateProvider appBindingStateProvider) {
+ mErrorHandler = errorHandler;
+ mAppName = appName;
+ mANRToken = anrToken;
+ mTelemetryHandler = telemetryHandler;
+ mAppBindingStateProvider = appBindingStateProvider;
+ }
+
+ @CallSuper
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ mANRToken.dismiss();
+ mTelemetryHandler.logCarAppApiSuccessTelemetry(mAppName, mANRToken.getCarAppApi());
+ }
+
+ @CallSuper
+ @Override
+ public void onFailure(Bundleable failureResponse) {
+ mANRToken.dismiss();
+ ThreadUtils.runOnMain(
+ () -> {
+ FailureResponse failure;
+ try {
+ failure = (FailureResponse) failureResponse.get();
+
+ CarAppError.Builder errorBuilder =
+ CarAppError.builder(mAppName).setDebugMessage(failure.getStackTrace());
+ if (shouldLogTelemetryForError(
+ mANRToken.getCarAppApi(), mAppBindingStateProvider.isAppBound())) {
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ mAppName, mANRToken.getCarAppApi(), getErrorType(failure));
+ } else {
+ errorBuilder.setLogVerbose(true);
+ }
+
+ mErrorHandler.showError(errorBuilder.build());
+ } catch (BundlerException e) {
+ mErrorHandler.showError(CarAppError.builder(mAppName).setCause(e).build());
+
+ // If we fail to unbundle the response, log telemetry as a failed IPC due to bundling.
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ mAppName, mANRToken.getCarAppApi(), getErrorType(new FailureResponse(e)));
+ }
+ });
+ }
+
+ private static boolean shouldLogTelemetryForError(CarAppApi api, boolean isAppBound) {
+ boolean isApiPreBinding;
+ switch (api) {
+ case GET_APP_VERSION:
+ case ON_HANDSHAKE_COMPLETED:
+ case ON_APP_CREATE:
+ isApiPreBinding = true;
+ break;
+ default:
+ isApiPreBinding = false;
+ }
+ return isAppBound || isApiPreBinding;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java
new file mode 100644
index 0000000..2e9aeea
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java
@@ -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.apphost.common;
+
+import android.os.RemoteException;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+
+/**
+ * A request to send over the wire to the app.
+ *
+ * <p>The method interface of the client should be marked {@code oneway}.
+ *
+ * <p>You should not call {@link #send} yourself, but rather use the {@link AppDispatcher} to send
+ * this request. This allows for a single location to handle exceptions and performing IPC.
+ */
+public interface OneWayIPC {
+ /** Sends an IPC to the app, using the given {@link ANRToken}. */
+ void send(ANRToken anrToken) throws BundlerException, RemoteException;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java
new file mode 100644
index 0000000..72b5adc
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.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.apphost.common;
+
+/**
+ * Manages the state of routing information in template apps.
+ *
+ * <p>This class tracks the state of routing information across multiple template apps.
+ */
+public interface RoutingInfoState {
+ /** Sets whether routing information is visible on the car screen. */
+ void setIsRoutingInfoVisible(boolean isVisible);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java
new file mode 100644
index 0000000..4104d33
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * This class is forked from {@link android.view.ScaleGestureDetector} in order to modify {@link
+ * #mMinSpan} attribute.
+ *
+ * <p>{@link #mMinSpan} caused the detector to ignore pinch-zoom events when the distance between
+ * the fingers was too small. See b/193927730 for more details.
+ */
+public class ScaleGestureDetector {
+ private static final String TAG = "ScaleGestureDetector";
+
+ /**
+ * The listener for receiving notifications when gestures occur. If you want to listen for all the
+ * different gestures then implement this interface. If you only want to listen for a subset it
+ * might be easier to extend {@link SimpleOnScaleGestureListener}.
+ *
+ * <p>An application will receive events in the following order:
+ *
+ * <ul>
+ * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
+ * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
+ * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
+ * </ul>
+ */
+ public interface OnScaleGestureListener {
+ /**
+ * Responds to scaling events for a gesture in progress. Reported by pointer motion.
+ *
+ * @param detector The detector reporting the event - use this to retrieve extended info about
+ * event state.
+ * @return Whether or not the detector should consider this event as handled. If an event was
+ * not handled, the detector will continue to accumulate movement until an event is handled.
+ * This can be useful if an application, for example, only wants to update scaling factors
+ * if the change is greater than 0.01.
+ */
+ boolean onScale(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the beginning of a scaling gesture. Reported by new pointers going down.
+ *
+ * @param detector The detector reporting the event - use this to retrieve extended info about
+ * event state.
+ * @return Whether or not the detector should continue recognizing this gesture. For example, if
+ * a gesture is beginning with a focal point outside of a region where it makes sense,
+ * onScaleBegin() may return false to ignore the rest of the gesture.
+ */
+ boolean onScaleBegin(ScaleGestureDetector detector);
+
+ /**
+ * Responds to the end of a scale gesture. Reported by existing pointers going up.
+ *
+ * <p>Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} and {@link
+ * ScaleGestureDetector#getFocusY()} will return focal point of the pointers remaining on the
+ * screen.
+ *
+ * @param detector The detector reporting the event - use this to retrieve extended info about
+ * event state.
+ */
+ void onScaleEnd(ScaleGestureDetector detector);
+ }
+
+ /**
+ * A convenience class to extend when you only want to listen for a subset of scaling-related
+ * events. This implements all methods in {@link OnScaleGestureListener} but does nothing. {@link
+ * OnScaleGestureListener#onScale(ScaleGestureDetector)} returns {@code false} so that a subclass
+ * can retrieve the accumulated scale factor in an overridden onScaleEnd. {@link
+ * OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns {@code true}.
+ */
+ public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return false;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ // Intentionally empty
+ }
+ }
+
+ private final Context mContext;
+ private final OnScaleGestureListener mListener;
+
+ private float mFocusX;
+ private float mFocusY;
+
+ private boolean mQuickScaleEnabled;
+ private boolean mStylusScaleEnabled;
+
+ private float mCurrSpan;
+ private float mPrevSpan;
+ private float mInitialSpan;
+ private float mCurrSpanX;
+ private float mCurrSpanY;
+ private float mPrevSpanX;
+ private float mPrevSpanY;
+ private long mCurrTime;
+ private long mPrevTime;
+ private boolean mInProgress;
+ private final int mSpanSlop;
+ private final int mMinSpan;
+
+ private final Handler mHandler;
+
+ private float mAnchoredScaleStartX;
+ private float mAnchoredScaleStartY;
+ private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+
+ private static final float SCALE_FACTOR = .5f;
+ private static final int ANCHORED_SCALE_MODE_NONE = 0;
+ private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
+ private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
+
+ private GestureDetector mGestureDetector;
+
+ private boolean mEventBeforeOrAboveStartingGestureEvent;
+
+ /**
+ * Creates a ScaleGestureDetector with the supplied listener. You may only use this constructor
+ * from a {@link android.os.Looper Looper} thread.
+ *
+ * @param context the application's context
+ * @param listener the listener invoked for all the callbacks, this must not be null.
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ @SuppressWarnings("nullness:argument")
+ public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
+ this(context, listener, null, -1);
+ }
+
+ /**
+ * Creates a ScaleGestureDetector with the supplied listener.
+ *
+ * @see android.os.Handler#Handler()
+ * @param context the application's context
+ * @param listener the listener invoked for all the callbacks, this must not be null.
+ * @param handler the handler to use for running deferred listener events.
+ * @param minSpan the minimum span for the gesture to be recognized as a scale event.
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ @SuppressWarnings("nullness:method.invocation")
+ public ScaleGestureDetector(
+ Context context, OnScaleGestureListener listener, Handler handler, int minSpan) {
+ mContext = context;
+ mListener = listener;
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+ mSpanSlop = viewConfiguration.getScaledTouchSlop() * 2;
+ mMinSpan = Math.max(minSpan, 0);
+ mHandler = handler;
+ // Quick scale is enabled by default after JB_MR2
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+ if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ setQuickScaleEnabled(true);
+ }
+ // Stylus scale is enabled by default after LOLLIPOP_MR1
+ if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ setStylusScaleEnabled(true);
+ }
+ }
+
+ /**
+ * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} when
+ * appropriate.
+ *
+ * <p>Applications should pass a complete and consistent event stream to this method. A complete
+ * and consistent event stream involves all MotionEvents from the initial ACTION_DOWN to the final
+ * ACTION_UP or ACTION_CANCEL.
+ *
+ * @param event The event to process
+ * @return true if the event was processed and the detector wants to receive the rest of the
+ * MotionEvents in this event stream.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ mCurrTime = event.getEventTime();
+
+ final int action = event.getActionMasked();
+
+ // Forward the event to check for double tap gesture
+ if (mQuickScaleEnabled) {
+ mGestureDetector.onTouchEvent(event);
+ }
+
+ final int count = event.getPointerCount();
+ final boolean isStylusButtonDown =
+ (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
+
+ final boolean anchoredScaleCancelled =
+ mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
+ final boolean streamComplete =
+ action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || anchoredScaleCancelled;
+
+ if (action == MotionEvent.ACTION_DOWN || streamComplete) {
+ // Reset any scale in progress with the listener.
+ // If it's an ACTION_DOWN we're beginning a new event stream.
+ // This means the app probably didn't give us all the events. Shame on it.
+ if (mInProgress) {
+ mListener.onScaleEnd(this);
+ mInProgress = false;
+ mInitialSpan = 0;
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+ } else if (inAnchoredScaleMode() && streamComplete) {
+ mInProgress = false;
+ mInitialSpan = 0;
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+ }
+
+ if (streamComplete) {
+ return true;
+ }
+ }
+
+ if (!mInProgress
+ && mStylusScaleEnabled
+ && !inAnchoredScaleMode()
+ && !streamComplete
+ && isStylusButtonDown) {
+ // Start of a button scale gesture
+ mAnchoredScaleStartX = event.getX();
+ mAnchoredScaleStartY = event.getY();
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
+ mInitialSpan = 0;
+ }
+
+ final boolean configChanged =
+ action == MotionEvent.ACTION_DOWN
+ || action == MotionEvent.ACTION_POINTER_UP
+ || action == MotionEvent.ACTION_POINTER_DOWN
+ || anchoredScaleCancelled;
+
+ final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+ final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+ // Determine focal point
+ float sumX = 0;
+ float sumY = 0;
+ final int div = pointerUp ? count - 1 : count;
+ final float focusX;
+ final float focusY;
+ if (inAnchoredScaleMode()) {
+ // In anchored scale mode, the focal pt is always where the double tap
+ // or button down gesture started
+ focusX = mAnchoredScaleStartX;
+ focusY = mAnchoredScaleStartY;
+ if (event.getY() < focusY) {
+ mEventBeforeOrAboveStartingGestureEvent = true;
+ } else {
+ mEventBeforeOrAboveStartingGestureEvent = false;
+ }
+ } else {
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i) {
+ continue;
+ }
+ sumX += event.getX(i);
+ sumY += event.getY(i);
+ }
+
+ focusX = sumX / div;
+ focusY = sumY / div;
+ }
+
+ // Determine average deviation from focal point
+ float devSumX = 0;
+ float devSumY = 0;
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i) {
+ continue;
+ }
+
+ // Convert the resulting diameter into a radius.
+ devSumX += Math.abs(event.getX(i) - focusX);
+ devSumY += Math.abs(event.getY(i) - focusY);
+ }
+ final float devX = devSumX / div;
+ final float devY = devSumY / div;
+
+ // Span is the average distance between touch points through the focal point;
+ // i.e. the diameter of the circle with a radius of the average deviation from
+ // the focal point.
+ final float spanX = devX * 2;
+ final float spanY = devY * 2;
+ final float span;
+ if (inAnchoredScaleMode()) {
+ span = spanY;
+ } else {
+ span = (float) Math.hypot(spanX, spanY);
+ }
+
+ // Dispatch begin/end events as needed.
+ // If the configuration changes, notify the app to reset its current state by beginning
+ // a fresh scale event stream.
+ final boolean wasInProgress = mInProgress;
+ mFocusX = focusX;
+ mFocusY = focusY;
+ if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
+ mListener.onScaleEnd(this);
+ mInProgress = false;
+ mInitialSpan = span;
+ }
+ if (configChanged) {
+ mPrevSpanX = mCurrSpanX = spanX;
+ mPrevSpanY = mCurrSpanY = spanY;
+ mInitialSpan = mPrevSpan = mCurrSpan = span;
+ }
+
+ final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
+ if (!mInProgress
+ && span >= minSpan
+ && (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
+ mPrevSpanX = mCurrSpanX = spanX;
+ mPrevSpanY = mCurrSpanY = spanY;
+ mPrevSpan = mCurrSpan = span;
+ mPrevTime = mCurrTime;
+ mInProgress = mListener.onScaleBegin(this);
+ }
+
+ // Handle motion; focal point and span/scale factor are changing.
+ if (action == MotionEvent.ACTION_MOVE) {
+ mCurrSpanX = spanX;
+ mCurrSpanY = spanY;
+ mCurrSpan = span;
+
+ boolean updatePrev = true;
+
+ if (mInProgress) {
+ updatePrev = mListener.onScale(this);
+ }
+
+ if (updatePrev) {
+ mPrevSpanX = mCurrSpanX;
+ mPrevSpanY = mCurrSpanY;
+ mPrevSpan = mCurrSpan;
+ mPrevTime = mCurrTime;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean inAnchoredScaleMode() {
+ return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
+ }
+
+ /**
+ * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks when
+ * the user performs a doubleTap followed by a swipe. Note that this is enabled by default if the
+ * app targets API 19 and newer.
+ *
+ * @param scales true to enable quick scaling, false to disable
+ */
+ public void setQuickScaleEnabled(boolean scales) {
+ mQuickScaleEnabled = scales;
+ if (mQuickScaleEnabled && mGestureDetector == null) {
+ GestureDetector.SimpleOnGestureListener gestureListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ // Double tap: start watching for a swipe
+ mAnchoredScaleStartX = e.getX();
+ mAnchoredScaleStartY = e.getY();
+ mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
+ return true;
+ }
+ };
+ mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
+ }
+ }
+
+ /**
+ * Return whether the quick scale gesture, in which the user performs a double tap followed by a
+ * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
+ */
+ public boolean isQuickScaleEnabled() {
+ return mQuickScaleEnabled;
+ }
+
+ /**
+ * Sets whether the associates {@link OnScaleGestureListener} should receive onScale callbacks
+ * when the user uses a stylus and presses the button. Note that this is enabled by default if the
+ * app targets API 23 and newer.
+ *
+ * @param scales true to enable stylus scaling, false to disable.
+ */
+ public void setStylusScaleEnabled(boolean scales) {
+ mStylusScaleEnabled = scales;
+ }
+
+ /**
+ * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
+ * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
+ */
+ public boolean isStylusScaleEnabled() {
+ return mStylusScaleEnabled;
+ }
+
+ /** Returns {@code true} if a scale gesture is in progress. */
+ public boolean isInProgress() {
+ return mInProgress;
+ }
+
+ /**
+ * Get the X coordinate of the current gesture's focal point. If a gesture is in progress, the
+ * focal point is between each of the pointers forming the gesture.
+ *
+ * <p>If {@link #isInProgress()} would return false, the result of this function is undefined.
+ *
+ * @return X coordinate of the focal point in pixels.
+ */
+ public float getFocusX() {
+ return mFocusX;
+ }
+
+ /**
+ * Get the Y coordinate of the current gesture's focal point. If a gesture is in progress, the
+ * focal point is between each of the pointers forming the gesture.
+ *
+ * <p>If {@link #isInProgress()} would return false, the result of this function is undefined.
+ *
+ * @return Y coordinate of the focal point in pixels.
+ */
+ public float getFocusY() {
+ return mFocusY;
+ }
+
+ /**
+ * Return the average distance between each of the pointers forming the gesture in progress
+ * through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpan() {
+ return mCurrSpan;
+ }
+
+ /**
+ * Return the average X distance between each of the pointers forming the gesture in progress
+ * through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpanX() {
+ return mCurrSpanX;
+ }
+
+ /**
+ * Return the average Y distance between each of the pointers forming the gesture in progress
+ * through the focal point.
+ *
+ * @return Distance between pointers in pixels.
+ */
+ public float getCurrentSpanY() {
+ return mCurrSpanY;
+ }
+
+ /**
+ * Return the previous average distance between each of the pointers forming the gesture in
+ * progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpan() {
+ return mPrevSpan;
+ }
+
+ /**
+ * Return the previous average X distance between each of the pointers forming the gesture in
+ * progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpanX() {
+ return mPrevSpanX;
+ }
+
+ /**
+ * Return the previous average Y distance between each of the pointers forming the gesture in
+ * progress through the focal point.
+ *
+ * @return Previous distance between pointers in pixels.
+ */
+ public float getPreviousSpanY() {
+ return mPrevSpanY;
+ }
+
+ /**
+ * Return the scaling factor from the previous scale event to the current event. This value is
+ * defined as ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
+ *
+ * @return The current scaling factor.
+ */
+ public float getScaleFactor() {
+ if (inAnchoredScaleMode()) {
+ // Drag is moving up; the further away from the gesture
+ // start, the smaller the span should be, the closer,
+ // the larger the span, and therefore the larger the scale
+ final boolean scaleUp =
+ mEventBeforeOrAboveStartingGestureEvent
+ ? (mCurrSpan < mPrevSpan)
+ : (mCurrSpan > mPrevSpan);
+ final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
+ return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
+ }
+ return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
+ }
+
+ /**
+ * Return the time difference in milliseconds between the previous accepted scaling event and the
+ * current scaling event.
+ *
+ * @return Time difference since the last scaling event in milliseconds.
+ */
+ public long getTimeDelta() {
+ return mCurrTime - mPrevTime;
+ }
+
+ /**
+ * Return the event time of the current event being processed.
+ *
+ * @return Current event time in milliseconds.
+ */
+ public long getEventTime() {
+ return mCurrTime;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java
new file mode 100644
index 0000000..2f26643
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.view.View;
+
+/**
+ * A manager that allows presenters to control some attributes of the status bar, such as the color
+ * of the text and background.
+ */
+public interface StatusBarManager {
+ /** The type of status bar to display. */
+ enum StatusBarState {
+ /**
+ * The status bar is designed to be rendered over an app drawn surface such as a map, where it
+ * will have a background protection to ensure the user can read the status bar information.
+ */
+ OVER_SURFACE,
+
+ /**
+ * The status bar is designed to be rendered over a dark background (e.g. white text with
+ * transparent background).
+ */
+ LIGHT,
+
+ /** The status bar is designed the status bar */
+ GONE
+ }
+
+ /** Updates the {@link StatusBarState}. */
+ void setStatusBarState(StatusBarState statusBarState, View rootView);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java
new file mode 100644
index 0000000..b1ed7db
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/** Assorted string manipulation utilities. */
+public class StringUtils {
+ /** Milliseconds per unit of time LUT. Needs to be in sync with {@link #UNIT_SUFFIXES}. */
+ private static final long[] MILLIS_PER_UNIT =
+ new long[] {
+ DAYS.toMillis(1),
+ HOURS.toMillis(1),
+ MINUTES.toMillis(1),
+ SECONDS.toMillis(1),
+ 1 // 1 millisecond in milliseconds
+ };
+
+ private static final String[] UNIT_SUFFIXES = new String[] {"d", "h", "m", "s", "ms"};
+
+ /**
+ * Returns a compact string representation of a duration.
+ *
+ * <p>The format is {@code "xd:xh:xm:xs:xms"}, where {@code "x"} is an unpadded numeric value. If
+ * {@code "x"} is 0, it is altogether omitted.
+ *
+ * <p>For example, {@code "1d:25m:123ms"} denotes 1 day, 25 minutes, and 123 milliseconds.
+ *
+ * <p>Negative durations are returned as {@code "-"}
+ */
+ public static String formatDuration(long durationMillis) {
+ StringBuilder builder = new StringBuilder();
+ if (durationMillis < 0) {
+ return "-";
+ } else if (durationMillis == 0) {
+ return "0ms";
+ }
+ boolean first = true;
+ for (int i = 0; i < MILLIS_PER_UNIT.length; ++i) {
+ long value =
+ (i > 0 ? (durationMillis % MILLIS_PER_UNIT[i - 1]) : durationMillis) / MILLIS_PER_UNIT[i];
+ if (value > 0) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(":");
+ }
+ builder.append(value).append(UNIT_SUFFIXES[i]);
+ }
+ }
+ return builder.toString();
+ }
+
+ private StringUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java
new file mode 100644
index 0000000..5ff1e98
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java
@@ -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.apphost.common;
+
+/** Interface for handling surface callbacks such as pan and zoom. */
+public interface SurfaceCallbackHandler {
+
+ /** Returns whether a new gesture can begin. */
+ default boolean canStartNewGesture() {
+ return true;
+ }
+
+ /**
+ * Forwards a scroll gesture event to the car app's {@link
+ * androidx.car.app.ISurfaceCallback#onScroll(float, float)}.
+ */
+ void onScroll(float distanceX, float distanceY);
+
+ /**
+ * Forwards a fling gesture event to the car app's {@link
+ * androidx.car.app.ISurfaceCallback#onFling(float, float)}.
+ */
+ void onFling(float velocityX, float velocityY);
+
+ /**
+ * Forwards a scale gesture event to the car app's {@link
+ * androidx.car.app.ISurfaceCallback#onScale(float, float, float)}.
+ */
+ void onScale(float focusX, float focusY, float scaleFactor);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java
new file mode 100644
index 0000000..35f9dc3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.graphics.Rect;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A class for storing and retrieving the properties such as the visible area and stable center of a
+ * surface.
+ */
+public interface SurfaceInfoProvider {
+ /**
+ * Returns the {@link Rect} that specifies the region in the view where the templated-content
+ * (e.g. the card container, FAB) currently extends to. Returns {@code null} if the value is not
+ * set.
+ */
+ @Nullable Rect getVisibleArea();
+
+ /**
+ * Sets the safe area and if needed updates the stable center.
+ *
+ * <p>Subscribe to the event {@link EventManager.EventType#SURFACE_VISIBLE_AREA} to be notify when
+ * the safe area has been updated.
+ */
+ void setVisibleArea(Rect safeArea);
+
+ /**
+ * Returns the {@link Rect} that specifies the region of the stable visible area where the
+ * templated content (e.g. card container, action strip) could possibly extend to. It is stable in
+ * that the area is the guaranteed visible no matter any dynamic changes to the view. It is
+ * possible for stable area to increase or decrease due to changes in the template content or a
+ * template change.
+ */
+ @Nullable Rect getStableArea();
+
+ /** Indicates that the stable area should be recalculated the next time the safe area is set. */
+ void invalidateStableArea();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java
new file mode 100644
index 0000000..9217b4e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.os.SystemClock;
+
+/**
+ * Wrapper of SystemClock
+ *
+ * <p>Real instances should just delegate the calls to the static methods, while test instances
+ * return values set manually. See {@link android.os.SystemClock}.
+ */
+public final class SystemClockWrapper {
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @return elapsed milliseconds since boot
+ */
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java
new file mode 100644
index 0000000..2015581
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+import androidx.annotation.Nullable;
+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.logging.TelemetryHandler;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Context for various template components to retrieve important bits of information for presenting
+ * content.
+ */
+public abstract class TemplateContext extends ContextWrapper {
+
+ private final Map<Class<? extends AppHostService>, AppHostService> mAppHostServices =
+ new HashMap<>();
+
+ /**
+ * Constructs an instance of a {@link TemplateContext} wrapping the given {@link Context} object.
+ */
+ public TemplateContext(Context base) {
+ super(base);
+ }
+
+ /**
+ * Creates a {@link TemplateContext} that replaces the inner {@link Context} for the given {@link
+ * TemplateContext}.
+ *
+ * <p>This is used for using an uiContext for view elements, since they may have a theme applied
+ * on them.
+ *
+ * @param other the {@link TemplateContext} to wrap for all the getters
+ * @param uiContext the {@link Context} that this instance will wrap
+ */
+ public static TemplateContext from(TemplateContext other, Context uiContext) {
+ return new TemplateContext(uiContext) {
+ @Override
+ public CarAppPackageInfo getCarAppPackageInfo() {
+ return other.getCarAppPackageInfo();
+ }
+
+ @Override
+ public InputManager getInputManager() {
+ return other.getInputManager();
+ }
+
+ @Override
+ public ErrorHandler getErrorHandler() {
+ return other.getErrorHandler();
+ }
+
+ @Override
+ public ANRHandler getAnrHandler() {
+ return other.getAnrHandler();
+ }
+
+ @Override
+ public BackPressedHandler getBackPressedHandler() {
+ return other.getBackPressedHandler();
+ }
+
+ @Override
+ public SurfaceCallbackHandler getSurfaceCallbackHandler() {
+ return other.getSurfaceCallbackHandler();
+ }
+
+ @Override
+ public InputConfig getInputConfig() {
+ return other.getInputConfig();
+ }
+
+ @Override
+ public StatusBarManager getStatusBarManager() {
+ return other.getStatusBarManager();
+ }
+
+ @Override
+ public SurfaceInfoProvider getSurfaceInfoProvider() {
+ return other.getSurfaceInfoProvider();
+ }
+
+ @Override
+ public EventManager getEventManager() {
+ return other.getEventManager();
+ }
+
+ @Override
+ public AppDispatcher getAppDispatcher() {
+ return other.getAppDispatcher();
+ }
+
+ @Override
+ public SystemClockWrapper getSystemClockWrapper() {
+ return other.getSystemClockWrapper();
+ }
+
+ @Override
+ public ToastController getToastController() {
+ return other.getToastController();
+ }
+
+ @Override
+ @Nullable
+ public Context getAppConfigurationContext() {
+ return other.getAppConfigurationContext();
+ }
+
+ @Override
+ public CarAppManager getCarAppManager() {
+ return other.getCarAppManager();
+ }
+
+ @Override
+ public void updateConfiguration(Configuration configuration) {
+ other.updateConfiguration(configuration);
+ }
+
+ @Override
+ public TelemetryHandler getTelemetryHandler() {
+ return other.getTelemetryHandler();
+ }
+
+ @Override
+ public DebugOverlayHandler getDebugOverlayHandler() {
+ return other.getDebugOverlayHandler();
+ }
+
+ @Override
+ public RoutingInfoState getRoutingInfoState() {
+ return other.getRoutingInfoState();
+ }
+
+ @Override
+ public ColorContrastCheckState getColorContrastCheckState() {
+ return other.getColorContrastCheckState();
+ }
+
+ @Override
+ public ConstraintsProvider getConstraintsProvider() {
+ return other.getConstraintsProvider();
+ }
+
+ @Override
+ public CarHostConfig getCarHostConfig() {
+ return other.getCarHostConfig();
+ }
+
+ @Override
+ public HostResourceIds getHostResourceIds() {
+ return other.getHostResourceIds();
+ }
+
+ @Override
+ public AppBindingStateProvider getAppBindingStateProvider() {
+ return other.getAppBindingStateProvider();
+ }
+
+ @Override
+ public boolean registerAppHostService(
+ Class<? extends AppHostService> clazz, AppHostService appHostService) {
+ return other.registerAppHostService(clazz, appHostService);
+ }
+
+ @Override
+ @Nullable
+ public <T extends AppHostService> T getAppHostService(Class<T> clazz) {
+ // TODO(b/169182143): Make single use type services use this getter.
+ return other.getAppHostService(clazz);
+ }
+ };
+ }
+
+ /**
+ * Provides the package information such as accent colors, component names etc. associated with
+ * the 3p app.
+ */
+ public abstract CarAppPackageInfo getCarAppPackageInfo();
+
+ /** Provides the {@link InputManager} for the current car activity to bring up the keyboard. */
+ public abstract InputManager getInputManager();
+
+ /** Provides the {@link ErrorHandler} for displaying errors to the user. */
+ public abstract ErrorHandler getErrorHandler();
+
+ /** Provides the {@link ANRHandler} for managing ANRs. */
+ public abstract ANRHandler getAnrHandler();
+
+ /** Provides the {@link BackPressedHandler} for dispatching back press events to the app. */
+ public abstract BackPressedHandler getBackPressedHandler();
+
+ /** Provides the {@link SurfaceCallbackHandler} for dispatching surface callbacks to the app. */
+ public abstract SurfaceCallbackHandler getSurfaceCallbackHandler();
+
+ /** Provides the {@link InputConfig} to access the input configuration. */
+ public abstract InputConfig getInputConfig();
+
+ /**
+ * Provides the {@link StatusBarManager} to allow for overriding the status bar background and
+ * text color.
+ */
+ public abstract StatusBarManager getStatusBarManager();
+
+ /** Provides the {@link SurfaceInfoProvider} to allow storing and retrieving safe area insets. */
+ public abstract SurfaceInfoProvider getSurfaceInfoProvider();
+
+ /** Provides the {@link EventManager} to allow dispatching and subscribing to different events. */
+ public abstract EventManager getEventManager();
+
+ /** Provides the {@link AppDispatcher} which allows dispatching IPCs to the client app. */
+ public abstract AppDispatcher getAppDispatcher();
+
+ /** Returns the system {@link SystemClockWrapper}. */
+ public abstract SystemClockWrapper getSystemClockWrapper();
+
+ /** Returns the {@link ToastController} which allows clients to show toasts. */
+ public abstract ToastController getToastController();
+
+ /**
+ * Returns a {@link Context} instance for the remote app, configured with this context's
+ * configuration (which includes configuration from the car's resources, such as screen size and
+ * DPI).
+ *
+ * <p>The theme in this context is also set to the application's theme id, so that attributes in
+ * remote resources can be resolved using the that theme (see {@link
+ * ColorUtils#loadThemeId(Context, ComponentName)}).
+ *
+ * <p>Use method to load drawable resources from app's APKs, so that they are returned with the
+ * target DPI of the car screen, rather than the phone's. See b/159088813 for more details.
+ *
+ * @return the remote app's context, or {@code null} if unavailable due to an error (the logcat
+ * will contain a log with the error in this case).
+ */
+ @Nullable
+ public abstract Context getAppConfigurationContext();
+
+ /** Returns the {@link CarAppManager} that is to be used for starting and finishing car apps. */
+ public abstract CarAppManager getCarAppManager();
+
+ /**
+ * Updates the {@link Configuration} of the app configuration context that is retrieved via {@link
+ * #getAppConfigurationContext}, and publishes a {@link
+ * EventManager.EventType#CONFIGURATION_CHANGED} event using the {@link EventManager}.
+ */
+ public abstract void updateConfiguration(Configuration configuration);
+
+ /** Returns the {@link TelemetryHandler} instance that allows reporting telemetry data. */
+ public abstract TelemetryHandler getTelemetryHandler();
+
+ /** Returns the {@link DebugOverlayHandler} instance that updating the debug overlay. */
+ public abstract DebugOverlayHandler getDebugOverlayHandler();
+
+ /**
+ * Returns the {@link RoutingInfoState} that keeps track of the routing information state across
+ * template apps.
+ */
+ // TODO(b/169182143): Use a generic getService model to retrieve this object
+ public abstract RoutingInfoState getRoutingInfoState();
+
+ /**
+ * Returns the {@link RoutingInfoState} that keeps track of the color contrast check state in the
+ * current template.
+ */
+ public abstract ColorContrastCheckState getColorContrastCheckState();
+
+ /**
+ * Returns the {@link ConstraintsProvider} that can provide the limits associated with this car
+ * app.
+ */
+ public abstract ConstraintsProvider getConstraintsProvider();
+
+ /**
+ * Returns a {@link CarHostConfig} object containing a series of flags and configuration options
+ */
+ public abstract CarHostConfig getCarHostConfig();
+
+ /** Produces a status report for this context, used for diagnostics and logging. */
+ public void reportStatus(PrintWriter pw) {}
+
+ /** Returns the {@link HostResourceIds} to use for this host implementation */
+ public abstract HostResourceIds getHostResourceIds();
+
+ /** Returns the {@link AppBindingStateProvider} instance. */
+ public abstract AppBindingStateProvider getAppBindingStateProvider();
+
+ /**
+ * Dynamically registers a {@link AppHostService}.
+ *
+ * @return {@code true} if register is successful, {@code false} if the service already exists.
+ */
+ public boolean registerAppHostService(
+ Class<? extends AppHostService> clazz, AppHostService appHostService) {
+ if (mAppHostServices.containsKey(clazz)) {
+ return false;
+ }
+
+ mAppHostServices.put(clazz, appHostService);
+ return true;
+ }
+
+ /**
+ * Returns the {@link AppHostService} of the requested class, or {@code null} if it does not exist
+ * for this host.
+ */
+ @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof T
+ @Nullable
+ public <T extends AppHostService> T getAppHostService(Class<T> clazz) {
+ return (T) mAppHostServices.get(clazz);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java
new file mode 100644
index 0000000..25c0985
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.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.apphost.common;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/** Utility functions to handle running functions on the main thread. */
+public class ThreadUtils {
+ private static final Handler HANDLER = new Handler(Looper.getMainLooper());
+
+ /** Field assignment is atomic in java and we are only checking reference equality. */
+ private static Thread sMainThread;
+
+ /** Executes the {@code action} on the main thread. */
+ public static void runOnMain(Runnable action) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ action.run();
+ } else {
+ HANDLER.post(action);
+ }
+ }
+
+ /** Enqueues the {@code action} to the message queue on the main thread. */
+ public static void enqueueOnMain(Runnable action) {
+ HANDLER.post(action);
+ }
+
+ /**
+ * Checks that currently running on the main thread.
+ *
+ * @throws IllegalStateException if the current thread is not the main thread
+ */
+ public static void checkMainThread() {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("Not running on main thread when it is required to.");
+ }
+ }
+
+ /** Returns true if the current thread is the UI thread. */
+ public static boolean getsMainThread() {
+ if (sMainThread == null) {
+ sMainThread = Looper.getMainLooper().getThread();
+ }
+ return Thread.currentThread() == sMainThread;
+ }
+
+ /** Checks that the current thread is the UI thread. Otherwise throws an exception. */
+ public static void ensureMainThread() {
+ if (!getsMainThread()) {
+ throw new AssertionError("Must be called on the UI thread");
+ }
+ }
+
+ private ThreadUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java
new file mode 100644
index 0000000..a1ebafe
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java
@@ -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.apphost.common;
+
+import android.widget.Toast;
+
+/** Allows controlling the toasts on car screen. */
+public interface ToastController {
+ /**
+ * Shows the Toast view with the specified text for the specified duration.
+ *
+ * @param text the text message to be displayed
+ * @param duration how long to display the message. Either {@link Toast#LENGTH_SHORT} or {@link
+ * Toast#LENGTH_LONG}
+ */
+ void showToast(CharSequence text, int duration);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java
new file mode 100644
index 0000000..242591a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction;
+
+/** A {@link FlowViolationException} that indicates an incorrect back flow. */
+public class BackFlowViolationException extends FlowViolationException {
+ BackFlowViolationException(String message) {
+ super(message);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java
new file mode 100644
index 0000000..c0685d0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction;
+
+/** Wrapper class for exceptions that indicate template flow violations. */
+public abstract class FlowViolationException extends Exception {
+ protected FlowViolationException(String message) {
+ super(message);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java
new file mode 100644
index 0000000..4892bf7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction;
+
+/** A {@link FlowViolationException} that indicates the flow is over the max limit. */
+public class OverLimitFlowViolationException extends FlowViolationException {
+ protected OverLimitFlowViolationException(String message) {
+ super(message);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java
new file mode 100644
index 0000000..04d5118
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction;
+
+import android.content.Context;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateInfo;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.signin.SignInTemplate;
+import androidx.car.app.navigation.model.NavigationTemplate;
+import com.android.car.libraries.apphost.common.AppHostService;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.checkers.TemplateChecker;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A class for validating whether an app's template flow abide by the flow rules.
+ *
+ * <p>The host should call {@link #validateFlow} to check whether a new template is allowed in the
+ * flow, governed by the following rules:
+ *
+ * <ul>
+ * <li>BACK: if the new template contains the same ID and type as another template that have
+ * already been seen, it is considered a back operation, and the step count will be reset to
+ * the value used for the previously-seen template.
+ * <li>REFRESH: if the new template does not contain different immutable contents compared to the
+ * most recent template, it is considered a refresh and the step count will not increased.
+ * <li>NEW: Otherwise, the template is considered a new view and is only allowed if the given step
+ * limit has not been reached. If the template is allowed and is a consumption view, as
+ * defined by {@link #isConsumptionView}, the step count is reset and the next new template
+ * will start from a step count of zero again.
+ * </ul>
+ *
+ * <p>See go/watevra-distraction-part1 for more details.
+ */
+public class TemplateValidator implements AppHostService {
+ private final int mStepLimit;
+ private @Nullable TemplateWrapper mLastTemplateWrapper;
+ private final Deque<TemplateStackItem> mTemplateItemStack = new ArrayDeque<>();
+
+ /**
+ * When set to the true, the next template received for validation will have its step reset tot
+ * zero (e.g. the template will be considered the start of a new task).
+ */
+ private boolean mIsReset;
+
+ /**
+ * When set to the true, the next template received for validation will be considered a refresh
+ * regardless of content as long as it is of the same type.
+ */
+ private boolean mIsNextTemplateContentRefreshIfSameType;
+
+ /**
+ * The step count of the last sent template.
+ *
+ * <p>Note that this value is 1-based. For example, the first template is step 1.
+ */
+ private int mLastStep;
+
+ private final Map<Class<? extends Template>, TemplateChecker<? extends Template>>
+ mTemplateCheckerMap = new HashMap<>();
+
+ /** Constructs a {@link TemplateValidator} instance with a given maximum number of steps. */
+ public static TemplateValidator create(int stepLimit) {
+ return new TemplateValidator(stepLimit);
+ }
+
+ /**
+ * Registers a {@link TemplateChecker} to be used for the template type during the {@link
+ * #validateFlow} operation.
+ */
+ public <T extends Template> void registerTemplateChecker(
+ Class<T> templateClass, TemplateChecker<T> checker) {
+ mTemplateCheckerMap.put(templateClass, checker);
+ }
+
+ /** Reset the current step count on the next template received. */
+ public void reset() {
+ // Note that we don't clear the stack here. The host needs to keep track of the templates
+ // it has seen, so that it can compare the list of TemplateInfo inside TemplateWrapper,
+ // and not count them after the refresh. See b/179085934 for more details.
+ // Additionally, we don't reset mLastTemplateWrapper since that will mean navigating out of
+ // the app (IE to the launcher) and back will cause the template to be recreated rather than
+ // refreshed.
+ mIsReset = true;
+ }
+
+ /**
+ * Sets whether the next template should be considered a refresh as long as it is of the same
+ * type.
+ */
+ public void setIsNextTemplateContentRefreshIfSameType(boolean isContentRefresh) {
+ mIsNextTemplateContentRefreshIfSameType = isContentRefresh;
+ }
+
+ /** Whether the next template should be considered a refresh as long as it is of the same type. */
+ @VisibleForTesting
+ boolean isNextTemplateContentRefreshIfSameType() {
+ return mIsNextTemplateContentRefreshIfSameType;
+ }
+
+ /** Returns the step count that was used for the last template. */
+ @VisibleForTesting
+ public int getLastStep() {
+ return mLastStep;
+ }
+
+ /** Returns whether the validator will reset the step count on the next template received. */
+ @VisibleForTesting
+ public boolean isPendingReset() {
+ return mIsReset;
+ }
+
+ @Override
+ public String toString() {
+ return "[ step limit: " + mStepLimit + ", last step: " + mLastStep + "]";
+ }
+
+ /**
+ * Validates whether the application has the required permissions for this template.
+ *
+ * @throws SecurityException if the application is missing any required permission
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"}) // ignoring TemplateChecker raw type warnings.
+ public void validateHasRequiredPermissions(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+ Template template = templateWrapper.getTemplate();
+
+ TemplateChecker checker = mTemplateCheckerMap.get(template.getClass());
+ if (checker == null) {
+ throw new IllegalStateException(
+ "Permission check failed. No checker has been registered for the template"
+ + " type: "
+ + template);
+ }
+
+ Context appConfigurationContext = templateContext.getAppConfigurationContext();
+ if (appConfigurationContext == null) {
+ L.d(
+ LogTags.DISTRACTION,
+ "Permission check failed. No app configuration context is registered.");
+ // If we do not have a context for the car app do not allow due to missing
+ // permissions, this is a bad state.
+ throw new IllegalStateException(
+ "Could not validate whether the app has required permissions");
+ }
+
+ checker.checkPermissions(appConfigurationContext, template);
+ }
+
+ /**
+ * Validates whether the given {@link TemplateWrapper} meets the flow restriction requirements.
+ *
+ * @throws FlowViolationException if the new template contains the same ID as a previously seen
+ * template but is of a different template type
+ * @throws FlowViolationException if the step limit has been reached and the template is not
+ */
+ public void validateFlow(TemplateWrapper templateWrapper) throws FlowViolationException {
+ fillInBackStackIfNeeded(templateWrapper);
+
+ boolean isNextTemplateContentRefreshIfSameType = mIsNextTemplateContentRefreshIfSameType;
+ mIsNextTemplateContentRefreshIfSameType = false;
+
+ // Order is important here. We want to make sure we check for back first because there
+ // might be cases when an app goes back from one template to another, the content changes
+ // might satisfy the refresh conditions, thus keeping the current step instead of
+ // decrementing to the previous step.
+ if (validateBackFlow(templateWrapper)
+ || validateRefreshFlow(templateWrapper, isNextTemplateContentRefreshIfSameType)) {
+ mLastTemplateWrapper = templateWrapper;
+ return;
+ }
+
+ // Before we check whether a new template is allowed, check whether a reset should happen so
+ // we don't prematurely disallow the next template.
+ Template template = templateWrapper.getTemplate();
+
+ // Parked-only template should not increment step count.
+ int currentStep = isParkedOnlyTemplate(template.getClass()) ? mLastStep : mLastStep + 1;
+ currentStep = resetTaskStepIfNeeded(currentStep, template.getClass());
+
+ throwIfNewTemplateDisallowed(currentStep, templateWrapper);
+
+ L.d(
+ LogTags.DISTRACTION,
+ "NEW template detected. Task step currently at %d of %d. %s",
+ currentStep,
+ mStepLimit,
+ templateWrapper);
+
+ templateWrapper.setCurrentTaskStep(currentStep);
+ mTemplateItemStack.push(
+ new TemplateStackItem(
+ templateWrapper.getId(), template.getClass(), templateWrapper.getCurrentTaskStep()));
+ mLastTemplateWrapper = templateWrapper;
+ mLastStep = currentStep;
+ }
+
+ private void fillInBackStackIfNeeded(TemplateWrapper templateWrapper) {
+ // The template infos are ordered as follows: top, second, third, bottom
+ // Look through our known stack, if there are more than 1 templates in the top that we do
+ // not currently have, we need to add them to our stack.
+ //
+ // If there is 1 extra template, it'll be handled by the pushing logic in validateFlow.
+ //
+ // If there the top template ids are the same, it will be handled by the logic in
+ // validateRefreshFlow.
+ //
+ // If there are less in the client provided stack, it will be handled by the logic in
+ // validateBackFlow.
+ Deque<TemplateInfo> newTemplates = new ArrayDeque<>();
+ for (TemplateInfo templateInfo : templateWrapper.getTemplateInfosForScreenStack()) {
+ // For each not known template push it onto a separate stack, so that after all the
+ // pushes, it will be ordered as follows:
+ //
+ // i.e. if the client has 3 new templates that the host does not know about this
+ // temporary stack will be third, second, top
+ if (findExistingTemplateStackItem(templateInfo.getTemplateId()) == null) {
+ newTemplates.push(templateInfo);
+ } else {
+ break;
+ }
+ }
+
+ // At this point the "newTemplates" stack contains any values they are new templates that
+ // the host does not know about.
+ // We do not need to push the bottom of this new stack, as that is the new template which
+ // will be handled by validateFlow.
+ while (newTemplates.size() > 1) {
+ // Set last template wrapper to null so that we don't check if the new one is possibly a
+ // refresh since we are preseeding templates in between the current top and the new top.
+ mLastTemplateWrapper = null;
+ TemplateInfo info = newTemplates.pop();
+ Class<? extends Template> templateClass = info.getTemplateClass();
+
+ // Parked-only template should not increment step count.
+ int currentStep = isParkedOnlyTemplate(templateClass) ? mLastStep : mLastStep + 1;
+ currentStep = resetTaskStepIfNeeded(currentStep, templateClass);
+ mTemplateItemStack.push(
+ new TemplateStackItem(info.getTemplateId(), templateClass, currentStep));
+ mLastStep = currentStep;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link TemplateWrapper} is a refresh of the last-sent
+ * template based on the registered {@link TemplateChecker}, or {@code false otherwise}.
+ *
+ * <p>Note that if a {@link TemplateChecker} is not available for a template type, the {@link
+ * #validateFlow} operation will return false by default.
+ *
+ * <p>A template is considered a refresh if it is of the same template type and does not have data
+ * that we consider immutable as compared to the previous template. If the input template is
+ * deemed a refresh, the task step count will be changed.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"}) // ignoring TemplateChecker raw type warnings.
+ private boolean validateRefreshFlow(
+ TemplateWrapper templateWrapper, boolean isNextTemplateContentRefreshIfSameType) {
+ TemplateWrapper lastTemplateWrapper = mLastTemplateWrapper;
+ TemplateStackItem lastTemplateStackItem = mTemplateItemStack.peek();
+ if (lastTemplateWrapper == null || lastTemplateStackItem == null) {
+ return false;
+ }
+
+ Template lastTemplate = lastTemplateWrapper.getTemplate();
+ Template newTemplate = templateWrapper.getTemplate();
+
+ TemplateChecker checker = mTemplateCheckerMap.get(newTemplate.getClass());
+ if (checker == null) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. No checker has been registered for the template type:" + " %s",
+ newTemplate.getClass());
+ return false;
+ }
+
+ if (lastTemplate.getClass() != newTemplate.getClass()) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. Template type differs (previous: %s, new: %s).",
+ lastTemplateWrapper,
+ templateWrapper);
+ return false;
+ }
+
+ if (isNextTemplateContentRefreshIfSameType || checker.isRefresh(newTemplate, lastTemplate)) {
+ int currentStep =
+ resetTaskStepIfNeeded(lastTemplateStackItem.getStep(), newTemplate.getClass());
+ templateWrapper.setCurrentTaskStep(currentStep);
+ templateWrapper.setRefresh(true);
+ mLastStep = currentStep;
+
+ // We push the new template as a new stack item so that we can keep track of the refresh
+ // stack. This is needed to handle a case where if a template is refreshed across
+ // multiple screens (e.g. same template content, different template ids), when the app
+ // pops back to a previous screen and sends the previous template, the host will
+ // recognize the id in the stack and consider it a back operation. (See b/160892144 for
+ // more context).
+ mTemplateItemStack.push(
+ new TemplateStackItem(
+ templateWrapper.getId(),
+ newTemplate.getClass(),
+ templateWrapper.getCurrentTaskStep()));
+
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH detected. Task step currently at %d of %d. %s",
+ templateWrapper.getCurrentTaskStep(),
+ mStepLimit,
+ templateWrapper);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given {@link TemplateWrapper} is a back operation. Returns {@code true} if
+ * so, {@code false otherwise}.
+ *
+ * <p>A template is considered a back operation if it is of the same template type and the same ID
+ * as a template that is already on the stack. If the input template is deemed to be a back
+ * operation, method will pop any templates on the stack above the target template we are going
+ * back to, and reset the task step count to the value held by the target template.
+ *
+ * @throws FlowViolationException if the target template with the matching ID is of a different
+ * template type
+ */
+ private boolean validateBackFlow(TemplateWrapper templateWrapper) throws FlowViolationException {
+ String id = templateWrapper.getId();
+ Template template = templateWrapper.getTemplate();
+
+ // This detects the case where the app is popping screens (e.g. going back).
+ // If there is a template with a matching ID and type in the stack, pop everything
+ // above the found item, then update the template and set the task step to the value at that
+ // found item.
+ TemplateStackItem foundItem = findExistingTemplateStackItem(id);
+ if (foundItem != null) {
+ if (foundItem.getTemplateClass() != template.getClass()) {
+ throw new BackFlowViolationException(
+ String.format(
+ "BACK operation failed. Template types differ (previous: %s, new:" + " %s).",
+ foundItem, templateWrapper));
+ }
+
+ // A special case where if the found template is already at the top of stack, then
+ // it is not a back, but a refresh (e.g. an app sending the same template as before).
+ if (foundItem == mTemplateItemStack.peek()) {
+ return false;
+ }
+
+ while (foundItem != mTemplateItemStack.peek()) {
+ mTemplateItemStack.pop();
+ }
+
+ L.d(
+ LogTags.DISTRACTION,
+ "BACK detected. Task step currently at %d of %d. %s",
+ foundItem.getStep(),
+ mStepLimit,
+ templateWrapper);
+
+ // Set the task step back to the value of the found template the app is going back to.
+ int currentStep = resetTaskStepIfNeeded(foundItem.getStep(), template.getClass());
+ templateWrapper.setCurrentTaskStep(currentStep);
+ mLastStep = currentStep;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the {@link TemplateStackItem} currently in the stack with the given ID, or {@code null}
+ * if none is found.
+ */
+ private @Nullable TemplateStackItem findExistingTemplateStackItem(String id) {
+ TemplateStackItem foundItem = null;
+ for (TemplateStackItem stackItem : mTemplateItemStack) {
+ if (stackItem.getTemplateId().equals(id)) {
+ foundItem = stackItem;
+ break;
+ }
+ }
+
+ return foundItem;
+ }
+
+ /**
+ * Validates that we still have budget for a new template, throw otherwise. If it is the last step
+ * in the flow, also validates that only certain template classes are allowed, throw otherwise.
+ */
+ private void throwIfNewTemplateDisallowed(int nextStepToUse, TemplateWrapper templateWrapper)
+ throws OverLimitFlowViolationException {
+ // Check that we still have quota.
+ if (nextStepToUse > mStepLimit) {
+ throw new OverLimitFlowViolationException(
+ String.format("No template allowed after %d templates. %s", mStepLimit, templateWrapper));
+ }
+
+ // Special case for the last step - only certain template types are supported.
+ // 1. For NavigationTemplates, they are consumption view so they will reset the step count.
+ // 2. For SignInTemplates and LongMessageTemplates, they are parked-only and will not
+ // increase the step count.
+ // 3. PaneTemplates and MessageTemplates are the only other two templates that are allowed
+ // at the end of a task.
+ if (nextStepToUse == mStepLimit) {
+ Class<? extends Template> templateClass = templateWrapper.getTemplate().getClass();
+ if (!(templateClass.equals(NavigationTemplate.class)
+ || templateClass.equals(PaneTemplate.class)
+ || templateClass.equals(MessageTemplate.class)
+ || templateClass.equals(SignInTemplate.class)
+ || templateClass.equals(LongMessageTemplate.class))) {
+ throw new OverLimitFlowViolationException(
+ String.format(
+ "Unsupported template type as the last step in a task. %s", templateWrapper));
+ }
+ }
+ }
+
+ private TemplateValidator(int stepLimit) {
+ mStepLimit = stepLimit;
+ }
+
+ /**
+ * Returns the task step that should be used for the next template, resetting it to 1 if a reset
+ * has been requested or if the template is a "consumption view".
+ */
+ private int resetTaskStepIfNeeded(int taskStep, Class<? extends Template> templateClass) {
+ if (mIsReset || isConsumptionView(templateClass)) {
+ taskStep = 1;
+ L.d(LogTags.DISTRACTION, "Resetting task step to %d. %s", taskStep, templateClass.getName());
+ mIsReset = false;
+ }
+
+ return taskStep;
+ }
+
+ /**
+ * Returns whether the given {@link Template} is a "consumption view".
+ *
+ * <p>Consumption views are defined as “sit-and-stay” experiences. In our library's context, these
+ * is the {@link NavigationTemplate}, and can be extended to other templates such as media
+ * playback and in-call view templates in the future when we support them.
+ */
+ private static boolean isConsumptionView(Class<? extends Template> templateClass) {
+ boolean isConsumptionTemplate = NavigationTemplate.class.equals(templateClass);
+ if (isConsumptionTemplate) {
+ L.d(LogTags.DISTRACTION, "Consumption template detected. %s", templateClass.getName());
+ }
+ return isConsumptionTemplate;
+ }
+
+ /** Returns whether the given {@link Template} is a parked-only template. */
+ private static boolean isParkedOnlyTemplate(Class<? extends Template> templateClass) {
+ boolean isParkedOnly =
+ SignInTemplate.class.equals(templateClass)
+ || LongMessageTemplate.class.equals(templateClass);
+ if (isParkedOnly) {
+ L.d(LogTags.DISTRACTION, "Parked only template detected. %s", templateClass.getName());
+ }
+ return isParkedOnly;
+ }
+
+ /** Structure contain the template information to be stored onto the stack. */
+ private static class TemplateStackItem {
+ private final String mTemplateid;
+ private final Class<? extends Template> mTemplateClass;
+ private final int mStep;
+
+ TemplateStackItem(String templateid, Class<? extends Template> templateClass, int step) {
+ mTemplateid = templateid;
+ mTemplateClass = templateClass;
+ mStep = step;
+ }
+
+ String getTemplateId() {
+ return mTemplateid;
+ }
+
+ Class<? extends Template> getTemplateClass() {
+ return mTemplateClass;
+ }
+
+ int getStep() {
+ return mStep;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java
new file mode 100644
index 0000000..706c227
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.Item;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.List;
+import java.util.Objects;
+
+/** Shared util methods for handling different template checking logic. */
+public class CheckerUtils {
+ /** Returns whether the sizes and string contents of the two lists of items are equal. */
+ public static <T extends Item> boolean itemsHaveSameContent(
+ List<T> itemList1, List<T> itemList2) {
+ if (itemList1.size() != itemList2.size()) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. Different item list sizes. Old: %d. New: %d",
+ itemList1.size(),
+ itemList2.size());
+ return false;
+ }
+
+ for (int i = 0; i < itemList1.size(); i++) {
+ T itemObj1 = itemList1.get(i);
+ T itemObj2 = itemList2.get(i);
+
+ if (itemObj1.getClass() != itemObj2.getClass()) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. Different item types at index %d. Old: %s. New: %s",
+ i,
+ itemObj1.getClass(),
+ itemObj2.getClass());
+ return false;
+ }
+
+ if (itemObj1 instanceof Row) {
+ if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i)) {
+ return false;
+ }
+ } else if (itemObj1 instanceof GridItem) {
+ if (!gridItemsHaveSameContent((GridItem) itemObj1, (GridItem) itemObj2, i)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns whether the string contents of the two rows are equal. */
+ private static boolean rowsHaveSameContent(Row row1, Row row2, int index) {
+ // Special case for rows with toggles - if the toggle state has changed, then text updates
+ // are allowed.
+ if (toggleStateHasChanged(row1.getToggle(), row2.getToggle())) {
+ return true;
+ }
+
+ if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. Different row titles at index %d. Old: %s. New: %s",
+ index,
+ row1.getTitle(),
+ row2.getTitle());
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Returns whether the string contents of the two grid items are equal. */
+ private static boolean gridItemsHaveSameContent(
+ GridItem gridItem1, GridItem gridItem2, int index) {
+ // We only check the item's title - changes in text and image are considered a refresh.
+ if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
+ L.d(
+ LogTags.DISTRACTION,
+ "REFRESH check failed. Different grid item titles at index %d. Old: %s. New:" + " %s",
+ index,
+ gridItem1.getTitle(),
+ gridItem2.getTitle());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether the strings of the two {@link CarText}s are the same.
+ *
+ * <p>Spans that are attached to the strings are ignored from the comparison.
+ */
+ private static boolean carTextsHasSameString(
+ @Nullable CarText carText1, @Nullable CarText carText2) {
+ // If both carText1 and carText2 are null, return true. If only one of them is null, return
+ // false.
+ if (carText1 == null || carText2 == null) {
+ return carText1 == null && carText2 == null;
+ }
+
+ return Objects.equals(carText1.toString(), carText2.toString());
+ }
+
+ private static boolean toggleStateHasChanged(@Nullable Toggle toggle1, @Nullable Toggle toggle2) {
+ return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+ }
+
+ private CheckerUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java
new file mode 100644
index 0000000..fecd3f0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Toggle;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link GridTemplate} */
+public class GridTemplateChecker implements TemplateChecker<GridTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the number of grid items and the string contents
+ * (title, texts) of each grid item have not changed.
+ * <li>For grid items that contain a {@link Toggle}, updates to the title, text and image are
+ * also allowed if the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(GridTemplate newTemplate, GridTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ ItemList oldList = oldTemplate.getSingleList();
+ ItemList newList = newTemplate.getSingleList();
+ if (oldList != null && newList != null) {
+ return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+ }
+
+ return true;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java
new file mode 100644
index 0000000..a3023f2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.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.apphost.distraction.checkers;
+
+import androidx.car.app.model.Item;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.SectionedItemList;
+import androidx.car.app.model.Toggle;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link ListTemplate} */
+public class ListTemplateChecker implements TemplateChecker<ListTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the {@link ItemList} structure between the
+ * templates have not changed. This means that if the previous template has multiple {@link
+ * ItemList} sections, the new template must have the same number of sections with the same
+ * headers. Further, the number of rows and the string contents (title, texts, not counting
+ * spans) of each row must not have changed.
+ * <li>For rows that contain a {@link Toggle}, updates to the title or texts are also allowed if
+ * the toggle state has changed between the previous and new templates.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(ListTemplate newTemplate, ListTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ ItemList oldList = oldTemplate.getSingleList();
+ ItemList newList = newTemplate.getSingleList();
+ if (oldList != null && newList != null) {
+ return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+ } else {
+ List<SectionedItemList> oldSectionedList = oldTemplate.getSectionedLists();
+ List<SectionedItemList> newSectionedList = newTemplate.getSectionedLists();
+
+ if (oldSectionedList.size() != newSectionedList.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < newSectionedList.size(); i++) {
+ SectionedItemList newSection = newSectionedList.get(i);
+ SectionedItemList oldSection = oldSectionedList.get(i);
+
+ ItemList oldItemList = oldSection.getItemList();
+ ItemList newItemList = newSection.getItemList();
+ List<Item> oldSubList =
+ oldItemList == null ? Collections.emptyList() : oldItemList.getItems();
+ List<Item> newSubList =
+ newItemList == null ? Collections.emptyList() : newItemList.getItems();
+ if (!Objects.equals(newSection.getHeader(), oldSection.getHeader())
+ || !CheckerUtils.itemsHaveSameContent(oldSubList, newSubList)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java
new file mode 100644
index 0000000..b3c698a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.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.apphost.distraction.checkers;
+
+import androidx.car.app.model.MessageTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link MessageTemplate} */
+public class MessageTemplateChecker implements TemplateChecker<MessageTemplate> {
+ /**
+ * A new template is considered a refresh of a previous one if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title and messages have not changed.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(MessageTemplate newTemplate, MessageTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is not considered a refresh.
+ return false;
+ }
+
+ return Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())
+ && Objects.equals(oldTemplate.getDebugMessage(), newTemplate.getDebugMessage())
+ && Objects.equals(oldTemplate.getMessage(), newTemplate.getMessage());
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java
new file mode 100644
index 0000000..596dec5
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java
@@ -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.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.navigation.model.NavigationTemplate;
+
+/** A {@link TemplateChecker} implementation for {@link NavigationTemplate} */
+public class NavigationTemplateChecker implements TemplateChecker<NavigationTemplate> {
+ @Override
+ public boolean isRefresh(NavigationTemplate newTemplate, NavigationTemplate oldTemplate) {
+ // Always allow routing template refreshes.
+ return true;
+ }
+
+ @Override
+ public void checkPermissions(Context context, NavigationTemplate newTemplate) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java
new file mode 100644
index 0000000..cf07650
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.PaneTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PaneTemplate} */
+public class PaneTemplateChecker implements TemplateChecker<PaneTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the number of rows and the string contents
+ * (title, texts, not counting spans) of each row between the previous and new {@link Pane}s
+ * have not changed.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(PaneTemplate newTemplate, PaneTemplate oldTemplate) {
+ Pane oldPane = oldTemplate.getPane();
+ Pane newPane = newTemplate.getPane();
+ if (oldPane.isLoading()) {
+ return true;
+ } else if (newPane.isLoading()) {
+ return false;
+ }
+
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ return CheckerUtils.itemsHaveSameContent(oldPane.getRows(), newPane.getRows());
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java
new file mode 100644
index 0000000..edaf6e7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.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.apphost.distraction.checkers;
+
+import android.Manifest.permission;
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.PlaceListMapTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PlaceListMapTemplate} */
+public class PlaceListMapTemplateChecker implements TemplateChecker<PlaceListMapTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the number of rows and the string contents
+ * (title, texts, not counting spans) of each row between the previous and new {@link
+ * ItemList}s have not changed.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(PlaceListMapTemplate newTemplate, PlaceListMapTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ ItemList oldList = oldTemplate.getItemList();
+ ItemList newList = newTemplate.getItemList();
+ if (oldList != null && newList != null) {
+ return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void checkPermissions(Context context, PlaceListMapTemplate newTemplate) {
+ if (newTemplate.isCurrentLocationEnabled()) {
+ CarAppPermission.checkHasPermission(context, permission.ACCESS_FINE_LOCATION);
+ }
+
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.MAP_TEMPLATES);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java
new file mode 100644
index 0000000..7bf534b
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PlaceListNavigationTemplate} */
+public class PlaceListNavigationTemplateChecker
+ implements TemplateChecker<PlaceListNavigationTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the number of rows and the string contents
+ * (title, texts, not counting spans) of each row between the previous and new {@link
+ * ItemList}s have not changed.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(
+ PlaceListNavigationTemplate newTemplate, PlaceListNavigationTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ ItemList oldList = oldTemplate.getItemList();
+ ItemList newList = newTemplate.getItemList();
+ if (oldList != null && newList != null) {
+ return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void checkPermissions(Context context, PlaceListNavigationTemplate newTemplate) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java
new file mode 100644
index 0000000..8c70ac1
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link RoutePreviewNavigationTemplate} */
+public class RoutePreviewNavigationTemplateChecker
+ implements TemplateChecker<RoutePreviewNavigationTemplate> {
+ /**
+ * A new template is considered a refresh of the old if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title has not changed, and the number of rows and the string contents
+ * (title, texts, not counting spans) of each row between the previous and new {@link
+ * ItemList}s have not changed.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(
+ RoutePreviewNavigationTemplate newTemplate, RoutePreviewNavigationTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is disallowed.
+ return false;
+ }
+
+ if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+ return false;
+ }
+
+ ItemList oldList = oldTemplate.getItemList();
+ ItemList newList = newTemplate.getItemList();
+ if (oldList != null && newList != null) {
+ return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+ }
+
+ return true;
+ }
+
+ @Override
+ public void checkPermissions(Context context, RoutePreviewNavigationTemplate newTemplate) {
+ CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java
new file mode 100644
index 0000000..3de3493
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.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.apphost.distraction.checkers;
+
+import androidx.car.app.model.signin.InputSignInMethod;
+import androidx.car.app.model.signin.SignInTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link SignInTemplate} */
+public class SignInTemplateChecker implements TemplateChecker<SignInTemplate> {
+ /**
+ * A new template is considered a refresh of a previous one if:
+ *
+ * <ul>
+ * <li>The previous template is in a loading state, or
+ * <li>The template title and instructions have not changed and the input method is the same
+ * type.
+ * </ul>
+ */
+ @Override
+ public boolean isRefresh(SignInTemplate newTemplate, SignInTemplate oldTemplate) {
+ if (oldTemplate.isLoading()) {
+ // Transition from a previous loading state is allowed.
+ return true;
+ } else if (newTemplate.isLoading()) {
+ // Transition to a loading state is not considered a refresh.
+ return false;
+ }
+ boolean equalSignInMethods =
+ Objects.equals(
+ oldTemplate.getSignInMethod().getClass(), newTemplate.getSignInMethod().getClass());
+
+ if (equalSignInMethods && oldTemplate.getSignInMethod() instanceof InputSignInMethod) {
+ InputSignInMethod oldMethod = (InputSignInMethod) oldTemplate.getSignInMethod();
+ InputSignInMethod newMethod = (InputSignInMethod) newTemplate.getSignInMethod();
+
+ equalSignInMethods =
+ oldMethod.getKeyboardType() == newMethod.getKeyboardType()
+ && Objects.equals(oldMethod.getHint(), newMethod.getHint())
+ && oldMethod.getInputType() == newMethod.getInputType();
+ }
+
+ return Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())
+ && Objects.equals(oldTemplate.getInstructions(), newTemplate.getInstructions())
+ && Objects.equals(oldTemplate.getAdditionalText(), newTemplate.getAdditionalText())
+ && equalSignInMethods;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java
new file mode 100644
index 0000000..c3474a6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.model.Template;
+
+/**
+ * Used for checking template of the specified type within the distraction framework to see if they
+ * meet certain criteria (e.g. whether they are refreshes).
+ *
+ * @param <T> the type of template to check
+ */
+public interface TemplateChecker<T extends Template> {
+ /** Returns whether the {@code newTemplate} is a refresh of the {@code oldTemplate}. */
+ boolean isRefresh(T newTemplate, T oldTemplate);
+
+ /**
+ * Checks that the application has the required permissions for this template.
+ *
+ * @throws SecurityException if the application is missing any required permissions
+ */
+ default void checkPermissions(Context context, T newTemplate) {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java
new file mode 100644
index 0000000..ab54fc7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.constraints;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Encapsulates the constraints to apply when rendering a list of {@link Action}s on a template. */
+public class ActionsConstraints {
+ /** Conservative constraints for most template types. */
+ private static final ActionsConstraints ACTIONS_CONSTRAINTS_CONSERVATIVE =
+ ActionsConstraints.builder().setMaxActions(2).build();
+
+ /**
+ * Constraints for template headers, where only the special-purpose back and app-icon standard
+ * actions are allowed.
+ */
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_HEADER =
+ ActionsConstraints.builder().setMaxActions(1).addDisallowedAction(Action.TYPE_CUSTOM).build();
+
+ /** Default constraints that should be applied to most templates (2 actions, 1 can have title). */
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE =
+ ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxCustomTitles(1).build();
+
+ /** Constraints for navigation templates. */
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION =
+ ACTIONS_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setMaxActions(4)
+ .setMaxCustomTitles(1)
+ .addRequiredAction(Action.TYPE_CUSTOM)
+ .build();
+
+ /** Constraints for navigation templates. */
+ public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION_MAP =
+ ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxActions(4).build();
+
+ private final int mMaxActions;
+ private final int mMaxCustomTitles;
+ private final Set<Integer> mRequiredActionTypes;
+ private final Set<Integer> mDisallowedActionTypes;
+
+ /** Returns a builder of {@link ActionsConstraints}. */
+ @VisibleForTesting
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a new builder that contains the same data as this {@link ActionsConstraints} instance,
+ */
+ @VisibleForTesting
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /** Returns the max number of actions allowed. */
+ public int getMaxActions() {
+ return mMaxActions;
+ }
+
+ /** Returns the max number of actions with custom titles allowed. */
+ public int getMaxCustomTitles() {
+ return mMaxCustomTitles;
+ }
+
+ /** Adds the set of required action types. */
+ @NonNull
+ public Set<Integer> getRequiredActionTypes() {
+ return mRequiredActionTypes;
+ }
+
+ /** Adds the set of disallowed action types. */
+ @NonNull
+ public Set<Integer> getDisallowedActionTypes() {
+ return mDisallowedActionTypes;
+ }
+
+ /** A builder of {@link ActionsConstraints}. */
+ @VisibleForTesting
+ public static class Builder {
+ private int mMaxActions = Integer.MAX_VALUE;
+ private int mMaxCustomTitles;
+ private final Set<Integer> mRequiredActionTypes = new HashSet<>();
+ private final Set<Integer> mDisallowedActionTypes = new HashSet<>();
+
+ /** Sets the maximum number of actions allowed. */
+ public Builder setMaxActions(int maxActions) {
+ mMaxActions = maxActions;
+ return this;
+ }
+
+ /** Sets the maximum number of actions with custom titles allowed. */
+ public Builder setMaxCustomTitles(int maxCustomTitles) {
+ mMaxCustomTitles = maxCustomTitles;
+ return this;
+ }
+
+ /** Adds an action type to the set of required types. */
+ public Builder addRequiredAction(int actionType) {
+ mRequiredActionTypes.add(actionType);
+ return this;
+ }
+
+ /** Adds an action type to the set of disallowed types. */
+ public Builder addDisallowedAction(int actionType) {
+ mDisallowedActionTypes.add(actionType);
+ return this;
+ }
+
+ /** TODO(b/174880910): Adding javadoc for AOSP */
+ public ActionsConstraints build() {
+ return new ActionsConstraints(this);
+ }
+
+ private Builder() {}
+
+ private Builder(ActionsConstraints constraints) {
+ mMaxActions = constraints.mMaxActions;
+ mMaxCustomTitles = constraints.mMaxCustomTitles;
+ mRequiredActionTypes.addAll(constraints.mRequiredActionTypes);
+ mDisallowedActionTypes.addAll(constraints.mDisallowedActionTypes);
+ }
+ }
+
+ private ActionsConstraints(Builder builder) {
+ mMaxActions = builder.mMaxActions;
+ mMaxCustomTitles = builder.mMaxCustomTitles;
+ mRequiredActionTypes = new HashSet<>(builder.mRequiredActionTypes);
+
+ if (!builder.mDisallowedActionTypes.isEmpty()) {
+ Set<Integer> disallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+ disallowedActionTypes.retainAll(mRequiredActionTypes);
+ if (!disallowedActionTypes.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Disallowed action types cannot also be in the required set.");
+ }
+ }
+ mDisallowedActionTypes = new HashSet<>(builder.mDisallowedActionTypes);
+
+ if (mRequiredActionTypes.size() > mMaxActions) {
+ throw new IllegalArgumentException("Required action types exceeded max allowed actions.");
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java
new file mode 100644
index 0000000..6809e08
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.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.apphost.distraction.constraints;
+
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarColor.CarColorType;
+import java.util.HashSet;
+
+/** Encapsulates the constraints to apply when rendering a {@link CarColor} on a template. */
+public class CarColorConstraints {
+ public static final CarColorConstraints UNCONSTRAINED =
+ CarColorConstraints.create(
+ new int[] {
+ CarColor.TYPE_CUSTOM,
+ CarColor.TYPE_DEFAULT,
+ CarColor.TYPE_PRIMARY,
+ CarColor.TYPE_SECONDARY,
+ CarColor.TYPE_RED,
+ CarColor.TYPE_GREEN,
+ CarColor.TYPE_BLUE,
+ CarColor.TYPE_YELLOW
+ });
+
+ public static final CarColorConstraints STANDARD_ONLY =
+ CarColorConstraints.create(
+ new int[] {
+ CarColor.TYPE_DEFAULT,
+ CarColor.TYPE_PRIMARY,
+ CarColor.TYPE_SECONDARY,
+ CarColor.TYPE_RED,
+ CarColor.TYPE_GREEN,
+ CarColor.TYPE_BLUE,
+ CarColor.TYPE_YELLOW
+ });
+
+ public static final CarColorConstraints NO_COLOR = CarColorConstraints.create(new int[] {});
+
+ @SuppressWarnings("RestrictTo")
+ @CarColorType
+ private final HashSet<Integer> mAllowedTypes;
+
+ private static CarColorConstraints create(int[] allowedColorTypes) {
+ return new CarColorConstraints(allowedColorTypes);
+ }
+
+ /**
+ * Returns whether the {@link CarColor} meets the constraints' requirement.
+ *
+ * @throws IllegalArgumentException if the color type is not allowed
+ */
+ @SuppressWarnings("RestrictTo")
+ public void validateOrThrow(CarColor carColor) {
+ @CarColorType int type = carColor.getType();
+ if (!mAllowedTypes.contains(type)) {
+ throw new IllegalArgumentException("Car color type is not allowed: " + carColor);
+ }
+ }
+
+ private CarColorConstraints(int[] allowedColorTypes) {
+ mAllowedTypes = new HashSet<>();
+ for (int type : allowedColorTypes) {
+ mAllowedTypes.add(type);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java
new file mode 100644
index 0000000..a0ea196
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.constraints;
+
+import android.content.ContentResolver;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.IconCompat;
+
+/** Encapsulates the constraints to apply when rendering a {@link CarIcon} on a template. */
+public class CarIconConstraints {
+ /** Allow all custom icon types. */
+ public static final CarIconConstraints UNCONSTRAINED =
+ CarIconConstraints.create(
+ new int[] {
+ IconCompat.TYPE_BITMAP, IconCompat.TYPE_RESOURCE, IconCompat.TYPE_URI
+ });
+
+ /** By default, do not allow custom icon types that would load asynchronously in the host. */
+ public static final CarIconConstraints DEFAULT =
+ CarIconConstraints.create(new int[] {IconCompat.TYPE_BITMAP, IconCompat.TYPE_RESOURCE});
+
+ private final int[] mAllowedTypes;
+
+ private static CarIconConstraints create(int[] allowedCustomIconTypes) {
+ return new CarIconConstraints(allowedCustomIconTypes);
+ }
+
+ /**
+ * Returns whether the {@link CarIcon} meets the constraints' requirement.
+ *
+ * @throws IllegalStateException if the custom icon does not have a backing {@link IconCompat}
+ * instance
+ * @throws IllegalArgumentException if the custom icon type is not allowed
+ */
+ public void validateOrThrow(@Nullable CarIcon carIcon) {
+ if (carIcon == null || carIcon.getType() != CarIcon.TYPE_CUSTOM) {
+ return;
+ }
+
+ IconCompat iconCompat = carIcon.getIcon();
+ if (iconCompat == null) {
+ throw new IllegalStateException("Custom icon does not have a backing IconCompat");
+ }
+
+ checkSupportedIcon(iconCompat);
+ }
+
+ /**
+ * Checks whether the given icon is supported.
+ *
+ * @throws IllegalArgumentException if the given icon type is unsupported
+ */
+ public IconCompat checkSupportedIcon(IconCompat iconCompat) {
+ int type = iconCompat.getType();
+ for (int allowedType : mAllowedTypes) {
+ if (type == allowedType) {
+ if (type == IconCompat.TYPE_URI
+ && !ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(
+ iconCompat.getUri().getScheme())) {
+ throw new IllegalArgumentException("Unsupported URI scheme for: " + iconCompat);
+ }
+ return iconCompat;
+ }
+ }
+ throw new IllegalArgumentException("Custom icon type is not allowed: " + type);
+ }
+
+ private CarIconConstraints(int[] allowedCustomIconTypes) {
+ mAllowedTypes = allowedCustomIconTypes;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java
new file mode 100644
index 0000000..777a1af
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.constraints;
+
+/** Used to provide different limit values for the car app. */
+public interface ConstraintsProvider {
+ /** Provides the max length this car app can use for a content type. */
+ default int getContentLimit(int contentType) {
+ return 0;
+ }
+
+ /** Provides the max size for the template stack for this car app. */
+ default int getTemplateStackMaxSize() {
+ return 0;
+ }
+
+ /** Provides the max length this car app can use for a text view */
+ default int getStringCharacterLimit() {
+ return Integer.MAX_VALUE;
+ }
+
+ /** Returns true if keyboard is restricted for this car app */
+ default boolean isKeyboardRestricted() {
+ return false;
+ }
+
+ /** Returns true if config is restricted for this car app */
+ default boolean isConfigRestricted() {
+ return false;
+ }
+
+ /** Returns true if filtering is restricted for this car app */
+ default boolean isFilteringRestricted() {
+ return false;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java
new file mode 100644
index 0000000..5d68adc
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.distraction.constraints;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link androidx.car.app.model.Row} in
+ * different contexts.
+ */
+public class RowConstraints {
+ public static final RowConstraints UNCONSTRAINED = RowConstraints.builder().build();
+
+ /** Conservative constraints for a row. */
+ public static final RowConstraints ROW_CONSTRAINTS_CONSERVATIVE =
+ RowConstraints.builder()
+ .setMaxActionsExclusive(0)
+ .setImageAllowed(false)
+ .setMaxTextLinesPerRow(1)
+ .setOnClickListenerAllowed(true)
+ .setToggleAllowed(false)
+ .build();
+
+ /** The constraints for a full-width row in a pane. */
+ public static final RowConstraints ROW_CONSTRAINTS_PANE =
+ RowConstraints.builder()
+ .setMaxActionsExclusive(2)
+ .setImageAllowed(true)
+ .setMaxTextLinesPerRow(2)
+ .setToggleAllowed(false)
+ .setOnClickListenerAllowed(false)
+ .build();
+
+ /** The constraints for a simple row (2 rows of text and 1 image */
+ public static final RowConstraints ROW_CONSTRAINTS_SIMPLE =
+ RowConstraints.builder()
+ .setMaxActionsExclusive(0)
+ .setImageAllowed(true)
+ .setMaxTextLinesPerRow(2)
+ .setToggleAllowed(false)
+ .setOnClickListenerAllowed(true)
+ .build();
+
+ /** The constraints for a full-width row in a list (simple + toggle support). */
+ public static final RowConstraints ROW_CONSTRAINTS_FULL_LIST =
+ ROW_CONSTRAINTS_SIMPLE.newBuilder().setToggleAllowed(true).build();
+
+ private final int mMaxTextLinesPerRow;
+ private final int mMaxActionsExclusive;
+ private final boolean mIsImageAllowed;
+ private final boolean mIsToggleAllowed;
+ private final boolean mIsOnClickListenerAllowed;
+ private final CarIconConstraints mCarIconConstraints;
+
+ /** Returns a builder of {@link RowConstraints}. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Returns a builder of {@link RowConstraints} set up with the information from this instance. */
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /** Returns whether the row can have a click listener associated with it. */
+ public boolean isOnClickListenerAllowed() {
+ return mIsOnClickListenerAllowed;
+ }
+
+ /** Returns the maximum number lines of text, excluding the title, to render in the row. */
+ public int getMaxTextLinesPerRow() {
+ return mMaxTextLinesPerRow;
+ }
+
+ /** Returns the maximum number actions to allowed in a row that consists only of actions. */
+ public int getMaxActionsExclusive() {
+ return mMaxActionsExclusive;
+ }
+
+ /** Returns whether a toggle can be added to the row. */
+ public boolean isToggleAllowed() {
+ return mIsToggleAllowed;
+ }
+
+ /** Returns whether an image can be added to the row. */
+ public boolean isImageAllowed() {
+ return mIsImageAllowed;
+ }
+
+ /** Returns the {@link CarIconConstraints} enforced for the row images. */
+ public CarIconConstraints getCarIconConstraints() {
+ return mCarIconConstraints;
+ }
+
+ private RowConstraints(Builder builder) {
+ mIsOnClickListenerAllowed = builder.mIsOnClickListenerAllowed;
+ mMaxTextLinesPerRow = builder.mMaxTextLines;
+ mMaxActionsExclusive = builder.mMaxActionsExclusive;
+ mIsToggleAllowed = builder.mIsToggleAllowed;
+ mIsImageAllowed = builder.mIsImageAllowed;
+ mCarIconConstraints = builder.mCarIconConstraints;
+ }
+
+ /** A builder of {@link RowConstraints}. */
+ public static class Builder {
+ private boolean mIsOnClickListenerAllowed = true;
+ private boolean mIsToggleAllowed = true;
+ private int mMaxTextLines = Integer.MAX_VALUE;
+ private int mMaxActionsExclusive = Integer.MAX_VALUE;
+ private boolean mIsImageAllowed = true;
+ private CarIconConstraints mCarIconConstraints = CarIconConstraints.UNCONSTRAINED;
+
+ /** Sets whether a click listener is allowed on the row. */
+ public Builder setOnClickListenerAllowed(boolean isOnClickListenerAllowed) {
+ mIsOnClickListenerAllowed = isOnClickListenerAllowed;
+ return this;
+ }
+
+ /** Sets the maximum number of text lines in a row. */
+ public Builder setMaxTextLinesPerRow(int maxTextLinesPerRow) {
+ mMaxTextLines = maxTextLinesPerRow;
+ return this;
+ }
+
+ /** Sets the maximum number actions to allowed in a row that consists only of actions. */
+ public Builder setMaxActionsExclusive(int maxActionsExclusive) {
+ mMaxActionsExclusive = maxActionsExclusive;
+ return this;
+ }
+
+ /** Sets whether an image can be added to the row. */
+ public Builder setImageAllowed(boolean imageAllowed) {
+ mIsImageAllowed = imageAllowed;
+ return this;
+ }
+
+ /** Sets whether a toggle can be added to the row. */
+ public Builder setToggleAllowed(boolean toggleAllowed) {
+ mIsToggleAllowed = toggleAllowed;
+ return this;
+ }
+
+ /** Sets the {@link CarIconConstraints} enforced for the row images. */
+ public Builder setCarIconConstraints(CarIconConstraints carIconConstraints) {
+ mCarIconConstraints = carIconConstraints;
+ return this;
+ }
+
+ /** Constructs a {@link RowConstraints} object from this builder. */
+ public RowConstraints build() {
+ return new RowConstraints(this);
+ }
+
+ private Builder() {}
+
+ private Builder(RowConstraints constraints) {
+ mIsOnClickListenerAllowed = constraints.mIsOnClickListenerAllowed;
+ mMaxTextLines = constraints.mMaxTextLinesPerRow;
+ mMaxActionsExclusive = constraints.mMaxActionsExclusive;
+ mIsToggleAllowed = constraints.mIsToggleAllowed;
+ mIsImageAllowed = constraints.mIsImageAllowed;
+ mCarIconConstraints = constraints.mCarIconConstraints;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java
new file mode 100644
index 0000000..0bb9c7d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.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.apphost.distraction.constraints;
+
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_LIST;
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_PANE;
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_FULL_LIST;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_PANE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_SIMPLE;
+
+/** Encapsulates the constraints to apply when rendering a row list under different contexts. */
+public class RowListConstraints {
+ /** Conservative constraints for all types lists. */
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE =
+ RowListConstraints.builder()
+ .setListContentType(CONTENT_LIMIT_TYPE_LIST)
+ .setMaxActions(0)
+ .setRowConstraints(ROW_CONSTRAINTS_CONSERVATIVE)
+ .setAllowSelectableLists(false)
+ .build();
+
+ /** Default constraints for heterogeneous pane of items, full width. */
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_PANE =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setMaxActions(2)
+ .setListContentType(CONTENT_LIMIT_TYPE_PANE)
+ .setRowConstraints(ROW_CONSTRAINTS_PANE)
+ .setAllowSelectableLists(false)
+ .build();
+
+ /** Default constraints for uniform lists of items, no toggles. */
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_SIMPLE =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+ .build();
+
+ /** Default constraints for the route preview card. */
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setListContentType(CONTENT_LIMIT_TYPE_ROUTE_LIST)
+ .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+ .setAllowSelectableLists(true)
+ .build();
+
+ /** Default constraints for uniform lists of items, full width (simple + toggle support). */
+ public static final RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST =
+ ROW_LIST_CONSTRAINTS_CONSERVATIVE
+ .newBuilder()
+ .setRowConstraints(ROW_CONSTRAINTS_FULL_LIST)
+ .setAllowSelectableLists(true)
+ .build();
+
+ private final int mListContentType;
+ private final int mMaxActions;
+ private final RowConstraints mRowConstraints;
+ private final boolean mAllowSelectableLists;
+
+ /** A builder of {@link RowListConstraints}. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns a builder of {@link RowListConstraints} set up with the information from this instance.
+ */
+ public Builder newBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Returns the list content type for this constraint.
+ *
+ * <p>This should be one of the content types as defined in {@link
+ * androidx.car.app.constraints.ConstraintManager}.
+ */
+ public int getListContentType() {
+ return mListContentType;
+ }
+
+ /** Returns the maximum number of actions allowed to be added alongside the list. */
+ public int getMaxActions() {
+ return mMaxActions;
+ }
+
+ /** Returns the constraints to apply on individual rows. */
+ public RowConstraints getRowConstraints() {
+ return mRowConstraints;
+ }
+
+ /** Returns whether radio lists are allowed. */
+ public boolean getAllowSelectableLists() {
+ return mAllowSelectableLists;
+ }
+
+ private RowListConstraints(Builder builder) {
+ mMaxActions = builder.mMaxActions;
+ mRowConstraints = builder.mRowConstraints;
+ mAllowSelectableLists = builder.mAllowSelectableLists;
+ mListContentType = builder.mListContentType;
+ }
+
+ /** A builder of {@link RowListConstraints}. */
+ public static class Builder {
+ private int mListContentType;
+ private int mMaxActions;
+ private RowConstraints mRowConstraints = RowConstraints.UNCONSTRAINED;
+ private boolean mAllowSelectableLists;
+
+ /**
+ * Sets the content type for this constraint.
+ *
+ * <p>This should be one of the content types as defined in {@link
+ * androidx.car.app.constraints.ConstraintManager}.
+ */
+ public Builder setListContentType(int contentType) {
+ mListContentType = contentType;
+ return this;
+ }
+
+ /** Sets the maximum number of actions allowed to be added alongside the list. */
+ public Builder setMaxActions(int maxActions) {
+ mMaxActions = maxActions;
+ return this;
+ }
+
+ /** Sets the constraints to apply on individual rows. */
+ public Builder setRowConstraints(RowConstraints rowConstraints) {
+ mRowConstraints = rowConstraints;
+ return this;
+ }
+
+ /** Sets whether radio lists are allowed. */
+ public Builder setAllowSelectableLists(boolean allowSelectableLists) {
+ mAllowSelectableLists = allowSelectableLists;
+ return this;
+ }
+
+ /** Constructs a {@link RowListConstraints} from this builder. */
+ public RowListConstraints build() {
+ return new RowListConstraints(this);
+ }
+
+ private Builder() {}
+
+ private Builder(RowListConstraints constraints) {
+ mMaxActions = constraints.mMaxActions;
+ mRowConstraints = constraints.mRowConstraints;
+ mAllowSelectableLists = constraints.mAllowSelectableLists;
+ mListContentType = constraints.mListContentType;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java
new file mode 100644
index 0000000..db843a9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java
@@ -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.apphost.input;
+
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/** Views that implement this interface are editable by the IME system. */
+public interface CarEditable {
+ /** Notifies that the input connection has been created. */
+ InputConnection onCreateInputConnection(EditorInfo outAttrs);
+
+ /** Sets a listener for events related to input on this car editable. */
+ void setCarEditableListener(CarEditableListener listener);
+
+ /** Sets whether input is enabled. */
+ void setInputEnabled(boolean enabled);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java
new file mode 100644
index 0000000..2577070
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.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.apphost.input;
+
+/**
+ * Callbacks from the {@link CarEditable} to the IME. These methods should be called on the main
+ * thread.
+ */
+public interface CarEditableListener {
+ /**
+ * Indicates that the selection has changed on the current {@link CarEditable}. Note that
+ * selection changes include cursor movements.
+ *
+ * @param oldSelStart the old selection starting index
+ * @param oldSelEnd the old selection ending index
+ * @param newSelStart the new selection starting index
+ * @param newSelEnd the new selection ending index
+ */
+ void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java
new file mode 100644
index 0000000..896225d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.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.apphost.input;
+
+/** Input configurations of the head unit. */
+public interface InputConfig {
+ /** Returns {@code true} if user can use touchpad to navigate UI, {@code false} otherwise. */
+ boolean hasTouchpadForUiNavigation();
+
+ /** Returns {@code true} if touch input is available, {@code false} otherwise. */
+ boolean hasTouch();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java
new file mode 100644
index 0000000..6d8d13a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.input;
+
+/**
+ * Manages use of the in-car IME. All methods should only be called on the main thread.
+ * TODO(b/174880910): Use @MainThread here.
+ */
+public interface InputManager {
+ /**
+ * Starts input on the requested {@link CarEditable}, showing the IME. If IME input is already
+ * occurring for another view, this call stops input on the previous view and starts input on the
+ * new view.
+ *
+ * <p>This method must only be called from the UI thread. This method should not be called from a
+ * stopped activity.
+ */
+ void startInput(CarEditable view);
+
+ /**
+ * Stops input, hiding the IME. This method fails silently if the calling application didn't
+ * request input and isn't the active IME.
+ *
+ * <p>This function must only be called from the UI thread.
+ */
+ void stopInput();
+
+ /**
+ * Returns {@code true} while the {@link InputManager} is valid. The {@link InputManager} is valid
+ * as long as the activity from which it was obtained has been created and not destroyed.
+ */
+ boolean isValid();
+
+ /**
+ * Returns whether this {@link InputManager} is valid and the IME is active on the given {@link
+ * CarEditable}.
+ */
+ boolean isInputActive();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java
new file mode 100644
index 0000000..8432d92
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.internal;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_UNBOUND;
+
+import android.content.ComponentName;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.ErrorHandler;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.CarAppApiErrorType;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+/** Implementation of an {@link ANRHandler}. */
+public class ANRHandlerImpl implements ANRHandler {
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), new HandlerCallback());
+ private final ComponentName mAppName;
+ private final TelemetryHandler mTelemetryhandler;
+ private final ErrorHandler mErrorHandler;
+
+ /** Creates an {@link ANRHandler} */
+ public static ANRHandler create(
+ ComponentName appName,
+ ErrorHandler errorHandler,
+ TelemetryHandler telemetryHandler,
+ EventManager eventManager) {
+ return new ANRHandlerImpl(appName, errorHandler, telemetryHandler, eventManager);
+ }
+
+ /**
+ * Performs the call and checks for application not responding.
+ *
+ * <p>The ANR check will happen in {@link #ANR_TIMEOUT_MS} milliseconds after calling {@link
+ * ANRCheckingCall#call}.
+ */
+ @Override
+ public void callWithANRCheck(CarAppApi carAppApi, ANRCheckingCall call) {
+ enqueueANRCheck(carAppApi);
+ call.call(
+ new ANRToken() {
+ @Override
+ public void dismiss() {
+ mHandler.removeMessages(carAppApi.ordinal());
+ }
+
+ @Override
+ public CarAppApi getCarAppApi() {
+ return carAppApi;
+ }
+ });
+ }
+
+ private void enqueueANRCheck(CarAppApi carAppApi) {
+ mHandler.removeMessages(carAppApi.ordinal());
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(carAppApi.ordinal()), ANR_TIMEOUT_MS);
+ }
+
+ private void onWaitClicked(CarAppApi carAppApi) {
+ enqueueANRCheck(carAppApi);
+ mErrorHandler.showError(
+ CarAppError.builder(mAppName).setType(CarAppError.Type.ANR_WAITING).build());
+ }
+
+ private void removeAllANRChecks() {
+ for (CarAppApi api : CarAppApi.values()) {
+ mHandler.removeMessages(api.ordinal());
+ }
+ }
+
+ @SuppressWarnings("nullness")
+ private ANRHandlerImpl(
+ ComponentName appName,
+ ErrorHandler errorHandler,
+ TelemetryHandler telemetryHandler,
+ EventManager eventManager) {
+ mAppName = appName;
+ mErrorHandler = errorHandler;
+ mTelemetryhandler = telemetryHandler;
+
+ // Remove any outstanding ANR check whenever the app becomes unbound or crashes.
+ eventManager.subscribeEvent(this, APP_UNBOUND, this::removeAllANRChecks);
+ eventManager.subscribeEvent(this, APP_DISCONNECTED, this::removeAllANRChecks);
+ }
+
+ /** A {@link Handler.Callback} used to implement unbinding. */
+ private class HandlerCallback implements Handler.Callback {
+ @Override
+ public boolean handleMessage(Message msg) {
+ final CarAppApi carAppApi = CarAppApi.values()[msg.what];
+ if (carAppApi == CarAppApi.UNKNOWN_API) {
+ L.w(LogTags.APP_HOST, "Unexpected message for handler %s", msg);
+ return false;
+ } else {
+ // Show an ANR screen allowing the user to wait.
+ // If the user wants to wait, we will show a waiting screen that still allows EXIT.
+ mTelemetryhandler.logCarAppApiFailureTelemetry(mAppName, carAppApi, CarAppApiErrorType.ANR);
+
+ mErrorHandler.showError(
+ CarAppError.builder(mAppName)
+ .setType(CarAppError.Type.ANR_TIMEOUT)
+ .setDebugMessage("ANR API: " + carAppApi.name())
+ .setExtraAction(() -> onWaitClicked(carAppApi))
+ .build());
+
+ return true;
+ }
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml
new file mode 100644
index 0000000..69189b9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest package="com.android.car.libraries.apphost.internal"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-sdk android:minSdkVersion="23"/>
+</manifest>
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java
new file mode 100644
index 0000000..ba7356c
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.internal;
+
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.InputCallbackDelegate;
+import androidx.car.app.model.OnCheckedChangeDelegate;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnContentRefreshDelegate;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.SearchCallbackDelegate;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+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.CarAppError;
+import com.android.car.libraries.apphost.common.ErrorHandler;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.OneWayIPC;
+import com.android.car.libraries.apphost.internal.BlockingOneWayIPC.BlockingResponse;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Class to set up safe remote callbacks to apps.
+ *
+ * <p>App interfaces to client are {@code oneway} so the calling thread does not block waiting for a
+ * response.
+ */
+public class AppDispatcherImpl implements AppDispatcher {
+ /** A request to send over the wire to the app that does not wait for a ANR check. */
+ private interface OneWayIPCNoANRCheck {
+ void send() throws RemoteException;
+ }
+
+ private final ComponentName mAppName;
+ private final ErrorHandler mErrorHandler;
+ private final ANRHandler mANRHandler;
+ private final TelemetryHandler mTelemetryHandler;
+ private final AppBindingStateProvider mAppBindingStateProvider;
+
+ /** Creates an {@link AppDispatcher} instance for an app. */
+ public static AppDispatcher create(
+ ComponentName appName,
+ ErrorHandler errorHandler,
+ ANRHandler anrHandler,
+ TelemetryHandler telemetryHandler,
+ AppBindingStateProvider appBindingStateProvider) {
+ return new AppDispatcherImpl(
+ appName, errorHandler, anrHandler, telemetryHandler, appBindingStateProvider);
+ }
+
+ @Override
+ public void dispatchSurfaceAvailable(
+ ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer) {
+ dispatch(
+ anrToken ->
+ surfaceListener.onSurfaceAvailable(
+ Bundleable.create(surfaceContainer),
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_SURFACE_AVAILABLE);
+ }
+
+ @Override
+ public void dispatchSurfaceDestroyed(
+ ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer) {
+ // onSurfaceDestroyed is called blocking since the OS expects that whenever we return
+ // the call we are done using the Surface.
+ BlockingResponse<Void> blockingResponse = new BlockingResponse<>();
+ OneWayIPC ipc =
+ anrToken ->
+ surfaceListener.onSurfaceDestroyed(
+ Bundleable.create(surfaceContainer),
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ blockingResponse.setResponse(null);
+ super.onSuccess(response);
+ }
+
+ @Override
+ public void onFailure(Bundleable failureResponse) {
+ blockingResponse.setResponse(null);
+ super.onFailure(failureResponse);
+ }
+ });
+
+ dispatch(new BlockingOneWayIPC<>(ipc, blockingResponse), CarAppApi.ON_SURFACE_DESTROYED);
+ }
+
+ @Override
+ public void dispatchVisibleAreaChanged(ISurfaceCallback surfaceListener, Rect visibleArea) {
+ dispatch(
+ anrToken ->
+ surfaceListener.onVisibleAreaChanged(
+ visibleArea,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_VISIBLE_AREA_CHANGED);
+ }
+
+ @Override
+ public void dispatchStableAreaChanged(ISurfaceCallback surfaceListener, Rect stableArea) {
+ dispatch(
+ anrToken ->
+ surfaceListener.onStableAreaChanged(
+ stableArea,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_STABLE_AREA_CHANGED);
+ }
+
+ @Override
+ public void dispatchOnSurfaceScroll(
+ ISurfaceCallback surfaceListener, float distanceX, float distanceY) {
+ dispatchNoANRCheck(() -> surfaceListener.onScroll(distanceX, distanceY), "onSurfaceScroll");
+ }
+
+ @Override
+ public void dispatchOnSurfaceFling(
+ ISurfaceCallback surfaceListener, float velocityX, float velocityY) {
+ dispatchNoANRCheck(() -> surfaceListener.onFling(velocityX, velocityY), "onSurfaceFling");
+ }
+
+ @Override
+ public void dispatchOnSurfaceScale(
+ ISurfaceCallback surfaceListener, float focusX, float focusY, float scaleFactor) {
+ dispatchNoANRCheck(
+ () -> surfaceListener.onScale(focusX, focusY, scaleFactor), "onSurfaceScale");
+ }
+
+ @Override
+ public void dispatchSearchTextChanged(
+ SearchCallbackDelegate searchCallbackDelegate, String searchText) {
+ dispatch(
+ anrToken ->
+ searchCallbackDelegate.sendSearchTextChanged(
+ searchText,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_SEARCH_TEXT_CHANGED);
+ }
+
+ @Override
+ public void dispatchInputTextChanged(
+ InputCallbackDelegate inputCallbackDelegate, String inputText) {
+ dispatch(
+ anrToken ->
+ inputCallbackDelegate.sendInputTextChanged(
+ inputText,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_INPUT_TEXT_CHANGED);
+ }
+
+ @Override
+ public void dispatchInputSubmitted(
+ InputCallbackDelegate inputCallbackDelegate, String inputText) {
+ dispatch(
+ anrToken ->
+ inputCallbackDelegate.sendInputSubmitted(
+ inputText,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_INPUT_SUBMITTED);
+ }
+
+ @Override
+ public void dispatchSearchSubmitted(
+ SearchCallbackDelegate searchCallbackDelegate, String searchText) {
+ dispatch(
+ anrToken ->
+ searchCallbackDelegate.sendSearchSubmitted(
+ searchText,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_SEARCH_SUBMITTED);
+ }
+
+ @Override
+ public void dispatchItemVisibilityChanged(
+ OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate,
+ int startIndexInclusive,
+ int endIndexExclusive) {
+ dispatch(
+ anrToken ->
+ onItemVisibilityChangedDelegate.sendItemVisibilityChanged(
+ startIndexInclusive,
+ endIndexExclusive,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_ITEM_VISIBILITY_CHANGED);
+ }
+
+ @Override
+ public void dispatchSelected(OnSelectedDelegate onSelectedDelegate, int index) {
+ dispatch(
+ anrToken ->
+ onSelectedDelegate.sendSelected(
+ index,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_SELECTED);
+ }
+
+ @Override
+ public void dispatchCheckedChanged(
+ OnCheckedChangeDelegate onCheckedChangeDelegate, boolean isChecked) {
+ dispatch(
+ anrToken ->
+ onCheckedChangeDelegate.sendCheckedChange(
+ isChecked,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_CHECKED_CHANGED);
+ }
+
+ @Override
+ public void dispatchPanModeChanged(PanModeDelegate panModeDelegate, boolean isChecked) {
+ dispatch(
+ anrToken ->
+ panModeDelegate.sendPanModeChanged(
+ isChecked,
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_PAN_MODE_CHANGED);
+ }
+
+ @Override
+ public void dispatchClick(OnClickDelegate onClickDelegate) {
+ dispatch(
+ anrToken ->
+ onClickDelegate.sendClick(
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_CLICK);
+ }
+
+ @Override
+ public void dispatchContentRefreshRequest(OnContentRefreshDelegate onContentRefreshDelegate) {
+ dispatch(
+ anrToken ->
+ onContentRefreshDelegate.sendContentRefreshRequested(
+ new OnDoneCallbackStub(
+ mErrorHandler,
+ mAppName,
+ anrToken,
+ mTelemetryHandler,
+ mAppBindingStateProvider)),
+ CarAppApi.ON_CLICK);
+ }
+
+ /** Dispatches the given IPC call without checking for an ANR. */
+ private void dispatchNoANRCheck(OneWayIPCNoANRCheck ipc, String callName) {
+ try {
+ ipc.send();
+ } catch (RemoteException e) {
+ mErrorHandler.showError(
+ CarAppError.builder(mAppName)
+ .setCause(e)
+ .setDebugMessage("Remote call " + callName + " failed.")
+ .build());
+ }
+ }
+
+ @Override
+ public void dispatch(OneWayIPC ipc, CarAppApi carAppApi) {
+ dispatch(ipc, mErrorHandler::showError, carAppApi);
+ }
+
+ @Override
+ public void dispatch(OneWayIPC ipc, ExceptionHandler exceptionHandler, CarAppApi carAppApi) {
+ L.d(LogTags.APP_HOST, "Dispatching call %s", carAppApi.name());
+
+ mANRHandler.callWithANRCheck(
+ carAppApi,
+ anrToken -> {
+ try {
+ ipc.send(anrToken);
+ } catch (RemoteException | BundlerException | RuntimeException e) {
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ mAppName, carAppApi, getErrorType(new FailureResponse(e)));
+
+ exceptionHandler.handle(
+ CarAppError.builder(mAppName)
+ .setCause(e)
+ .setDebugMessage("Remote call " + carAppApi.name() + " failed.")
+ .build());
+ }
+ });
+ }
+
+ private AppDispatcherImpl(
+ ComponentName appName,
+ ErrorHandler errorHandler,
+ ANRHandler anrHandler,
+ TelemetryHandler telemetryHandler,
+ AppBindingStateProvider appBindingStateProvider) {
+ mAppName = appName;
+ mErrorHandler = errorHandler;
+ mANRHandler = anrHandler;
+ mTelemetryHandler = telemetryHandler;
+ mAppBindingStateProvider = appBindingStateProvider;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java
new file mode 100644
index 0000000..5d801e8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.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.apphost.internal;
+
+import android.os.RemoteException;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.OneWayIPC;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link OneWayIPC} that will block waiting for a response from the client before returning.
+ *
+ * <p>Once the client responds set the value received via calling {@link
+ * BlockingResponse#setResponse} on the {@link BlockingResponse} that was supplied.
+ *
+ * <p>When {@link #send} is called, the thread will be blocked up to {@link
+ * BlockingResponse#BLOCKING_MAX_MILLIS} milliseconds, or until the client responds, whichever comes
+ * first.
+ *
+ * <p>If the client does not respond until the timeout, the {@link ANRHandler} will display an ANR
+ * to the user.
+ *
+ * @param <T> the type of the response for the IPC
+ */
+public class BlockingOneWayIPC<T> implements OneWayIPC {
+ /**
+ * A class to block waiting on a response from the client.
+ *
+ * @param <T> the type of the response for the IPC
+ */
+ public static class BlockingResponse<T> {
+ // Set to 4 seconds instead of 5 seconds so the system does not ANR.
+ private static final long BLOCKING_MAX_MILLIS = 4000;
+ private static long sBlockingMaxMillis = BLOCKING_MAX_MILLIS;
+
+ @GuardedBy("this")
+ private boolean mComplete;
+
+ @GuardedBy("this")
+ @Nullable
+ private T mResponse;
+
+ /** Sets the response from the app, releasing any blocking threads. */
+ public void setResponse(@Nullable T response) {
+ synchronized (this) {
+ mResponse = response;
+ mComplete = true;
+ notifyAll();
+ }
+ }
+
+ /** Sets the maximum time to block the IPC for before considering it an ANR, in milliseconds. */
+ @VisibleForTesting
+ public static void setBlockingMaxMillis(long valueForTesting) {
+ sBlockingMaxMillis = valueForTesting;
+ }
+
+ /**
+ * Returns the value provided by calling {@link #setResponse}.
+ *
+ * <p>This method will block waiting for the client to call back before returning.
+ *
+ * <p>The max time method will wait is {@link #BLOCKING_MAX_MILLIS}.
+ */
+ @Nullable
+ private T getBlocking() throws TimeoutException, InterruptedException {
+ synchronized (this) {
+ long startedTimeMillis = System.currentTimeMillis();
+ long waitMillis = sBlockingMaxMillis;
+
+ while (!mComplete && waitMillis > 0) {
+ wait(waitMillis);
+
+ long elapsedMillis = System.currentTimeMillis() - startedTimeMillis;
+ waitMillis = sBlockingMaxMillis - elapsedMillis;
+ }
+ if (!mComplete) {
+ throw new TimeoutException("Response was not set while blocked");
+ }
+
+ return mResponse;
+ }
+ }
+ }
+
+ private final OneWayIPC mOneWayIPC;
+ private final BlockingResponse<T> mBlockingResponse;
+ @Nullable private T mResponse;
+
+ /** Constructs an instance of a {@link BlockingOneWayIPC}. */
+ public BlockingOneWayIPC(OneWayIPC oneWayIPC, BlockingResponse<T> blockingResponse) {
+ mOneWayIPC = oneWayIPC;
+ mBlockingResponse = blockingResponse;
+ }
+
+ @Override
+ public void send(ANRToken anrToken) throws BundlerException, RemoteException {
+ mOneWayIPC.send(anrToken);
+ try {
+ mResponse = mBlockingResponse.getBlocking();
+ anrToken.dismiss();
+ } catch (InterruptedException e) {
+ anrToken.dismiss();
+ throw new IllegalStateException("Exception while waiting for client response.", e);
+ } catch (TimeoutException e) {
+ L.w(LogTags.APP_HOST, e, "Timeout blocking for a client response");
+ // Let the ANR handler handle the ANR by not dismissing the token.
+ }
+ }
+
+ /**
+ * Returns the {@code Response} returned from the {@link Future} provided, or {@code null} if the
+ * app did not respond.
+ *
+ * <p>{@link #send} should be called before calling method, otherwise the result will be {@code
+ * null}.
+ */
+ @Nullable
+ public T getResponse() {
+ return mResponse;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java
new file mode 100644
index 0000000..e142e78
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.internal;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.AppInfo;
+import androidx.car.app.CarContext;
+import androidx.car.app.HandshakeInfo;
+import androidx.car.app.IAppManager;
+import androidx.car.app.ICarApp;
+import androidx.car.app.ICarHost;
+import androidx.car.app.navigation.INavigationManager;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.core.util.Consumer;
+import androidx.lifecycle.Lifecycle.Event;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.AppDispatcher;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.CarHostConfig;
+import com.android.car.libraries.apphost.common.IncompatibleApiException;
+import com.android.car.libraries.apphost.common.IntentUtils;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+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.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import java.io.PrintWriter;
+import java.security.InvalidParameterException;
+
+/** Manages a binding to the {@link ICarApp} and handles the communication with it. */
+public class CarAppBinding implements StatusReporter {
+
+ private static final int MSG_UNBIND = 1;
+ private static final int MSG_REBIND = 2;
+
+ private enum BindingState {
+ UNBOUND,
+ BINDING,
+ BOUND
+ }
+
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper(), new HandlerCallback());
+ private final ComponentName mAppName;
+ private final ICarHost mCarHost;
+ private final CarAppBindingCallback mCarAppBindingCallback;
+ private final ServiceConnection mServiceConnection = new ServiceConnectionImpl();
+ private final TelemetryHandler mTelemetryHandler;
+
+ // The following fields can be updated by different threads, therefore they are volatile so that
+ // readers use the latest value.
+
+ private volatile TemplateContext mTemplateContext;
+
+ @Nullable private volatile ICarApp mCarApp;
+ @Nullable private volatile IInterface mAppManager;
+ @Nullable private volatile IInterface mNavigationManager;
+
+ @Nullable private volatile Intent mOriginalIntent;
+ @Nullable private volatile ANRToken mANRToken;
+
+ @Nullable private AppInfo mAppInfo;
+
+ /**
+ * The current state of the binding with the client app service. Use {@link
+ * #setBindingState(BindingState)} to update it.
+ */
+ private volatile BindingState mBindingState = BindingState.UNBOUND;
+
+ /**
+ * Creates a {@link CarAppBinding} for binding to and communicating with {@code appName}.
+ *
+ * @param templateContext the context to retrieve template helpers from
+ * @param carHost the host to send to the app when it is bound
+ * @param carAppBindingCallback callback to perform once the app is bound
+ */
+ public static CarAppBinding create(
+ TemplateContext templateContext,
+ ICarHost carHost,
+ CarAppBindingCallback carAppBindingCallback) {
+ return new CarAppBinding(templateContext, carHost, carAppBindingCallback);
+ }
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {
+ pw.printf("- state: %s\n", mBindingState.name());
+ mTemplateContext.getCarHostConfig().reportStatus(pw, piiHandling);
+ }
+
+ /** Returns the name of the app this binding is managing. */
+ @AnyThread
+ public ComponentName getAppName() {
+ return mAppName;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + mAppName.flattenToShortString() + ", state: " + mBindingState + "]";
+ }
+
+ /** Sets the {@link TemplateContext} instance attached to this binding. */
+ @AnyThread
+ public void setTemplateContext(TemplateContext templateContext) {
+ mTemplateContext = templateContext;
+
+ AppInfo appInfo = mAppInfo;
+ if (appInfo != null) {
+ try {
+ mTemplateContext.getCarHostConfig().updateNegotiatedApi(appInfo);
+ } catch (IncompatibleApiException exception) {
+ unbind(CarAppError.builder(mAppName).setCause(exception).build());
+ }
+ }
+ }
+
+ /** Binds to the app, if not bound already. */
+ @AnyThread
+ public void bind(Intent binderIntent) {
+ L.d(LogTags.APP_HOST, "Binding to %s with intent %s", this, binderIntent);
+ mMainHandler.removeMessages(MSG_UNBIND);
+ mMainHandler.removeMessages(MSG_REBIND);
+ final Intent originalIntent = IntentUtils.extractOriginalIntent(binderIntent);
+ mOriginalIntent = originalIntent;
+
+ switch (mBindingState) {
+ case UNBOUND:
+ setBindingState(BindingState.BINDING);
+
+ try {
+ // We bind to the app with host's capabilities, which allows the "while-in-use"
+ // permission capabilities (e.g. location) in the app's process for the duration of
+ // the binding.
+ // See go/watevra-nav-location for more information on the process capabilities.
+ if (mTemplateContext
+ .getApplicationContext()
+ .bindService(
+ binderIntent,
+ mServiceConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
+ mTemplateContext
+ .getAnrHandler()
+ .callWithANRCheck(CarAppApi.BIND, (currentAnrToken) -> mANRToken = currentAnrToken);
+ } else {
+ failedToBind(null);
+ }
+ } catch (SecurityException e) {
+ L.e(LogTags.APP_HOST, e, "Cannot bind to the service.");
+ failedToBind(e);
+ }
+
+ return;
+ case BOUND:
+ dispatch(
+ CarContext.CAR_SERVICE,
+ NamedAppServiceCall.create(
+ CarAppApi.ON_NEW_INTENT,
+ (ICarApp carApp, ANRToken anrToken) ->
+ carApp.onNewIntent(
+ originalIntent,
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(response);
+ ThreadUtils.runOnMain(mCarAppBindingCallback::onNewIntentDispatched);
+ }
+ })));
+ return;
+ case BINDING:
+ L.d(LogTags.APP_HOST, "Already binding to %s", mAppName);
+ }
+ }
+
+ /** Dispatches the lifecycle call for the given {@code event} to the app. */
+ @AnyThread
+ public void dispatchAppLifecycleEvent(Event event) {
+ L.d(
+ LogTags.APP_HOST,
+ "Dispatching lifecycle event: %s, app: %s",
+ event,
+ mAppName.toShortString());
+
+ dispatch(
+ CarContext.CAR_SERVICE,
+ NamedAppServiceCall.create(
+ CarAppApi.DISPATCH_LIFECYCLE,
+ (ICarApp carApp, ANRToken anrToken) -> {
+ switch (event) {
+ case ON_START:
+ carApp.onAppStart(new OnDoneCallbackStub(mTemplateContext, anrToken));
+ return;
+ case ON_RESUME:
+ carApp.onAppResume(new OnDoneCallbackStub(mTemplateContext, anrToken));
+ return;
+ case ON_PAUSE:
+ carApp.onAppPause(new OnDoneCallbackStub(mTemplateContext, anrToken));
+ return;
+ case ON_STOP:
+ mMainHandler.removeMessages(MSG_UNBIND);
+ mMainHandler.removeMessages(MSG_REBIND);
+ mMainHandler.sendMessageDelayed(
+ mMainHandler.obtainMessage(MSG_UNBIND),
+ SECONDS.toMillis(mTemplateContext.getCarHostConfig().getAppUnbindSeconds()));
+ carApp.onAppStop(new OnDoneCallbackStub(mTemplateContext, anrToken));
+ return;
+ default:
+ // fall-through
+ }
+ throw new InvalidParameterException("Received unexpected lifecycle event: " + event);
+ }));
+ }
+
+ /**
+ * Dispatches the {@code call} to the appropriate manager.
+ *
+ * @param managerType one of the CarServiceType as defined in {@link CarContext}
+ * @param call the call to dispatch
+ */
+ @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
+ @AnyThread
+ public <ServiceT extends IInterface> void dispatch(
+ String managerType, NamedAppServiceCall<ServiceT> call) {
+
+ ICarApp carApp = mCarApp;
+
+ if (mBindingState != BindingState.BOUND || carApp == null) {
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(mAppName)
+ .setDebugMessage(
+ "App is not bound when attempting to get service: "
+ + managerType
+ + ", call: "
+ + call)
+ .build());
+ return;
+ }
+
+ AppDispatcher appDispatcher = mTemplateContext.getAppDispatcher();
+
+ switch (managerType) {
+ case CarContext.APP_SERVICE:
+ if (mAppManager == null) {
+ dispatchGetManager(
+ appDispatcher,
+ managerType,
+ carApp,
+ manager -> {
+ mAppManager = (IAppManager) manager;
+ dispatchCall(appDispatcher, call, (ServiceT) mAppManager);
+ });
+ } else {
+ dispatchCall(appDispatcher, call, (ServiceT) mAppManager);
+ }
+ break;
+ case CarContext.NAVIGATION_SERVICE:
+ if (mNavigationManager == null) {
+ dispatchGetManager(
+ appDispatcher,
+ managerType,
+ carApp,
+ manager -> {
+ mNavigationManager = (INavigationManager) manager;
+ dispatchCall(appDispatcher, call, (ServiceT) mNavigationManager);
+ });
+ } else {
+ dispatchCall(appDispatcher, call, (ServiceT) mNavigationManager);
+ }
+ break;
+ case CarContext.CAR_SERVICE:
+ dispatchCall(appDispatcher, call, (ServiceT) carApp);
+ break;
+ default:
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(mAppName)
+ .setDebugMessage("No manager was found for type: " + managerType)
+ .build());
+ break;
+ }
+ }
+
+ /** Returns whether the app is currently bound to. */
+ @AnyThread
+ public boolean isBound() {
+ return mBindingState == BindingState.BOUND;
+ }
+
+ /** Returns whether the binder is in unbound state. */
+ @AnyThread
+ @VisibleForTesting
+ public boolean isUnbound() {
+ return mBindingState == BindingState.UNBOUND;
+ }
+
+ /** Returns the {@link ServiceConnection} instance used by this binding. */
+ @VisibleForTesting
+ public ServiceConnection getServiceConnection() {
+ return mServiceConnection;
+ }
+
+ /**
+ * Unbinds from the app.
+ *
+ * <p>Will not set an error screen.
+ *
+ * <p>If already unbound the call will be a no-op.
+ */
+ @AnyThread
+ public void unbind() {
+ L.d(LogTags.APP_HOST, "Unbinding from %s", this);
+ internalUnbind(null);
+ }
+
+ /**
+ * Unbinds from the app and sets an error screen.
+ *
+ * <p>If already unbound the call will be a no-op.
+ */
+ private void unbind(CarAppError error) {
+ L.d(LogTags.APP_HOST, "Unbinding from %s with error: %s", this, error);
+
+ internalUnbind(error);
+ }
+
+ private void internalUnbind(@Nullable CarAppError errorToShow) {
+ if (mBindingState != BindingState.UNBOUND) {
+ // Run on main thread so that we can unregister from listening for surface changes on
+ // the main thread before an error message is shown which could cause a onSurfaceChanged
+ // callback.
+ ThreadUtils.runOnMain(
+ () -> {
+ mOriginalIntent = null;
+ setBindingState(BindingState.UNBOUND);
+ if (errorToShow != null) {
+ mTemplateContext.getErrorHandler().showError(errorToShow);
+ }
+ resetAppServices();
+ mCarAppBindingCallback.onCarAppUnbound();
+ // Perform tear down logic first, then actually unbind.
+ mTemplateContext.getApplicationContext().unbindService(mServiceConnection);
+ });
+ }
+ }
+
+ private CarAppBinding(
+ TemplateContext templateContext,
+ ICarHost carHost,
+ CarAppBindingCallback carAppBindingCallback) {
+ mTemplateContext = templateContext;
+ mAppName = templateContext.getCarAppPackageInfo().getComponentName();
+ mCarHost = carHost;
+ mCarAppBindingCallback = carAppBindingCallback;
+ mTelemetryHandler = templateContext.getTelemetryHandler();
+ }
+
+ private void resetAppServices() {
+ mCarApp = null;
+ mAppManager = null;
+ mNavigationManager = null;
+ }
+
+ private void setBindingState(BindingState bindingState) {
+ if (mBindingState == bindingState) {
+ return;
+ }
+ BindingState previousState = mBindingState;
+ mBindingState = bindingState;
+ L.d(
+ LogTags.APP_HOST,
+ "Binding state changed from %s to %s for %s",
+ previousState,
+ bindingState,
+ mAppName.flattenToShortString());
+ }
+
+ /**
+ * Retrieves a car service manager from the app
+ *
+ * @param appDispatcher the dispatcher used for making the getManager call
+ * @param managerType one of the CarServiceType as defined in {@link CarContext}
+ * @param carApp the car app to retrieve the manager from
+ * @param callback the callback to trigger on receiving the result from the app
+ */
+ private void dispatchGetManager(
+ AppDispatcher appDispatcher, String managerType, ICarApp carApp, Consumer<Object> callback) {
+ appDispatcher.dispatch(
+ anrToken ->
+ carApp.getManager(
+ managerType,
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(checkNotNull(response));
+
+ try {
+ callback.accept(response.get());
+ } catch (BundlerException e) {
+ mTemplateContext
+ .getErrorHandler()
+ .showError(CarAppError.builder(mAppName).setCause(e).build());
+ return;
+ }
+ }
+ }),
+ CarAppApi.GET_MANAGER);
+ }
+
+ @SuppressWarnings("cast.unsafe") // Cannot check if instanceof ServiceT
+ private static <ServiceT extends IInterface> void dispatchCall(
+ AppDispatcher appDispatcher, NamedAppServiceCall<ServiceT> call, ServiceT serviceT) {
+ appDispatcher.dispatch(anrToken -> call.dispatch(serviceT, anrToken), call.getCarAppApi());
+ }
+
+ private final class ServiceConnectionImpl implements ServiceConnection {
+ private boolean mHasConnectedSinceLastBind;
+
+ @Override
+ public void onServiceConnected(ComponentName appName, IBinder service) {
+ L.d(LogTags.APP_HOST, "App service connected: %s", appName.flattenToShortString());
+ ANRToken token = mANRToken;
+ if (token != null) {
+ token.dismiss();
+ }
+ mHasConnectedSinceLastBind = true;
+
+ resetAppServices();
+ mCarApp = ICarApp.Stub.asInterface(service);
+ dispatchGetAppInfo(mCarApp);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName appName) {
+ L.d(LogTags.APP_HOST, "App service disconnected: %s", appName.flattenToShortString());
+
+ if (mHasConnectedSinceLastBind) {
+ mHasConnectedSinceLastBind = false;
+ setBindingState(BindingState.BINDING);
+ resetAppServices();
+ mTemplateContext.getEventManager().dispatchEvent(APP_DISCONNECTED);
+ } else {
+ unbind(
+ CarAppError.builder(appName)
+ .setDebugMessage("The app has crashed multiple times")
+ .build());
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName appName) {
+ L.d(LogTags.APP_HOST, "App binding died: %s", appName.flattenToShortString());
+
+ mMainHandler.removeMessages(MSG_REBIND);
+
+ setBindingState(BindingState.UNBOUND);
+ resetAppServices();
+ mTemplateContext.getEventManager().dispatchEvent(APP_DISCONNECTED);
+
+ mMainHandler.sendMessageDelayed(mMainHandler.obtainMessage(MSG_REBIND), 500);
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ unbind(CarAppError.builder(mAppName).setDebugMessage("Null binding from app").build());
+ }
+
+ private void dispatchGetAppInfo(ICarApp carApp) {
+ mTemplateContext
+ .getAppDispatcher()
+ .dispatch(
+ anrToken -> sendAppInfoIPC(carApp, anrToken),
+ CarAppBinding.this::unbind,
+ CarAppApi.GET_APP_VERSION);
+ }
+
+ private void sendAppInfoIPC(ICarApp carApp, ANRToken anrToken) throws RemoteException {
+ carApp.getAppInfo(
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(checkNotNull(response));
+ CarHostConfig hostConfig = mTemplateContext.getCarHostConfig();
+ try {
+ AppInfo appInfo = (AppInfo) response.get();
+ mTelemetryHandler.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(UiAction.CLIENT_SDK_VERSION, mAppName)
+ .setCarAppSdkVersion(appInfo.getLibraryDisplayVersion()));
+ dispatchOnHandshakeCompleted(carApp, hostConfig.updateNegotiatedApi(appInfo));
+ mAppInfo = appInfo;
+
+ } catch (BundlerException e) {
+ unbind(CarAppError.builder(mAppName).setCause(e).build());
+ } catch (IncompatibleApiException e) {
+ unbind(
+ CarAppError.builder(mAppName)
+ .setType(CarAppError.Type.INCOMPATIBLE_CLIENT_VERSION)
+ .setCause(e)
+ .build());
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("RestrictTo")
+ private void dispatchOnHandshakeCompleted(ICarApp carApp, int negotiatedApiLevel) {
+ HandshakeInfo handshakeInfo =
+ new HandshakeInfo(mTemplateContext.getPackageName(), negotiatedApiLevel);
+ mTemplateContext
+ .getAppDispatcher()
+ .dispatch(
+ anrToken ->
+ carApp.onHandshakeCompleted(
+ Bundleable.create(handshakeInfo),
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(response);
+ dispatchOnAppCreate(carApp);
+ }
+ }),
+ CarAppBinding.this::unbind,
+ CarAppApi.ON_HANDSHAKE_COMPLETED);
+ }
+
+ private void dispatchOnAppCreate(ICarApp carApp) {
+ mTemplateContext
+ .getAppDispatcher()
+ .dispatch(
+ anrToken ->
+ carApp.onAppCreate(
+ mCarHost,
+ checkNotNull(mOriginalIntent),
+ mTemplateContext.getResources().getConfiguration(),
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(response);
+ setBindingState(BindingState.BOUND);
+ ThreadUtils.runOnMain(mCarAppBindingCallback::onCarAppBound);
+ }
+
+ @Override
+ public void onFailure(Bundleable failureResponse) {
+ super.onFailure(failureResponse);
+ L.d(LogTags.APP_HOST, "OnAppCreate Failure");
+ internalUnbind(null);
+ }
+ }),
+ CarAppBinding.this::unbind,
+ CarAppApi.ON_APP_CREATE);
+ }
+ }
+
+ /** A {@link Handler.Callback} used to implement unbinding. */
+ private class HandlerCallback implements Handler.Callback {
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_UNBIND) {
+ if (mTemplateContext.getCarAppPackageInfo().isNavigationApp()) {
+ L.d(LogTags.APP_HOST, "Not unbinding due to the app being a navigation app");
+ return true;
+ }
+ unbind();
+ return true;
+ } else if (msg.what == MSG_REBIND) {
+ bind(new Intent().setComponent(mAppName));
+ return true;
+ }
+
+ L.w(LogTags.APP_HOST, "Unknown message: %s", msg);
+ return false;
+ }
+ }
+
+ /** Updates the internal state and shows an error. */
+ private void failedToBind(@Nullable Throwable cause) {
+ // Set the state to unbound as the binding was unsuccessful.
+ setBindingState(BindingState.UNBOUND);
+
+ CarAppError.Builder builder =
+ CarAppError.builder(mAppName).setDebugMessage("Failed to bind to " + mAppName);
+
+ if (cause != null) {
+ builder.setCause(cause);
+ }
+
+ mTemplateContext.getErrorHandler().showError(builder.build());
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java
new file mode 100644
index 0000000..e1f642a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java
@@ -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
+ *
+ * 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.apphost.internal;
+
+/** Provides callbacks for binding-related interaction. */
+public interface CarAppBindingCallback {
+ /** Notifies when the app is bound. */
+ void onCarAppBound();
+
+ /** Notifies that bind was called, when already bound, and onNewIntent was dispatched. */
+ void onNewIntentDispatched();
+
+ /** Notifies when the app is no longer bound. */
+ void onCarAppUnbound();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java
new file mode 100644
index 0000000..661cf23
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.internal;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.AppIconLoader;
+import com.android.car.libraries.apphost.common.CarAppColors;
+import com.android.car.libraries.apphost.common.CarAppPackageInfo;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+import java.util.Objects;
+
+/** Provides package information of a 3p car app built using AndroidX Car SDK (go/watevra). */
+public class CarAppPackageInfoImpl implements CarAppPackageInfo {
+ private final Context mContext;
+ private final ComponentName mComponentName;
+ private final boolean mIsNavigationApp;
+ private final AppIconLoader mAppIconLoader;
+ private final HostResourceIds mHostResourceIds;
+
+ private boolean mIsLoaded;
+ @Nullable private CarAppColors mCarAppColors;
+
+ /**
+ * Creates a {@link CarAppPackageInfoImpl} for the application identified by the given {@link
+ * ComponentName}.
+ *
+ * @param context Host context, used to retrieve host resources and configurations
+ * @param componentName Identifier of the car app this instance will provide metadata for
+ * @param isNavigationApp Whether the given car app is a navigation app or not
+ * @param hostResourceIds Host resources, used to retrieve default colors to use in case the app
+ * doesn't provide their own
+ */
+ public static CarAppPackageInfo create(
+ @NonNull Context context,
+ @NonNull ComponentName componentName,
+ boolean isNavigationApp,
+ @NonNull HostResourceIds hostResourceIds,
+ @NonNull AppIconLoader appIconLoader) {
+ return new CarAppPackageInfoImpl(
+ context, componentName, isNavigationApp, hostResourceIds, appIconLoader);
+ }
+
+ @Override
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @NonNull
+ @Override
+ public CarAppColors getAppColors() {
+ ensureLoaded();
+ return Objects.requireNonNull(mCarAppColors);
+ }
+
+ @Override
+ public boolean isNavigationApp() {
+ return mIsNavigationApp;
+ }
+
+ @Override
+ @NonNull
+ public Drawable getRoundAppIcon() {
+ return mAppIconLoader.getRoundAppIcon(mContext, mComponentName);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + mComponentName.flattenToShortString() + ", isNav: " + mIsNavigationApp + "]";
+ }
+
+ @SuppressLint("ResourceType")
+ private void ensureLoaded() {
+ if (mIsLoaded) {
+ return;
+ }
+
+ mCarAppColors = CarColorUtils.resolveAppColor(mContext, mComponentName, mHostResourceIds);
+ mIsLoaded = true;
+ }
+
+ private CarAppPackageInfoImpl(
+ @NonNull Context context,
+ @NonNull ComponentName componentName,
+ boolean isNavigationApp,
+ @NonNull HostResourceIds hostResourceIds,
+ @NonNull AppIconLoader appIconLoader) {
+ mContext = context;
+ mComponentName = componentName;
+ mIsNavigationApp = isNavigationApp;
+ mHostResourceIds = hostResourceIds;
+ mAppIconLoader = appIconLoader;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java
new file mode 100644
index 0000000..10ce9db
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.internal;
+
+import android.location.Location;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.Place;
+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.ThreadUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link LocationMediator}.
+ *
+ * <p>There's only one set of places available at any given time, so the last writer wins. This
+ * class is not meant to be used with multiple publishers (e.g. shared between multiple apps) so
+ * that is just fine.
+ *
+ * <p>This class is not safe for concurrent access.
+ */
+public class LocationMediatorImpl implements LocationMediator {
+ /** Interface for requesting start and stop of location updates from an app. */
+ public interface AppLocationUpdateRequester {
+ /** Sets whether to get location updates from an app. */
+ void enableLocationUpdates(boolean enabled);
+ }
+
+ @Nullable private CarLocation mCameraAnchor;
+ private List<Place> mCurrentPlaces = ImmutableList.of();
+ private final List<AppLocationListener> mAppLocationListeners = new ArrayList<>();
+ private final EventManager mEventManager;
+ private final AppLocationUpdateRequester mLocationUpdateRequester;
+
+ /** Returns an instance of a {@link LocationMediator}. */
+ public static LocationMediator create(
+ EventManager eventManager, AppLocationUpdateRequester locationUpdateRequester) {
+ return new LocationMediatorImpl(eventManager, locationUpdateRequester);
+ }
+
+ @Override
+ public List<Place> getCurrentPlaces() {
+ return mCurrentPlaces;
+ }
+
+ @Override
+ public void setCurrentPlaces(List<Place> places) {
+ ThreadUtils.ensureMainThread();
+
+ if (mCurrentPlaces.equals(places)) {
+ return;
+ }
+ mCurrentPlaces = places;
+ mEventManager.dispatchEvent(EventType.PLACE_LIST);
+ }
+
+ @Override
+ @Nullable
+ public CarLocation getCameraAnchor() {
+ return mCameraAnchor;
+ }
+
+ @Override
+ public void setCameraAnchor(@Nullable CarLocation cameraAnchor) {
+ mCameraAnchor = cameraAnchor;
+ }
+
+ @Override
+ public void addAppLocationListener(AppLocationListener listener) {
+ if (mAppLocationListeners.isEmpty()) {
+ mLocationUpdateRequester.enableLocationUpdates(true);
+ }
+ mAppLocationListeners.add(listener);
+ }
+
+ @Override
+ public void removeAppLocationListener(AppLocationListener listener) {
+ mAppLocationListeners.remove(listener);
+ if (mAppLocationListeners.isEmpty()) {
+ mLocationUpdateRequester.enableLocationUpdates(false);
+ }
+ }
+
+ @Override
+ public void setAppLocation(Location location) {
+ for (AppLocationListener listener : mAppLocationListeners) {
+ listener.onAppLocationChanged(location);
+ }
+ }
+
+ private LocationMediatorImpl(
+ EventManager eventManager, AppLocationUpdateRequester locationUpdateRequester) {
+ mEventManager = eventManager;
+ mLocationUpdateRequester = locationUpdateRequester;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java
new file mode 100644
index 0000000..b878286
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.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.apphost.lang;
+
+import static com.android.car.libraries.apphost.logging.L.buildMessage;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Supplier;
+
+/**
+ * Utility methods for handling {@code null}able fields and methods.
+ *
+ * <p>These methods should be statically imported and <b>not</b> qualified by class name.
+ */
+public class NullUtils {
+ /**
+ * Returns a {@link Denullerator} with an initial value and type corresponding to the passed
+ * parameter.
+ */
+ public static <T extends Object> Denullerator<T> ifNonNull(@Nullable T reference) {
+ return new Denullerator<>(reference);
+ }
+
+ /**
+ * A reference that can store {@code null} values but from which {@code null} values can never
+ * be retrieved.
+ *
+ * <p>Note that the generic parameter must extend Object explicitly to ensure that the generic
+ * type itself does not match something @Nullable. See
+ * http://go/nullness_troubleshooting#issues-with-type-parameter-annotations
+ *
+ * @param <T> target class
+ */
+ public static class Denullerator<T extends Object> {
+ @Nullable private T mReference;
+
+ /**
+ * New Denullerators should only be created using {@link NullUtils#ifNonNull(Object)} above.
+ */
+ private Denullerator(@Nullable T reference) {
+ mReference = reference;
+ }
+
+ /** Returns a denullerator of a reference value. */
+ public Denullerator<T> otherwiseIfNonNull(@Nullable T reference) {
+ if (mReference == null) {
+ mReference = reference;
+ }
+ return this;
+ }
+
+ /** Returns a denullerators of a reference value supplier. */
+ public Denullerator<T> otherwiseIfNonNull(Supplier<@PolyNull T> referenceSupplier) {
+ if (mReference == null) {
+ mReference = referenceSupplier.get();
+ }
+ return this;
+ }
+
+ /** Return the value if it's not non-null. */
+ public T otherwise(T reference) {
+ return otherwiseIfNonNull(reference).otherwiseThrow();
+ }
+
+ /** Return the value if it's not non-null. */
+ public T otherwise(Supplier<T> referenceSupplier) {
+ return otherwiseIfNonNull(referenceSupplier).otherwiseThrow();
+ }
+
+ /** Returns an exception that values are not non-null */
+ public T otherwiseThrow() {
+ return otherwiseThrow("None of the supplied values were non-null!");
+ }
+
+ /** Returns the reference if it's not non-null. */
+ public T otherwiseThrow(String msg, Object... msgArgs) {
+ if (mReference == null) {
+ throw new NullPointerException(buildMessage(msg, msgArgs));
+ }
+ return mReference;
+ }
+ }
+
+ private NullUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java
new file mode 100644
index 0000000..2169c21
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.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.apphost.lang;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This is an annotation stub to avoid dependencies on annotations that aren't in the Android
+ * platform source tree.
+ */
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.LOCAL_VARIABLE,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.PARAMETER,
+ ElementType.TYPE,
+ ElementType.TYPE_PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface PolyNull {
+ /** This is an enum stub to avoid dependencies. */
+ enum MigrationStatus {
+ IGNORE,
+ WARN,
+ STRICT
+ }
+
+ // These fields maintain API compatibility with annotations that expect arguments.
+ String[] value() default {};
+
+ boolean result() default false;
+
+ String[] expression() default "";
+
+ MigrationStatus status() default MigrationStatus.IGNORE;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java
new file mode 100644
index 0000000..5198293
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.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.apphost.logging;
+
+/** Each enum represents one of the Car App Library's possible host to client APIs. */
+// TODO(b/171817245): Remove LINT.IFTT in copybara
+// LINT.IfChange
+public enum CarAppApi {
+ UNKNOWN_API,
+ GET_APP_VERSION,
+ ON_HANDSHAKE_COMPLETED,
+ GET_MANAGER,
+ GET_TEMPLATE,
+ ON_APP_CREATE,
+ DISPATCH_LIFECYCLE,
+ ON_NEW_INTENT,
+ ON_CONFIGURATION_CHANGED,
+ ON_SURFACE_AVAILABLE,
+ ON_SURFACE_DESTROYED,
+ ON_VISIBLE_AREA_CHANGED,
+ ON_STABLE_AREA_CHANGED,
+ ON_CLICK,
+ ON_SELECTED,
+ ON_SEARCH_TEXT_CHANGED,
+ ON_SEARCH_SUBMITTED,
+ ON_NAVIGATE,
+ STOP_NAVIGATION,
+ ON_RECORDING_STARTED,
+ ON_RECORDING_STOPPED,
+ ON_ITEM_VISIBILITY_CHANGED,
+ ON_CHECKED_CHANGED,
+ ON_BACK_PRESSED,
+ BIND,
+ ON_INPUT_SUBMITTED,
+ ON_INPUT_TEXT_CHANGED,
+ ON_CARHARDWARE_RESULT,
+ ON_PAN_MODE_CHANGED,
+ START_LOCATION_UPDATES,
+ STOP_LOCATION_UPDATES,
+}
+// LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/internal/\
+// TelemetryHandlerImpl.java,
+// //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+// ClearcutTelemetryHandler.java,
+// //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+// android_automotive_templates_host_info.proto)
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java
new file mode 100644
index 0000000..92d09e3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+/** Different errors that may happen due to a Car App Library IPC. */
+// LINT.IfChange
+public enum CarAppApiErrorType {
+ UNKNOWN_ERROR,
+ BUNDLER_EXCEPTION,
+ ILLEGAL_STATE_EXCEPTION,
+ INVALID_PARAMETER_EXCEPTION,
+ SECURITY_EXCEPTION,
+ RUNTIME_EXCEPTION,
+ REMOTE_EXCEPTION,
+ ANR
+}
+// LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/internal/\
+// TelemetryHandlerImpl.java,
+// //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+// ClearcutTelemetryLogger.java,
+// //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+// android_automotive_templates_host_info.proto)
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java
new file mode 100644
index 0000000..ea64917
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+import com.google.auto.value.AutoValue;
+
+/** Internal representation of the content limit queried by car app */
+@AutoValue
+public abstract class ContentLimitQuery {
+
+ /** Returns the content limit type */
+ public abstract int getContentLimitType();
+
+ /** Returns the content limit value */
+ public abstract int getContentLimitValue();
+
+ /**
+ * Returns a new builder of {@link ContentLimitQuery} set up with the information from this event.
+ */
+ public static ContentLimitQuery.Builder newBuilder() {
+ return new AutoValue_ContentLimitQuery.Builder();
+ }
+
+ /** ContentLimit builder. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ /** Sets the content limits {@code type}. */
+ public abstract Builder setContentLimitType(int type);
+
+ /** Sets the content limits {@code value}. */
+ public abstract Builder setContentLimitValue(int value);
+
+ /** Builds a {@link ContentLimitQuery} from this builder. */
+ public ContentLimitQuery build() {
+ return autoBuild();
+ }
+
+ abstract ContentLimitQuery autoBuild();
+ }
+
+ /** Returns a {@link ContentLimitQuery} with given {@code type} and {@code value}. */
+ public static ContentLimitQuery getContentLimitQuery(int type, int value) {
+ return ContentLimitQuery.newBuilder()
+ .setContentLimitValue(value)
+ .setContentLimitType(type)
+ .build();
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java
new file mode 100644
index 0000000..a93469d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+import java.util.Arrays;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+import java.util.function.Supplier;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Helper class for logging. */
+public final class L {
+ private static final String STRING_MEANING_NULL = "null";
+
+ /** Builds a log message from a message format string and its arguments. */
+ public static String buildMessage(@Nullable String message, @Nullable Object... args) {
+ // If the message is null, ignore the args and return "null";
+ if (message == null) {
+ return STRING_MEANING_NULL;
+ }
+
+ // else if the args are null or 0-length, return message
+ if (args == null || args.length == 0) {
+ try {
+ return String.format(Locale.US, message);
+ } catch (IllegalFormatException ex) {
+ return message;
+ }
+ }
+
+ // Use deepToString to get a more useful representation of any arrays in args
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] != null && args[i].getClass().isArray()) {
+ // Wrap in an array, deepToString, then remove the extra [] from the wrapper. This
+ // allows handling all array types rather than having separate branches for all
+ // primitive array types plus Object[].
+ String string = Arrays.deepToString(new Object[] {args[i]});
+ // Strip the outer [] from the wrapper array.
+ args[i] = string.substring(1, string.length() - 1);
+ }
+ }
+
+ // else try formatting the string.
+ try {
+ return String.format(Locale.US, message, args);
+ } catch (IllegalFormatException ex) {
+ return message + Arrays.deepToString(args);
+ }
+ }
+
+ /**
+ * Log a verbose message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void v(String tag, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.VERBOSE)) {
+ Log.v(tag, message);
+ }
+ }
+
+ /**
+ * Log a verbose message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void v(String tag, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.VERBOSE)) {
+ Log.v(tag, message.get());
+ }
+ }
+
+ /**
+ * Log a verbose message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void v(
+ String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.VERBOSE)) {
+ Log.v(tag, buildMessage(message, args));
+ }
+ }
+
+ /**
+ * Log a verbose message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void v(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.VERBOSE)) {
+ Log.v(tag, message, th);
+ }
+ }
+
+ /**
+ * Log a verbose message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void v(
+ String tag,
+ @Nullable Throwable th,
+ @NonNull @FormatString String message,
+ @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.VERBOSE)) {
+ Log.v(tag, buildMessage(message, args), th);
+ }
+ }
+
+ /**
+ * Log a debug message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void d(String tag, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, message);
+ }
+ }
+
+ /**
+ * Log a debug message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void d(String tag, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, message.get());
+ }
+ }
+
+ /**
+ * Log a debug message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void d(
+ String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, buildMessage(message, args));
+ }
+ }
+
+ /**
+ * Log a debug message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void d(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, message, th);
+ }
+ }
+
+ /**
+ * Log a debug message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void d(
+ String tag,
+ @Nullable Throwable th,
+ @NonNull @FormatString String message,
+ @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, buildMessage(message, args), th);
+ }
+ }
+
+ /**
+ * Log an info message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void i(String tag, @NonNull @FormatString String message) {
+
+ if (Log.isLoggable(tag, Log.INFO)) {
+ Log.i(tag, message);
+ }
+ }
+
+ /**
+ * Log an info message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void i(
+ String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.INFO)) {
+ Log.i(tag, buildMessage(message, args));
+ }
+ }
+
+ /**
+ * Log an info message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void i(String tag, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.INFO)) {
+ Log.i(tag, message.get());
+ }
+ }
+
+ /**
+ * Log an info message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void i(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.INFO)) {
+ Log.i(tag, message, th);
+ }
+ }
+
+ /**
+ * Log an info message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void i(
+ String tag,
+ @Nullable Throwable th,
+ @NonNull @FormatString String message,
+ @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.INFO)) {
+ Log.i(tag, buildMessage(message, args), th);
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void w(String tag, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, message);
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void w(String tag, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, message.get());
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void w(String tag, @Nullable Throwable th, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, message.get(), th);
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void w(
+ String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, buildMessage(message, args));
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void w(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, message, th);
+ }
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void w(
+ String tag,
+ @Nullable Throwable th,
+ @NonNull @FormatString String message,
+ @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.WARN)) {
+ Log.w(tag, buildMessage(message, args), th);
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void e(String tag, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, message);
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void e(
+ String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, buildMessage(message, args));
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ */
+ @FormatMethod
+ public static void e(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, message, th);
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void e(String tag, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, message.get());
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message a supplier of the message to log. This will only be executed if the log level
+ * for the given tag is enabled.
+ */
+ public static void e(String tag, @Nullable Throwable th, @NonNull Supplier<String> message) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, message.get(), th);
+ }
+ }
+
+ /**
+ * Log an error message.
+ *
+ * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+ * has this restriction.
+ * @param th a throwable to log.
+ * @param message the string message to log. This can also be a string format that's recognized by
+ * {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+ * a result".
+ * @param args the formatting args for the previous string.
+ */
+ @FormatMethod
+ public static void e(
+ String tag,
+ @Nullable Throwable th,
+ @NonNull @FormatString String message,
+ @Nullable Object... args) {
+ if (Log.isLoggable(tag, Log.ERROR)) {
+ Log.e(tag, buildMessage(message, args), th);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java
new file mode 100644
index 0000000..ad48ed3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.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.apphost.logging;
+
+/**
+ * Declares the log tags to use in the app host package.
+ *
+ * <p>These tags are defined at a higher logical and component level, rather than on a strict
+ * per-class basis.
+ *
+ * <p><strong>IMPORTANT</strong>: do not use per-class tags, since those are often way too granular,
+ * hard to manage, and an inferior choice in every way. If you need finer-granularity tags than
+ * those here, consider adding a new one.
+ */
+public abstract class LogTags {
+ /** General purpose tag used for most components. */
+ public static final String APP_HOST = "CarApp.H";
+
+ /** Tag for code related to constraint host. */
+ public static final String CONSTRAINT = APP_HOST + ".Con";
+
+ /** Tag for code related to driver distraction handling. */
+ public static final String DISTRACTION = APP_HOST + ".Dis";
+
+ /** Tag for code related to template handling. */
+ public static final String TEMPLATE = APP_HOST + ".Tem";
+
+ /** Tag for navigation specific host code. */
+ public static final String NAVIGATION = APP_HOST + ".Nav";
+
+ /** Tag for cluster specific host code. */
+ public static final String CLUSTER = APP_HOST + ".Clu";
+
+ /** Tag for renderer service (automotive) specific host code. */
+ public static final String SERVICE = APP_HOST + ".Ser";
+
+ private LogTags() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java
new file mode 100644
index 0000000..ab19b69
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+import java.io.PrintWriter;
+
+/** An interface for a component that can contribute status to a bug report. */
+public interface StatusReporter {
+ /** Specifies how to handle PII in a status report. */
+ enum Pii {
+ /** Omit PII from the bug report. */
+ HIDE,
+ /** Show PII in the bug report. */
+ SHOW
+ }
+
+ /**
+ * Writes the status of this component to a bug report.
+ *
+ * @param pw A {@link PrintWriter} to which to write the status.
+ * @param piiHandling How to handle PII in the report.
+ */
+ void reportStatus(PrintWriter pw, Pii piiHandling);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java
new file mode 100644
index 0000000..d5deb3a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+import android.content.ComponentName;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+
+/** Internal representation of a telemetry event. */
+@AutoValue
+public abstract class TelemetryEvent {
+
+ /** Types of actions to be reported */
+ // LINT.IfChange
+ public enum UiAction {
+ APP_START,
+ APP_RUNTIME,
+
+ CAR_APP_API_SUCCESS,
+ CAR_APP_API_FAILURE,
+
+ CAR_APPS_AVAILABLE,
+
+ CLIENT_SDK_VERSION,
+ HOST_SDK_VERSION,
+
+ TEMPLATE_FLOW_LIMIT_EXCEEDED,
+ TEMPLATE_FLOW_INVALID_BACK,
+
+ NAVIGATION_STARTED,
+ NAVIGATION_TRIP_UPDATED,
+ NAVIGATION_ENDED,
+
+ PAN,
+ ROTARY_PAN,
+ FLING,
+ ZOOM,
+
+ ROW_CLICKED,
+ ACTION_STRIP_FAB_CLICKED,
+ ACTION_BUTTON_CLICKED,
+
+ LIST_SIZE,
+ ACTION_STRIP_SIZE,
+ GRID_ITEM_LIST_SIZE,
+
+ ACTION_STRIP_SHOW,
+ ACTION_STRIP_HIDE,
+
+ CONTENT_LIMIT_QUERY,
+
+ HOST_FAILURE_CLUSTER_ICON,
+
+ MINIMIZED_STATE,
+
+ SPEEDBUMPED,
+
+ COLOR_CONTRAST_CHECK_PASSED,
+ COLOR_CONTRAST_CHECK_FAILED,
+ }
+
+ /** Returns the {@link UiAction} that represents the type of action associated with this event. */
+ public abstract UiAction getAction();
+
+ /** Returns the version of the app SDK. */
+ public abstract Optional<String> getCarAppSdkVersion();
+
+ /** Returns the duration of the event, in milliseconds. */
+ public abstract Optional<Long> getDurationMs();
+
+ /** Returns the {@link CarAppApi} associated with the event. */
+ public abstract Optional<CarAppApi> getCarAppApi();
+
+ /** Returns the {@link ComponentName} for the app the event is coming from. */
+ public abstract Optional<ComponentName> getComponentName();
+
+ /** Returns the {@link CarAppApiErrorType} if the event is an error. */
+ public abstract Optional<CarAppApiErrorType> getErrorType();
+
+ /** Returns the position of the event */
+ public abstract Optional<Integer> getPosition();
+
+ /** Returns the count of the loaded item */
+ public abstract Optional<Integer> getItemsLoadedCount();
+
+ /** Returns a {@link ContentLimitQuery} that is used in the car app. */
+ public abstract Optional<ContentLimitQuery> getCarAppContentLimitQuery();
+
+ /** Returns the name of the template used for this event. */
+ public abstract Optional<String> getTemplateClassName();
+
+ /**
+ * Returns a new builder of {@link TelemetryEvent} set up with the given {@link UiAction}, and the
+ * provided {@link ComponentName} set.
+ */
+ public static Builder newBuilder(UiAction action, ComponentName appName) {
+ return newBuilder(action).setComponentName(appName);
+ }
+
+ /** Returns a new builder of {@link TelemetryEvent} set up with the given {@link UiAction} */
+ public static Builder newBuilder(UiAction action) {
+ return new AutoValue_TelemetryEvent.Builder().setAction(action);
+ }
+
+ /** UiLogEvent builder. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ /** Sets the {@link UiAction} that represents the type of action associated with this event. */
+ public abstract Builder setAction(UiAction action);
+
+ /** Sets the version of the app SDK. */
+ public abstract Builder setCarAppSdkVersion(String carAppSdkVersion);
+
+ /** Sets the duration of the event, in milliseconds. */
+ public abstract Builder setDurationMs(long durationMillis);
+
+ /** Sets the {@link CarAppApi} associated with the event. */
+ public abstract Builder setCarAppApi(CarAppApi carAppApi);
+
+ /** Sets the {@link ComponentName} for the app the event is coming from. */
+ public abstract Builder setComponentName(ComponentName componentName);
+
+ /** Sets the {@link CarAppApiErrorType} if the event is an error. */
+ public abstract Builder setErrorType(CarAppApiErrorType errorType);
+
+ /** Sets the position of the event */
+ public abstract Builder setPosition(int position);
+
+ /** Sets the count of the loaded item */
+ public abstract Builder setItemsLoadedCount(int position);
+
+ /** Sets the {@link ContentLimitQuery} that is used in the car app. */
+ public abstract Builder setCarAppContentLimitQuery(ContentLimitQuery constraints);
+
+ /** Sets the class name of the template */
+ public abstract Builder setTemplateClassName(String className);
+
+ /** Builds a {@link TelemetryEvent} from this builder. */
+ public TelemetryEvent build() {
+ return autoBuild();
+ }
+
+ /** Non-visible builder method for AutoValue to implement. */
+ abstract TelemetryEvent autoBuild();
+ }
+ // LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/\
+ // internal/TelemetryHandlerImpl.java,
+ // //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+ // ClearcutTelemetryHandler.java,
+ // //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+ // android_automotive_templates_host_info.proto)
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java
new file mode 100644
index 0000000..9d68892
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.logging;
+
+import android.content.ComponentName;
+import androidx.car.app.FailureResponse;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+
+/**
+ * Telemetry service abstraction. Implementations are expected to convert these events to their own
+ * representation and send the information to their corresponding backend.
+ */
+public abstract class TelemetryHandler {
+
+ /** Logs a telemetry event for the given {@link TelemetryEvent.Builder}. */
+ public abstract void logCarAppTelemetry(TelemetryEvent.Builder logEventBuilder);
+
+ /**
+ * Logs a telemetry event with the given {@link UiAction}, the provided {@link ComponentName}, and
+ * the provided {@link CarAppApi}.
+ */
+ public void logCarAppApiSuccessTelemetry(ComponentName appName, CarAppApi carAppApi) {
+ TelemetryEvent.Builder builder =
+ TelemetryEvent.newBuilder(UiAction.CAR_APP_API_SUCCESS, appName).setCarAppApi(carAppApi);
+ logCarAppTelemetry(builder);
+ }
+
+ /**
+ * Logs a telemetry event with the given {@link UiAction}, the provided {@link ComponentName}, the
+ * provided {@link CarAppApi}, and the provided {@link CarAppApiErrorType}.
+ */
+ public void logCarAppApiFailureTelemetry(
+ ComponentName appName, CarAppApi carAppApi, CarAppApiErrorType errorType) {
+ TelemetryEvent.Builder builder =
+ TelemetryEvent.newBuilder(UiAction.CAR_APP_API_FAILURE, appName)
+ .setCarAppApi(carAppApi)
+ .setErrorType(errorType);
+
+ logCarAppTelemetry(builder);
+ }
+
+ /** Helper method for getting the telemetry error type based on a {@link FailureResponse}. */
+ public static CarAppApiErrorType getErrorType(FailureResponse failure) {
+ switch (failure.getErrorType()) {
+ case FailureResponse.BUNDLER_EXCEPTION:
+ return CarAppApiErrorType.BUNDLER_EXCEPTION;
+ case FailureResponse.ILLEGAL_STATE_EXCEPTION:
+ return CarAppApiErrorType.ILLEGAL_STATE_EXCEPTION;
+ case FailureResponse.INVALID_PARAMETER_EXCEPTION:
+ return CarAppApiErrorType.INVALID_PARAMETER_EXCEPTION;
+ case FailureResponse.SECURITY_EXCEPTION:
+ return CarAppApiErrorType.SECURITY_EXCEPTION;
+ case FailureResponse.RUNTIME_EXCEPTION:
+ return CarAppApiErrorType.RUNTIME_EXCEPTION;
+ case FailureResponse.REMOTE_EXCEPTION:
+ return CarAppApiErrorType.REMOTE_EXCEPTION;
+ case FailureResponse.UNKNOWN_ERROR:
+ default:
+ // fall-through
+ }
+ return CarAppApiErrorType.UNKNOWN_ERROR;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java
new file mode 100644
index 0000000..41cd8ea
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java
@@ -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.apphost.nav;
+
+import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.StringUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+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.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link Host} implementation that handles communication between the client app and the rest of
+ * the host.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ */
+public class NavigationHost extends AbstractHost {
+ private final NavigationManagerDispatcher mDispatcher;
+ private final NavigationHostStub mNavHostStub = new NavigationHostStub();
+
+ @Nullable private Trip mTrip;
+ private final NavigationStateCallback mNavigationStateCallback;
+
+ /** Number of status events to store in a circular buffer for debug reports */
+ private static final int MAX_STATUS_ITEMS = 10;
+
+ /**
+ * A circular buffer which will hold at most {@link #MAX_STATUS_ITEMS}. Items are added at the top
+ * of the list and deleted from the end.
+ */
+ private final ArrayDeque<StatusItem> mStatusItemList = new ArrayDeque<>();
+
+ /** Creates a {@link NavigationHost} instance. */
+ public static NavigationHost create(
+ Object appBinding, TemplateContext templateContext, NavigationStateCallback callback) {
+ return new NavigationHost(
+ NavigationManagerDispatcher.create(appBinding), templateContext, callback);
+ }
+
+ @Override
+ public INavigationHost.Stub getBinder() {
+ assertIsValid();
+ return mNavHostStub;
+ }
+
+ /** Returns the {@link Trip} instance currently set in this host. */
+ @Nullable
+ public Trip getTrip() {
+ assertIsValid();
+ return mTrip;
+ }
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {
+ if (mStatusItemList.isEmpty()) {
+ pw.println("No navigation status events stored.");
+ return;
+ }
+ long currentTime = System.currentTimeMillis();
+ // TODO(b/177353816): Update after TableWriter is accessible in the host.
+ pw.printf(
+ "Event | First Event Delta (millis), Num Consecutive Events, Last Event Delta"
+ + " (millis)\n");
+ for (StatusItem item : mStatusItemList) {
+ pw.printf(
+ "%8s| %s, %d, %s\n",
+ item.mEventType.name(),
+ StringUtils.formatDuration(currentTime - item.mInitialEventMillis),
+ item.mNumConsecutiveEvents,
+ StringUtils.formatDuration(currentTime - item.mFinalEventMillis));
+ }
+ }
+
+ @Override
+ public void onDisconnectedEvent() {
+ mNavigationStateCallback.onNavigationEnded();
+ }
+
+ @Override
+ public void onUnboundEvent() {
+ mNavigationStateCallback.onNavigationEnded();
+ }
+
+ private void setTrip(@Nullable Trip trip) {
+ mTrip = trip;
+ }
+
+ private NavigationHost(
+ NavigationManagerDispatcher dispatcher,
+ TemplateContext templateContext,
+ NavigationStateCallback navigationStateCallback) {
+ super(templateContext, LogTags.NAVIGATION);
+ mNavigationStateCallback = navigationStateCallback;
+ mDispatcher = dispatcher;
+ }
+
+ private final class NavigationHostStub extends INavigationHost.Stub {
+ @Override
+ public void updateTrip(Bundleable tripBundle) {
+ runIfValid(
+ "updateTrip",
+ () -> {
+ try {
+ Trip trip = (Trip) tripBundle.get();
+ ThreadUtils.runOnMain(
+ () -> {
+ addStatusItem(StatusItem.EventType.UPDATE);
+ if (mNavigationStateCallback.onUpdateTrip(trip)) {
+ setTrip(trip);
+ }
+ });
+ } catch (BundlerException e) {
+ mTemplateContext
+ .getErrorHandler()
+ .showError(CarAppError.builder(mDispatcher.getAppName()).setCause(e).build());
+ }
+
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(
+ TelemetryEvent.newBuilder(
+ UiAction.NAVIGATION_TRIP_UPDATED,
+ mTemplateContext.getCarAppPackageInfo().getComponentName()));
+ });
+ }
+
+ @Override
+ public void navigationStarted() {
+ runIfValid(
+ "navigationStarted",
+ () -> {
+ L.i(LogTags.NAVIGATION, "%s started navigation", getAppPackageName());
+ ThreadUtils.runOnMain(
+ () -> {
+ addStatusItem(StatusItem.EventType.START);
+ mNavigationStateCallback.onNavigationStarted(
+ () -> {
+ addStatusItem(StatusItem.EventType.STOP);
+ mDispatcher.dispatchStopNavigation(mTemplateContext);
+ });
+ });
+
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(
+ TelemetryEvent.newBuilder(
+ UiAction.NAVIGATION_STARTED,
+ mTemplateContext.getCarAppPackageInfo().getComponentName()));
+ });
+ }
+
+ @Override
+ public void navigationEnded() {
+ if (!isValid()) {
+ L.w(LogTags.NAVIGATION, "Accessed navigationEnded after host became invalidated");
+ }
+ // Run even if not valid so we cleanup state.
+
+ L.i(LogTags.NAVIGATION, "%s ended navigation", getAppPackageName());
+ ThreadUtils.runOnMain(
+ () -> {
+ addStatusItem(StatusItem.EventType.END);
+ mNavigationStateCallback.onNavigationEnded();
+ });
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(
+ TelemetryEvent.newBuilder(
+ UiAction.NAVIGATION_ENDED,
+ mTemplateContext.getCarAppPackageInfo().getComponentName()));
+ }
+ }
+
+ private String getAppPackageName() {
+ return mTemplateContext.getCarAppPackageInfo().getComponentName().getPackageName();
+ }
+
+ private void addStatusItem(StatusItem.EventType eventType) {
+ StatusItem item = mStatusItemList.peekFirst();
+ long timestampMillis = System.currentTimeMillis();
+
+ if (item != null && item.mEventType == eventType) {
+ item.appendTimeStamp(System.currentTimeMillis());
+ return;
+ }
+ item = new StatusItem(eventType, timestampMillis);
+ mStatusItemList.addFirst(item);
+ while (mStatusItemList.size() > MAX_STATUS_ITEMS) {
+ mStatusItemList.removeLast();
+ }
+ }
+
+ /**
+ * Entry for reporting the various events that can be reported on.
+ *
+ * <p>Only saves the time of the first and last event along with a count of the total events.
+ */
+ private static class StatusItem {
+ enum EventType {
+ START,
+ UPDATE,
+ END,
+ STOP
+ };
+
+ final EventType mEventType;
+ final long mInitialEventMillis;
+ int mNumConsecutiveEvents;
+ long mFinalEventMillis;
+
+ StatusItem(EventType eventType, long initialEventMillis) {
+ mEventType = eventType;
+ mInitialEventMillis = initialEventMillis;
+ mNumConsecutiveEvents = 1;
+ mFinalEventMillis = initialEventMillis;
+ }
+
+ public void appendTimeStamp(long timestampMillis) {
+ mNumConsecutiveEvents++;
+ mFinalEventMillis = timestampMillis;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java
new file mode 100644
index 0000000..eef041a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java
@@ -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.apphost.nav;
+
+import androidx.annotation.AnyThread;
+import androidx.car.app.CarContext;
+import androidx.car.app.navigation.INavigationManager;
+import com.android.car.libraries.apphost.ManagerDispatcher;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Dispatcher of calls to the {@link INavigationManager}. */
+public class NavigationManagerDispatcher extends ManagerDispatcher<INavigationManager> {
+ /** Creates an instance of {@link NavigationManagerDispatcher}. */
+ public static NavigationManagerDispatcher create(Object appBinding) {
+ return new NavigationManagerDispatcher(appBinding);
+ }
+
+ /** Dispatches {@link INavigationManager#onStopNavigation} to the app. */
+ @AnyThread
+ public void dispatchStopNavigation(TemplateContext templateContext) {
+ dispatch(
+ NamedAppServiceCall.create(
+ CarAppApi.STOP_NAVIGATION,
+ (manager, anrToken) ->
+ manager.onStopNavigation(new OnDoneCallbackStub(templateContext, anrToken))));
+ }
+
+ private NavigationManagerDispatcher(Object appBinding) {
+ super(CarContext.NAVIGATION_SERVICE, appBinding);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java
new file mode 100644
index 0000000..7d24819
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.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.apphost.nav;
+
+import androidx.car.app.navigation.model.Trip;
+
+/** Handles navigation state change events from {@link NavigationHost}. */
+public interface NavigationStateCallback {
+
+ /**
+ * Notifies that the {@link Trip} set in the {@link NavigationHost} has been updated from the app.
+ */
+ boolean onUpdateTrip(Trip trip);
+
+ /** Notifies that navigation has been started by the app. */
+ void onNavigationStarted(Runnable onNavigationStopRunnable);
+
+ /** Notifies that navigation has been stopped by the app. */
+ void onNavigationEnded();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java
new file mode 100644
index 0000000..7c1a611
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java
@@ -0,0 +1,635 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.SURFACE_STABLE_AREA;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.SURFACE_VISIBLE_AREA;
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.location.Location;
+import android.os.RemoteException;
+import android.view.Surface;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.IAppHost;
+import androidx.car.app.IAppManager;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.PlaceListMapTemplate;
+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 androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.versioning.CarAppApiLevels;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.FlowViolationException;
+import com.android.car.libraries.apphost.distraction.OverLimitFlowViolationException;
+import com.android.car.libraries.apphost.distraction.TemplateValidator;
+import com.android.car.libraries.apphost.distraction.checkers.GridTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.ListTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.MessageTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.NavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PaneTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PlaceListMapTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PlaceListNavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.RoutePreviewNavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.SignInTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.TemplateChecker;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+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.view.SurfaceProvider;
+import com.android.car.libraries.apphost.view.SurfaceProvider.SurfaceProviderListener;
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link Host} implementation that handles communication between the client app and the rest of
+ * the host.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ *
+ * <p>A host service keeps a reference to a {@link UIController} object which it delegates UI calls
+ * to, and is responsible for making all the necessary checks to let the operations go through e.g.
+ * check that the backing context (an activity or fragment) is alive, the app is in started state,
+ * etc.
+ *
+ * <p>The {@link UIController} instance may be updated when the backing context is re-created, e.g.
+ * during config changes such as light/dark mode switches.
+ */
+public class AppHost extends AbstractHost {
+ private final IAppHost.Stub mAppHostStub = new AppHostStub();
+ private final AppManagerDispatcher mDispatcher;
+
+ private UIController mUIController;
+ @Nullable private ISurfaceCallback mSurfaceListener;
+ @Nullable private SurfaceContainer mSurfaceContainer;
+ private final AtomicBoolean mIsPendingGetTemplate = new AtomicBoolean(false);
+ private final TemplateValidator mTemplateValidator;
+ private final TelemetryHandler mTelemetryHandler;
+
+ private final SurfaceProvider.SurfaceProviderListener mSurfaceProviderListener =
+ new SurfaceProviderListener() {
+ @Override
+ public void onSurfaceCreated() {
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface created");
+ }
+
+ // call to onVisibleAreaChanged() not allowed on the given receiver.
+ // call to onStableAreaChanged() not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onSurfaceChanged() {
+ SurfaceContainer container = createOrReuseContainer();
+ mSurfaceContainer = container;
+
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchSurfaceAvailable(listener, container);
+ }
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface updated: %s.", container);
+
+ onVisibleAreaChanged();
+ onStableAreaChanged();
+ }
+
+ @Override
+ public void onSurfaceDestroyed() {
+ SurfaceContainer container = createOrReuseContainer();
+ mSurfaceContainer = container;
+
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchSurfaceDestroyed(listener, container);
+ }
+
+ mSurfaceContainer = null;
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface destroyed");
+ }
+
+ // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onSurfaceScroll(float distanceX, float distanceY) {
+ AppHost.this.onSurfaceScroll(distanceX, distanceY);
+ }
+
+ // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onSurfaceFling(float velocityX, float velocityY) {
+ AppHost.this.onSurfaceFling(velocityX, velocityY);
+ }
+
+ // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+ @SuppressWarnings("nullness:method.invocation")
+ @Override
+ public void onSurfaceScale(float focusX, float focusY, float scaleFactor) {
+ AppHost.this.onSurfaceScale(focusX, focusY, scaleFactor);
+ }
+
+ private SurfaceContainer createOrReuseContainer() {
+ // dereference of possibly-null reference uiController
+ // dereference of possibly-null reference dispatcher
+ @SuppressWarnings("nullness:dereference.of.nullable")
+ SurfaceProvider provider = mUIController.getSurfaceProvider(mDispatcher.getAppName());
+
+ Surface surface = provider.getSurface();
+ int width = provider.getWidth();
+ int height = provider.getHeight();
+ int dpi = provider.getDpi();
+
+ if (mSurfaceContainer != null
+ && mSurfaceContainer.getSurface() == surface
+ && mSurfaceContainer.getWidth() == width
+ && mSurfaceContainer.getHeight() == height
+ && mSurfaceContainer.getDpi() == dpi) {
+ return mSurfaceContainer;
+ }
+
+ return new SurfaceContainer(surface, width, height, dpi);
+ }
+ };
+
+ /**
+ * Creates a template host service.
+ *
+ * @param uiController the controller to delegate UI calls to. Can be updated with {@link
+ * #setUIController(UIController)} henceforth
+ * @param appBinding the binding to use to dispatch client calls
+ */
+ public static AppHost create(
+ UIController uiController, Object appBinding, TemplateContext templateContext) {
+ return new AppHost(uiController, AppManagerDispatcher.create(appBinding), templateContext);
+ }
+
+ @Override
+ public IAppHost.Stub getBinder() {
+ assertIsValid();
+ return mAppHostStub;
+ }
+
+ @Override
+ public void onCarAppBound() {
+ super.onCarAppBound();
+
+ updateUiControllerListener();
+ mTemplateValidator.reset();
+ getTemplate();
+ }
+
+ @Override
+ public void onNewIntentDispatched() {
+ super.onNewIntentDispatched();
+
+ getTemplate();
+ }
+
+ @Override
+ public void onBindToApp(Intent intent) {
+ super.onBindToApp(intent);
+
+ if (mTemplateContext.getCarHostConfig().isNewTaskFlowIntent(intent)) {
+ mTemplateValidator.reset();
+ }
+ }
+
+ @Override
+ public void reportStatus(PrintWriter pw, Pii piiHandling) {
+ pw.printf("- flow validator: %s\n", mTemplateValidator);
+ pw.printf("- surface: %s\n", mSurfaceContainer);
+ }
+
+ /** Dispatches an on-back-pressed event. */
+ public void onBackPressed() {
+ assertIsValid();
+ mDispatcher.dispatchOnBackPressed(mTemplateContext);
+ }
+
+ /** Informs the app to start or stop sending location updates. */
+ public void trySetEnableLocationUpdates(boolean enable) {
+ assertIsValid();
+
+ // The enableLocationUpdates API is only available for API level 4+.
+ int apiLevel = mTemplateContext.getCarHostConfig().getNegotiatedApi();
+ if (apiLevel <= CarAppApiLevels.LEVEL_3) {
+ L.e(LogTags.APP_HOST, "Attempt to request location updates for app Api level %s", apiLevel);
+ return;
+ }
+
+ if (enable) {
+ mDispatcher.dispatchStartLocationUpdates(mTemplateContext);
+ } else {
+ mDispatcher.dispatchStopLocationUpdates(mTemplateContext);
+ }
+ }
+
+ /** Dispatches a surface scroll event. */
+ public void onSurfaceScroll(float distanceX, float distanceY) {
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchOnSurfaceScroll(listener, distanceX, distanceY);
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface scroll: [%f, %f]", distanceX, distanceY);
+ }
+ }
+
+ /** Dispatches a surface fling event. */
+ public void onSurfaceFling(float velocityX, float velocityY) {
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchOnSurfaceFling(listener, velocityX, velocityY);
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface fling: [%f, %f]", velocityX, velocityY);
+ }
+ }
+
+ /** Dispatches a surface scale event. */
+ public void onSurfaceScale(float focusX, float focusY, float scaleFactor) {
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext
+ .getAppDispatcher()
+ .dispatchOnSurfaceScale(listener, focusX, focusY, scaleFactor);
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface scale: [%f]", scaleFactor);
+ }
+ }
+
+ /**
+ * Updates the current {@link UIController}.
+ *
+ * <p>This is normally called when the caller detects that the controller set in the service is
+ * stale due to its backing context being destroyed.
+ */
+ public void setUIController(UIController uiController) {
+ assertIsValid();
+
+ removeUiControllerListener();
+ mUIController = uiController;
+
+ updateUiControllerListener();
+ }
+
+ /** Returns the {@link UIController} attached to this app host. */
+ public UIController getUIController() {
+ assertIsValid();
+ return mUIController;
+ }
+
+ /**
+ * Returns the {@link TemplateValidator} to use to validate whether the templates handled by this
+ * host \ abide by the flow rules.
+ */
+ @VisibleForTesting
+ public TemplateValidator getTemplateValidator() {
+ return mTemplateValidator;
+ }
+
+ /** Registers a {@link TemplateChecker} for a host-only {@link Template}. */
+ public <T extends Template> void registerHostTemplateChecker(
+ Class<T> templateClass, TemplateChecker<T> templateChecker) {
+ mTemplateValidator.registerTemplateChecker(templateClass, templateChecker);
+ }
+
+ @Override
+ public void setTemplateContext(TemplateContext templateContext) {
+ removeEventSubscriptions();
+
+ super.setTemplateContext(templateContext);
+ templateContext.registerAppHostService(TemplateValidator.class, mTemplateValidator);
+ updateEventSubscriptions();
+ }
+
+ @Override
+ public void onDisconnectedEvent() {
+ removeUiControllerListener();
+ }
+
+ private void getTemplate() {
+ boolean wasPendingTemplate = mIsPendingGetTemplate.getAndSet(true);
+ if (wasPendingTemplate) {
+ // Ignore extra invalidate calls between templates being returned.
+ return;
+ }
+
+ mDispatcher.dispatchGetTemplate(this::getTemplateAppServiceCall);
+ }
+
+ private void getTemplateAppServiceCall(IAppManager manager, ANRHandler.ANRToken anrToken)
+ throws RemoteException {
+ manager.getTemplate(
+ new OnDoneCallbackStub(mTemplateContext, anrToken) {
+ @Override
+ public void onSuccess(@Nullable Bundleable response) {
+ super.onSuccess(response);
+ mIsPendingGetTemplate.set(false);
+ ComponentName appName = mDispatcher.getAppName();
+
+ RuntimeException toThrow = null;
+ try {
+ TemplateWrapper wrapper = (TemplateWrapper) checkNotNull(response).get();
+
+ // This checks whether this template meets our task flow
+ // restriction guideline and will throw if the template should not
+ // be added.
+ mTemplateValidator.validateFlow(wrapper);
+ mTemplateValidator.validateHasRequiredPermissions(mTemplateContext, wrapper);
+
+ mUIController.setTemplate(appName, wrapper);
+ } catch (BundlerException e) {
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(appName)
+ .setCause(e)
+ .setDebugMessage("Invalid template")
+ .build());
+ } catch (SecurityException e) {
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(appName)
+ .setCause(e)
+ .setType(CarAppError.Type.MISSING_PERMISSION)
+ .build());
+ toThrow = e;
+ } catch (FlowViolationException e) {
+ mTelemetryHandler.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(
+ e instanceof OverLimitFlowViolationException
+ ? UiAction.TEMPLATE_FLOW_LIMIT_EXCEEDED
+ : UiAction.TEMPLATE_FLOW_INVALID_BACK,
+ appName));
+
+ mTemplateContext
+ .getErrorHandler()
+ .showError(
+ CarAppError.builder(appName)
+ .setCause(e)
+ .setDebugMessage("Template flow restrictions violated")
+ .build());
+ toThrow = new IllegalStateException(e);
+ } catch (RuntimeException e) {
+ mTelemetryHandler.logCarAppApiFailureTelemetry(
+ appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+ mTemplateContext
+ .getErrorHandler()
+ .showError(CarAppError.builder(appName).setCause(e).build());
+ toThrow = e;
+ }
+
+ if (toThrow != null) {
+ // Crash the client process if the template returned does not pass validations.
+ throw toThrow;
+ }
+ }
+
+ @Override
+ public void onFailure(Bundleable failureResponse) {
+ super.onFailure(failureResponse);
+ mIsPendingGetTemplate.set(false);
+ }
+ });
+ }
+
+ /**
+ * Dispatches a call to the template app if the surface has been created and there is a visible
+ * area available.
+ */
+ private void onVisibleAreaChanged() {
+ // Do not fire the visible area changed event until at least after the surfaceContainer
+ // is created, which is triggered by the onSurfaceChanged callback.
+ if (mSurfaceContainer == null) {
+ return;
+ }
+
+ Rect visibleArea = mTemplateContext.getSurfaceInfoProvider().getVisibleArea();
+ if (visibleArea == null) {
+ return;
+ }
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchVisibleAreaChanged(listener, visibleArea);
+ }
+ L.d(LogTags.TEMPLATE, "SurfaceProvider: onVisibleAreaChanged: visibleArea: [%s]", visibleArea);
+ }
+
+ /**
+ * Dispatches a call to the template app if the surface has been created and there is a stable
+ * area visible.
+ */
+ private void onStableAreaChanged() {
+ // Do not fire the Insets changed event until at least after the surfaceContainer
+ // is created, which is triggered by the onSurfaceChanged callback.
+ if (mSurfaceContainer == null) {
+ return;
+ }
+
+ Rect stableArea = mTemplateContext.getSurfaceInfoProvider().getStableArea();
+ if (stableArea == null) {
+ return;
+ }
+ ISurfaceCallback listener = mSurfaceListener;
+ if (listener != null) {
+ mTemplateContext.getAppDispatcher().dispatchStableAreaChanged(listener, stableArea);
+ }
+ L.d(LogTags.DISTRACTION, "SurfaceProvider: onStableAreaChanged: stableArea: [%s]", stableArea);
+ }
+
+ private void registerTemplateValidators() {
+ mTemplateValidator.registerTemplateChecker(GridTemplate.class, new GridTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(ListTemplate.class, new ListTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(MessageTemplate.class, new MessageTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(
+ NavigationTemplate.class, new NavigationTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(PaneTemplate.class, new PaneTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(
+ PlaceListMapTemplate.class, new PlaceListMapTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(
+ PlaceListNavigationTemplate.class, new PlaceListNavigationTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(
+ RoutePreviewNavigationTemplate.class, new RoutePreviewNavigationTemplateChecker());
+ mTemplateValidator.registerTemplateChecker(SignInTemplate.class, new SignInTemplateChecker());
+
+ // Templates that don't require refresh and permission checks.
+ mTemplateValidator.registerTemplateChecker(
+ LongMessageTemplate.class, (newTemplate, oldTemplate) -> true);
+ mTemplateValidator.registerTemplateChecker(
+ SearchTemplate.class, (newTemplate, oldTemplate) -> true);
+ }
+
+ private void updateUiControllerListener() {
+ SurfaceProvider surfaceProvider =
+ mUIController.getSurfaceProvider(
+ mTemplateContext.getCarAppPackageInfo().getComponentName());
+ if (surfaceProvider == null) {
+ // We should always be able to access the surface provider at the point where the ui
+ // controller is set.
+ throw new IllegalStateException(
+ "Can't get surface provider for "
+ + mTemplateContext.getCarAppPackageInfo().getComponentName().flattenToShortString());
+ }
+ surfaceProvider.setListener(mSurfaceProviderListener);
+ }
+
+ private void removeUiControllerListener() {
+ // Remove any outstanding surface listeners whenever the app crashes, otherwise the listener
+ // may send onSurfaceDestroyed calls when the app is not bound.
+ mUIController
+ .getSurfaceProvider(mTemplateContext.getCarAppPackageInfo().getComponentName())
+ .setListener(null);
+ }
+
+ private void updateEventSubscriptions() {
+ mTemplateContext
+ .getEventManager()
+ .subscribeEvent(this, SURFACE_VISIBLE_AREA, AppHost.this::onVisibleAreaChanged);
+ mTemplateContext
+ .getEventManager()
+ .subscribeEvent(this, SURFACE_STABLE_AREA, AppHost.this::onStableAreaChanged);
+ }
+
+ private void removeEventSubscriptions() {
+ mTemplateContext.getEventManager().unsubscribeEvent(this, SURFACE_VISIBLE_AREA);
+ mTemplateContext.getEventManager().unsubscribeEvent(this, SURFACE_STABLE_AREA);
+ }
+
+ @SuppressWarnings("nullness")
+ private AppHost(
+ UIController uiController, AppManagerDispatcher dispatcher, TemplateContext templateContext) {
+ super(templateContext, LogTags.APP_HOST);
+ mUIController = uiController;
+ mDispatcher = dispatcher;
+ mTemplateValidator =
+ TemplateValidator.create(
+ templateContext.getConstraintsProvider().getTemplateStackMaxSize());
+ mTelemetryHandler = templateContext.getTelemetryHandler();
+
+ templateContext.registerAppHostService(TemplateValidator.class, mTemplateValidator);
+
+ registerTemplateValidators();
+ updateUiControllerListener();
+ updateEventSubscriptions();
+ }
+
+ /**
+ * A {@link IAppHost.Stub} implementation that used to receive calls to the app host API from the
+ * client.
+ */
+ private final class AppHostStub extends IAppHost.Stub {
+ @Override
+ public void invalidate() {
+ runIfValid("invalidate", AppHost.this::getTemplate);
+ }
+
+ @Override
+ public void showToast(CharSequence text, int duration) {
+ ThreadUtils.runOnMain(
+ () ->
+ runIfValid(
+ "showToast",
+ () -> mTemplateContext.getToastController().showToast(text, duration)));
+ }
+
+ @Override
+ public void setSurfaceCallback(@Nullable ISurfaceCallback listener) {
+ runIfValid(
+ "setSurfaceCallback",
+ () -> {
+ ComponentName appName = mDispatcher.getAppName();
+ L.d(LogTags.TEMPLATE, "setSurfaceListener for %s", appName);
+
+ Context appConfigurationContext = mTemplateContext.getAppConfigurationContext();
+ if (appConfigurationContext == null) {
+ L.e(LogTags.TEMPLATE, "App configuration context is null");
+ return;
+ }
+
+ try {
+ CarAppPermission.checkHasLibraryPermission(
+ appConfigurationContext, CarAppPermission.ACCESS_SURFACE);
+ } catch (SecurityException e) {
+ // Catch the Exception here to log in host before throwing to the client
+ // app.
+ L.w(
+ LogTags.TEMPLATE,
+ e,
+ "App %s trying to access surface when the permission was not" + " granted",
+ appName);
+
+ throw new SecurityException(e);
+ }
+
+ ThreadUtils.runOnMain(
+ () -> {
+ mSurfaceListener = listener;
+ if (mSurfaceListener == null) {
+ return;
+ }
+
+ if (mSurfaceContainer != null) {
+ mSurfaceProviderListener.onSurfaceChanged();
+ }
+ });
+ });
+ }
+
+ @Override
+ public void sendLocation(Location location) {
+ ThreadUtils.runOnMain(
+ () ->
+ runIfValid(
+ "sendLocation",
+ () ->
+ Objects.requireNonNull(
+ mTemplateContext.getAppHostService(LocationMediator.class))
+ .setAppLocation(location)));
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java
new file mode 100644
index 0000000..a1a6ccb
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.MainThread;
+import androidx.car.app.CarContext;
+import androidx.car.app.IAppManager;
+import com.android.car.libraries.apphost.ManagerDispatcher;
+import com.android.car.libraries.apphost.common.AppServiceCall;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Dispatcher of calls to the {@link IAppManager}. */
+public class AppManagerDispatcher extends ManagerDispatcher<IAppManager> {
+ /** Creates an instance of {@link AppManagerDispatcher} with the given app binding object. */
+ public static AppManagerDispatcher create(Object appBinding) {
+ return new AppManagerDispatcher(appBinding);
+ }
+
+ /** Dispatches {@link IAppManager#getTemplate} to the app. */
+ @AnyThread
+ public void dispatchGetTemplate(AppServiceCall<IAppManager> getTemplateCall) {
+ dispatch(NamedAppServiceCall.create(CarAppApi.GET_TEMPLATE, getTemplateCall));
+ }
+
+ /** Dispatches {@link IAppManager#onBackPressed} to the app. */
+ @AnyThread
+ public void dispatchOnBackPressed(TemplateContext templateContext) {
+ dispatch(
+ NamedAppServiceCall.create(
+ CarAppApi.ON_BACK_PRESSED,
+ (manager, anrToken) ->
+ manager.onBackPressed(new OnDoneCallbackStub(templateContext, anrToken))));
+ }
+
+ /** Dispatches {@link IAppManager#startLocationUpdates} to the app. */
+ @MainThread
+ public void dispatchStartLocationUpdates(TemplateContext templateContext) {
+ dispatch(
+ NamedAppServiceCall.create(
+ CarAppApi.START_LOCATION_UPDATES,
+ (manager, anrToken) ->
+ manager.startLocationUpdates(new OnDoneCallbackStub(templateContext, anrToken))));
+ }
+
+ /** Dispatches {@link IAppManager#stopLocationUpdates} to the app. */
+ @MainThread
+ public void dispatchStopLocationUpdates(TemplateContext templateContext) {
+ dispatch(
+ NamedAppServiceCall.create(
+ CarAppApi.STOP_LOCATION_UPDATES,
+ (manager, anrToken) ->
+ manager.stopLocationUpdates(new OnDoneCallbackStub(templateContext, anrToken))));
+ }
+
+ private AppManagerDispatcher(Object appBinding) {
+ super(CarContext.APP_SERVICE, appBinding);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java
new file mode 100644
index 0000000..bdebe3c
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template;
+
+import androidx.car.app.constraints.IConstraintHost;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.ContentLimitQuery;
+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;
+
+/**
+ * A {@link Host} implementation that handles constraints enforced on the connecting app.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ */
+public final class ConstraintHost extends AbstractHost {
+ private final IConstraintHost.Stub mHostStub = new ConstraintHostStub();
+
+ /** Creates a template host service. */
+ public static ConstraintHost create(TemplateContext templateContext) {
+ return new ConstraintHost(templateContext);
+ }
+
+ @Override
+ public IConstraintHost.Stub getBinder() {
+ assertIsValid();
+ return mHostStub;
+ }
+
+ private ConstraintHost(TemplateContext templateContext) {
+ super(templateContext, LogTags.CONSTRAINT);
+ }
+
+ /**
+ * A {@link IConstraintHost.Stub} implementation that used to receive calls to the constraint host
+ * API from the client.
+ */
+ private final class ConstraintHostStub extends IConstraintHost.Stub {
+ @Override
+ public int getContentLimit(int contentType) {
+ if (!isValid()) {
+ L.w(LogTags.CONSTRAINT, "Accessed getContentLimit after host became invalidated");
+ }
+ int contentValue = mTemplateContext.getConstraintsProvider().getContentLimit(contentType);
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(
+ TelemetryEvent.newBuilder(UiAction.CONTENT_LIMIT_QUERY)
+ .setCarAppContentLimitQuery(
+ ContentLimitQuery.getContentLimitQuery(contentType, contentValue)));
+ return contentValue;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java
new file mode 100644
index 0000000..666b5d6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.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.apphost.template;
+
+import android.content.ComponentName;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.view.SurfaceProvider;
+
+/**
+ * Implements the UI operations that a client app may trigger in the UI.
+ *
+ * <p>This is normally implemented by a backing context such as an activity or fragment that a given
+ * template host is connected to.
+ *
+ * <p>The methods in this interface are tagged with an {@code appName} parameter that indicates
+ * which application the operation is intended to. The controller must drop the call if it is not
+ * currently connected to that app.
+ *
+ * <p>These methods can also drop the calls, or return {@code null} if the backing context is not
+ * available, e.g. because it's been collected by the GC or explicitly cleared.
+ */
+public interface UIController {
+ /** Sets the {@link TemplateWrapper} to display in the given app's view. */
+ void setTemplate(ComponentName appName, TemplateWrapper template);
+
+ /** Returns the {@link SurfaceProvider} for the given app. */
+ SurfaceProvider getSurfaceProvider(ComponentName appName);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java
new file mode 100644
index 0000000..53bf270
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template.view.model;
+
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import com.android.car.libraries.apphost.template.view.model.ActionWrapper.OnClickListener;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A host side wrapper for {@link ActionStrip} to allow additional callbacks to the host. */
+public final class ActionStripWrapper {
+ /**
+ * An invalid focused action index.
+ *
+ * <p>If this value is set, the focus will remain at the user's last focused button.
+ */
+ public static final int INVALID_FOCUSED_ACTION_INDEX = -1;
+
+ private final List<ActionWrapper> mActions;
+ private int mFocusedActionIndex;
+
+ /**
+ * Instantiates an {@link ActionStripWrapper}.
+ *
+ * <p>The optional {@link OnClickListener} allows the host to be notified when an action is
+ * clicked.
+ */
+ public ActionStripWrapper(List<ActionWrapper> actionWrappers, int focusedActionIndex) {
+ this.mActions = actionWrappers;
+ this.mFocusedActionIndex = focusedActionIndex;
+ }
+
+ /** Returns the list of {@link ActionWrapper} in the action strip. */
+ public List<ActionWrapper> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Returns the focused action index determined by the host.
+ *
+ * <p>The value of {@link #INVALID_FOCUSED_ACTION_INDEX} means that the host did not specify any
+ * action button to focus, in which case the focus will remain at the user's last focused button.
+ */
+ public int getFocusedActionIndex() {
+ return mFocusedActionIndex;
+ }
+
+ /** The builder of {@link ActionStripWrapper}. */
+ public static final class Builder {
+ private final List<ActionWrapper> mActions;
+ private int mFocusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+
+ /** Creates an {@link Builder} instance with the given list of {@link ActionWrapper}s. */
+ public Builder(List<ActionWrapper> actions) {
+ this.mActions = actions;
+ }
+
+ /** Creates an {@link Builder} instance with the given {@link ActionStrip}. */
+ public Builder(ActionStrip actionStrip) {
+ List<ActionWrapper> actions = new ArrayList<>();
+ for (Action action : actionStrip.getActions()) {
+ actions.add(new ActionWrapper.Builder(action).build());
+ }
+ this.mActions = actions;
+ }
+
+ /** Sets the index of the action button to focus. */
+ public Builder setFocusedActionIndex(int index) {
+ this.mFocusedActionIndex = index;
+ return this;
+ }
+
+ /** Constructs an {@link ActionStripWrapper} instance defined by this builder. */
+ public ActionStripWrapper build() {
+ if (mFocusedActionIndex < 0 || mFocusedActionIndex >= mActions.size()) {
+ mFocusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+ }
+ return new ActionStripWrapper(mActions, mFocusedActionIndex);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java
new file mode 100644
index 0000000..282fde2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template.view.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+
+/** A host side wrapper for {@link Action} to allow additional callbacks to the host. */
+public class ActionWrapper {
+ /** A host-side on-click listener. */
+ public interface OnClickListener {
+ /** Called when the user clicks the action. */
+ void onClick();
+ }
+
+ private final Action mAction;
+ @Nullable private final OnClickListener mOnClickListener;
+
+ /** Returns the wrapped action. */
+ @NonNull
+ public Action get() {
+ return mAction;
+ }
+
+ /** Returns the host-side on-click listener. */
+ @Nullable
+ public OnClickListener getOnClickListener() {
+ return mOnClickListener;
+ }
+
+ /** Instantiates an {@link ActionWrapper}. */
+ private ActionWrapper(Action action, @Nullable OnClickListener onClickListener) {
+ this.mAction = action;
+ this.mOnClickListener = onClickListener;
+ }
+
+ /** The builder of {@link ActionWrapper}. */
+ public static final class Builder {
+ private final Action mAction;
+ @Nullable private OnClickListener mOnClickListener;
+
+ /** Creates an {@link Builder} instance with the given {@link Action}. */
+ public Builder(Action action) {
+ this.mAction = action;
+ }
+
+ /** Sets the host-side {@link OnClickListener}. */
+ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+ this.mOnClickListener = onClickListener;
+ return this;
+ }
+
+ /** Constructs an {@link ActionWrapper} instance defined by this builder. */
+ public ActionWrapper build() {
+ return new ActionWrapper(mAction, mOnClickListener);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java
new file mode 100644
index 0000000..294dc6d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template.view.model;
+
+import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_NONE;
+
+import android.content.Context;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.SectionedItemList;
+import androidx.car.app.model.Toggle;
+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.RowFlags;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A host side wrapper for both {@link ItemList} and {@link Pane} to allow additional metadata such
+ * as a {@link androidx.car.app.model.Place} for each individual row and/or {@link
+ * RowListConstraints}.
+ */
+public class RowListWrapper {
+ /** Represents different flags to determine how to render the list. */
+ // TODO(b/174601019): clean this up along with RowFlags
+ @IntDef(
+ flag = true,
+ value = {
+ LIST_FLAGS_NONE,
+ LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS,
+ LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW,
+ LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW,
+ LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW,
+ LIST_FLAGS_RENDER_TITLE_AS_SECONDARY,
+ LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ListFlags {}
+
+ public static final int LIST_FLAGS_NONE = (1 << 0);
+
+ /** The list is selectable, and selection should be rendered with radio buttons. */
+ public static final int LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS = (1 << 1);
+
+ /** The list is selectable, and selection should be rendered by highlighting the row. */
+ public static final int LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW = (1 << 2);
+
+ /** The list is selectable, and focus on a row would select it. */
+ public static final int LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW = (1 << 3);
+
+ /** The list is selectable, and selection will scroll the list to the selected row. */
+ public static final int LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW = (1 << 4);
+
+ /** Renders the title of the rows as secondary text. */
+ public static final int LIST_FLAGS_RENDER_TITLE_AS_SECONDARY = (1 << 5);
+
+ /** Whether the list is placed alongside an image that needs to scroll with the list. */
+ public static final int LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE = (1 << 6);
+
+ /** Whether the list should hide the dividers between the rows. */
+ public static final int LIST_FLAGS_HIDE_ROW_DIVIDERS = (1 << 7);
+
+ /** The default flags to use for selectable lists. */
+ private static final int DEFAULT_SELECTABLE_LIST_FLAGS = LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS;
+
+ private final boolean mIsLoading;
+ private final boolean mIsRefresh;
+ @Nullable private final List<Object> mRowList;
+ @Nullable private final CarText mEmptyListText;
+ @Nullable private final CarIcon mImage;
+ @Nullable private final OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+ @Nullable private final Runnable mOnRepeatedSelectionCallback;
+ private final List<RowWrapper> mRowWrappers;
+ private final RowListConstraints mRowListConstraints;
+ @ListFlags private final int mListFlags;
+ private final boolean mIsHalfList;
+
+ /** Returns a builder of {@link RowListWrapper} that wraps the given {@link ItemList}. */
+ public static Builder wrap(Context context, @Nullable ItemList itemList) {
+ if (itemList == null) {
+ return new Builder(context);
+ }
+
+ @SuppressWarnings("unchecked")
+ List<Object> rows = (List) itemList.getItems();
+ Builder builder =
+ new Builder(context)
+ .setRows(rows)
+
+ // Set the default flags for the list, which can be overridden by the caller
+ // to the builder.
+ .setListFlags(getDefaultListFlags(itemList))
+ .setEmptyListText(itemList.getNoItemsMessage())
+ .setOnItemVisibilityChangedDelegate(itemList.getOnItemVisibilityChangedDelegate());
+
+ OnSelectedDelegate onSelectedDelegate = itemList.getOnSelectedDelegate();
+ if (onSelectedDelegate != null) {
+ // Create a selection group for the rows that encompasses the entire list.
+ // The selection groups keep a mutable selection index, and allow for having multiple
+ // selection groups (e.g. different sections of radio buttons) within the same list.
+ builder.setSelectionGroup(
+ SelectionGroup.create(
+ 0, rows.size() - 1, itemList.getSelectedIndex(), onSelectedDelegate));
+ }
+
+ return builder;
+ }
+
+ /** Returns a builder of {@link RowListWrapper} that wraps the given {@link SectionedItemList}. */
+ public static Builder wrap(Context context, List<SectionedItemList> sectionLists) {
+ if (sectionLists.isEmpty()) {
+ return new Builder(context);
+ }
+
+ @SuppressWarnings("unchecked")
+ List<Object> rows = (List) sectionLists;
+ return new Builder(context).setRows(rows);
+ }
+
+ /** Returns a builder of {@link RowListWrapper} that wraps the given {@link Pane}. */
+ public static Builder wrap(Context context, @Nullable Pane pane) {
+ if (pane == null) {
+ L.w(LogTags.TEMPLATE, "Pane is expected on the template but not set");
+ return new Builder(context);
+ }
+
+ // TODO(b/205522074): large image and dividers are specific to pane and the UI hierarchy between
+ // list and pane is diverging more and more. Investigate whether we can decouple the two.
+ int flags = LIST_FLAGS_HIDE_ROW_DIVIDERS;
+ if (pane.getImage() != null) {
+ flags |= LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE;
+ }
+
+ return new Builder(context)
+ .setRows(new ArrayList<>(pane.getRows()))
+ .setListFlags(flags)
+ .setImage(pane.getImage())
+ .setIsLoading(pane.isLoading());
+ }
+
+ /** Returns the list flags to use by default for the given {@link ItemList}. */
+ @ListFlags
+ public static int getDefaultListFlags(@Nullable ItemList itemList) {
+ return itemList != null && itemList.getOnSelectedDelegate() != null
+ ? DEFAULT_SELECTABLE_LIST_FLAGS
+ : LIST_FLAGS_NONE;
+ }
+
+ /** Returns a builder configured with the values from this {@link RowListWrapper} instance. */
+ public Builder newBuilder(Context context) {
+ return new Builder(context, this);
+ }
+
+ /**
+ * Returns the list of rows that make up this list.
+ *
+ * @see Builder#setRows(List)
+ */
+ @Nullable
+ public List<Object> getRows() {
+ return mRowList == null ? null : ImmutableList.copyOf(mRowList);
+ }
+
+ /** Returns the image that should be shown alongside the row list. */
+ @Nullable
+ public CarIcon getImage() {
+ return mImage;
+ }
+
+ /**
+ * Returns the flags that control how to render the list.
+ *
+ * @see Builder#setListFlags(int)
+ */
+ @ListFlags
+ public int getListFlags() {
+ return mListFlags;
+ }
+
+ /**
+ * Returns the delegate to use to notify when when the visibility of items in the list change, for
+ * example during scroll.
+ *
+ * @see Builder#setOnItemVisibilityChangedDelegate(OnItemVisibilityChangedDelegate)
+ */
+ @Nullable
+ public OnItemVisibilityChangedDelegate getOnItemVisibilityChangedDelegate() {
+ return mOnItemVisibilityChangedDelegate;
+ }
+
+ /**
+ * Returns the callback for when a row is repeatedly selected.
+ *
+ * @see Builder#setOnRepeatedSelectionCallback
+ */
+ @Nullable
+ public Runnable getRepeatedSelectionCallback() {
+ return mOnRepeatedSelectionCallback;
+ }
+
+ /** Returns whether the list has no rows. */
+ public boolean isEmpty() {
+ return mRowWrappers.isEmpty();
+ }
+
+ /**
+ * Returns the {@link RowListConstraints} that define the restrictions to apply to the list.
+ *
+ * @see Builder#setRowListConstraints(RowListConstraints)
+ */
+ public RowListConstraints getRowListConstraints() {
+ return mRowListConstraints;
+ }
+
+ /**
+ * Returns the text to display when the list is empty or {@code null} to not display any text.
+ *
+ * @see Builder#setEmptyListText(CarText)
+ */
+ @Nullable
+ public CarText getEmptyListText() {
+ return mEmptyListText;
+ }
+
+ /** Returns the list of {@link RowWrapper} instances that wrap the rows in the list. */
+ public List<RowWrapper> getRowWrappers() {
+ return mRowWrappers;
+ }
+
+ /**
+ * Returns whether the list is in loading state.
+ *
+ * @see Builder#setIsLoading(boolean)
+ */
+ public boolean isLoading() {
+ return mIsLoading;
+ }
+
+ /**
+ * Returns whether the list is in loading state.
+ *
+ * @see Builder#setIsRefresh(boolean)
+ */
+ public boolean isRefresh() {
+ return mIsRefresh;
+ }
+
+ /**
+ * Returns whether this is a half list, as opposed to a full width list.
+ *
+ * @see Builder#setIsHalfList(boolean)
+ */
+ public boolean isHalfList() {
+ return mIsHalfList;
+ }
+
+ /**
+ * Builds the {@link RowWrapper}s for a given list, expanding any sub-lists embedded within it.
+ */
+ @SuppressWarnings("RestrictTo")
+ private static ImmutableList<RowWrapper> buildRowWrappers(
+ Context context,
+ @Nullable List<Object> rowList,
+ @Nullable SelectionGroup selectionGroup,
+ RowListConstraints rowListConstraints,
+ @Nullable CarText selectedText,
+ @RowFlags int rowFlags,
+ @ListFlags int listFlags,
+ int startIndex,
+ boolean isHalfList) {
+ if (rowList == null || rowList.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ // If selectable lists are disallowed, set the selection group to null, which effectively
+ // disables selection.
+ if (!rowListConstraints.getAllowSelectableLists()) {
+ L.w(LogTags.TEMPLATE, "Selectable lists disallowed for template this list");
+ selectionGroup = null;
+ }
+
+ int labelIndex = 1;
+ ImmutableList.Builder<RowWrapper> wrapperListBuilder = new ImmutableList.Builder<>();
+
+ // Sub-lists are expanded inline in this list and become part of it. This size is the
+ // number of rows accounting for any such sub-list expansions.
+ int expandedSize = 0;
+
+ for (Object rowObj : rowList) {
+ // The row is a sub-list: we will expand it and add its rows to the parent list.
+ if (rowObj instanceof SectionedItemList) {
+ SectionedItemList section = (SectionedItemList) rowObj;
+ ItemList subList = section.getItemList();
+
+ if (subList == null || subList.getItems().isEmpty()) {
+ // This should never happen as the client side should prevent empty sub-lists.
+ L.e(LogTags.TEMPLATE, "Found empty sub-list, skipping...");
+ continue;
+ }
+
+ CarText header = section.getHeader();
+ if (header == null) {
+ // This should never happen as the client side should prevent null headers.
+ L.e(LogTags.TEMPLATE, "Header is expected on the section but not set, skipping...");
+ continue;
+ }
+
+ // Create a row representing the header.
+ Row headerRow = new Row.Builder().setTitle(header.toCharSequence()).build();
+ wrapperListBuilder.add(
+ RowWrapper.wrap(headerRow, startIndex + expandedSize)
+ .setListFlags(listFlags)
+ .setRowFlags(rowFlags | RowWrapper.ROW_FLAG_SECTION_HEADER)
+ .setIsHalfList(isHalfList)
+ .setRowConstraints(rowListConstraints.getRowConstraints())
+ .build());
+ expandedSize++;
+
+ // Create wrappers for each row in the sublist.
+ int subListSize = subList.getItems().size();
+ List<RowWrapper> subWrappers =
+ createRowWrappersForSublist(
+ context,
+ subList,
+ expandedSize,
+ rowListConstraints,
+ selectedText,
+ rowFlags,
+ listFlags,
+ isHalfList);
+ wrapperListBuilder.addAll(subWrappers);
+ expandedSize += subListSize;
+ } else {
+ RowWrapper.Builder wrapperBuilder = RowWrapper.wrap(rowObj, startIndex + expandedSize);
+ RowConstraints rowConstraints = rowListConstraints.getRowConstraints();
+ if (rowObj instanceof Row) {
+ Row row = (Row) rowObj;
+ labelIndex = addMetadataToRowWrapper(row, wrapperBuilder, labelIndex);
+
+ Toggle toggle = row.getToggle();
+ if (toggle != null) {
+ wrapperBuilder.setIsToggleChecked(toggle.isChecked());
+ }
+
+ wrapperBuilder.setSelectedText(selectedText);
+ }
+
+ wrapperListBuilder.add(
+ wrapperBuilder
+ .setRowFlags(rowFlags)
+ .setListFlags(listFlags)
+ .setIsHalfList(isHalfList)
+ .setSelectionGroup(selectionGroup)
+ .setRowConstraints(rowConstraints)
+ .build());
+
+ expandedSize++;
+ }
+ }
+
+ return wrapperListBuilder.build();
+ }
+
+ /**
+ * Adds any metadata from the original {@link Row} to its {@link RowWrapper}.
+ *
+ * <p>If a {@link Row} contains a default marker, this updates the marker to render a string based
+ * on the given {@code labelIndex}.
+ *
+ * @return the updated label index value that should be used for the next default marker
+ */
+ private static int addMetadataToRowWrapper(
+ Row row, RowWrapper.Builder wrapperBuilder, int labelIndex) {
+ Metadata metadata = row.getMetadata();
+ if (metadata != null) {
+ Place place = metadata.getPlace();
+ if (place != null) {
+ CarLocation location = place.getLocation();
+ if (location != null) {
+ // Assign any default markers (without text/icon) to show an integer value.
+ PlaceMarker marker = place.getMarker();
+ if (isDefaultMarker(marker)) {
+ PlaceMarker.Builder markerBuilder =
+ new PlaceMarker.Builder().setLabel(Integer.toString(labelIndex));
+ if (marker != null) {
+ CarColor markerColor = marker.getColor();
+ if (markerColor != null) {
+ markerBuilder.setColor(markerColor);
+ }
+ place = new Place.Builder(location).setMarker(markerBuilder.build()).build();
+ metadata = new Metadata.Builder(metadata).setPlace(place).build();
+ }
+ labelIndex++;
+ }
+ }
+ }
+
+ // Sets the metadata in the wrapper with the updated marker if set.
+ wrapperBuilder.setMetadata(metadata);
+ }
+
+ return labelIndex;
+ }
+
+ private static boolean isDefaultMarker(@Nullable PlaceMarker marker) {
+ return marker != null && marker.getIcon() == null && marker.getLabel() == null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ImmutableList<RowWrapper> createRowWrappersForSublist(
+ Context context,
+ ItemList subList,
+ int currentIndex,
+ RowListConstraints rowListConstraints,
+ @Nullable CarText selectedText,
+ @RowFlags int rowFlags,
+ @ListFlags int listFlags,
+ boolean isHalfList) {
+ // Create a selection group for this sub-list.
+ // Offset its indices it by the expanded size to account for any previously expanded
+ // sub-lists.
+ OnSelectedDelegate onSelectedDelegate = subList.getOnSelectedDelegate();
+ SelectionGroup subSelectionGroup =
+ onSelectedDelegate != null
+ ? SelectionGroup.create(
+ currentIndex,
+ currentIndex + subList.getItems().size() - 1,
+ currentIndex + subList.getSelectedIndex(),
+ onSelectedDelegate)
+ : null;
+
+ return buildRowWrappers(
+ context,
+ (List) subList.getItems(),
+ subSelectionGroup,
+ rowListConstraints,
+ selectedText,
+ rowFlags,
+ listFlags == 0 ? getDefaultListFlags(subList) : listFlags,
+ /* startIndex = */ currentIndex,
+ isHalfList);
+ }
+
+ private RowListWrapper(Builder builder) {
+ mIsLoading = builder.mIsLoading;
+ mRowList = builder.mRowList;
+ mImage = builder.mImage;
+ mEmptyListText = builder.mEmptyListText;
+ mOnRepeatedSelectionCallback = builder.mOnRepeatedSelectionCallback;
+ mOnItemVisibilityChangedDelegate = builder.mOnItemVisibilityChangedDelegate;
+ mRowListConstraints = builder.mRowListConstraints;
+ mListFlags = builder.mListFlags;
+ mIsRefresh = builder.mIsRefresh;
+ mIsHalfList = builder.mIsHalfList;
+ mRowWrappers =
+ buildRowWrappers(
+ builder.mContext,
+ builder.mRowList,
+ builder.mSelectionGroup,
+ builder.mRowListConstraints,
+ builder.mSelectedText,
+ builder.mRowFlags,
+ builder.mListFlags,
+ /* startIndex = */ 0,
+ builder.mIsHalfList);
+ }
+
+ /** The builder class for {@link RowListWrapper}. */
+ public static class Builder {
+ private final Context mContext;
+ @Nullable SelectionGroup mSelectionGroup;
+ @Nullable private List<Object> mRowList;
+ @RowFlags private int mRowFlags = ROW_FLAG_NONE;
+ @ListFlags private int mListFlags;
+ private RowListConstraints mRowListConstraints =
+ RowListConstraints.ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+ @Nullable private Runnable mOnRepeatedSelectionCallback;
+ @Nullable private OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+ private boolean mIsLoading;
+ private boolean mIsRefresh;
+ @Nullable private CarText mEmptyListText;
+ @Nullable private CarText mSelectedText;
+ @Nullable private CarIcon mImage;
+ private boolean mIsHalfList;
+
+ private Builder(Context context) {
+ mContext = context;
+ mRowList = null;
+ }
+
+ private Builder(Context context, RowListWrapper rowListWrapper) {
+ mContext = context;
+ mRowList = rowListWrapper.mRowList;
+ mImage = rowListWrapper.mImage;
+ mListFlags = rowListWrapper.mListFlags;
+ mRowListConstraints = rowListWrapper.mRowListConstraints;
+ mIsLoading = rowListWrapper.mIsLoading;
+ mIsRefresh = rowListWrapper.mIsRefresh;
+ mIsHalfList = rowListWrapper.mIsHalfList;
+ mEmptyListText = rowListWrapper.mEmptyListText;
+ mOnItemVisibilityChangedDelegate = rowListWrapper.mOnItemVisibilityChangedDelegate;
+ mOnRepeatedSelectionCallback = rowListWrapper.mOnRepeatedSelectionCallback;
+ }
+
+ /** Sets the set of rows that make up the list. */
+ public Builder setRows(@Nullable List<Object> rowList) {
+ mRowList = rowList;
+ return this;
+ }
+
+ /** Sets the image to be shown alongside the rows. */
+ public Builder setImage(@Nullable CarIcon image) {
+ mImage = image;
+ return this;
+ }
+
+ /** Set an extra callback for when a row in the list has been repeatedly selected. */
+ public Builder setOnRepeatedSelectionCallback(@Nullable Runnable runnable) {
+ mOnRepeatedSelectionCallback = runnable;
+ return this;
+ }
+
+ /**
+ * Sets the delegate to use to notify when when the visibility of items in the list change, for
+ * example, during scroll.
+ */
+ 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 rows added to the
+ * list. If set to {@code false}, the UI shows the actual row contents.
+ */
+ public Builder setIsLoading(boolean isLoading) {
+ mIsLoading = isLoading;
+ return this;
+ }
+
+ /**
+ * Sets whether the list is a refresh of the existing list.
+ *
+ * <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 add to a row when it is selected. */
+ public Builder setRowSelectedText(@Nullable CarText selectedText) {
+ mSelectedText = selectedText;
+ return this;
+ }
+
+ /** Sets the text to display when the list is empty or {@code null} to not display any text. */
+ public Builder setEmptyListText(@Nullable CarText emptyListText) {
+ mEmptyListText = emptyListText;
+ return this;
+ }
+
+ /**
+ * Sets a selection group for this list.
+ *
+ * <p>Selection groups are used for defining a mutually-exclusive selectable range of rows,
+ * which can be used for example for radio buttons.
+ *
+ * @see SelectionGroup
+ */
+ public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+ mSelectionGroup = selectionGroup;
+ return this;
+ }
+
+ /**
+ * Set whether the list is a "half" list.
+ *
+ * <p>"Half list " is the term we use for lists that don't span the entire width of the screen
+ * (e.g. inside of a card in a map template). Note these don't necessarily take exactly half the
+ * width (depending on the screen width and how the card may adapt to it).
+ */
+ public Builder setIsHalfList(boolean isHalfList) {
+ mIsHalfList = isHalfList;
+ return this;
+ }
+
+ /** Sets the flags that control how to render individual rows. */
+ public Builder setRowFlags(@RowFlags int rowFlags) {
+ mRowFlags = rowFlags;
+ return this;
+ }
+
+ /** Sets the flags that control how to render the list. */
+ public Builder setListFlags(@ListFlags int listFlags) {
+ mListFlags = listFlags;
+ return this;
+ }
+
+ /** Sets the {@link RowListConstraints} that define the restrictions to apply to the list. */
+ public Builder setRowListConstraints(RowListConstraints constraints) {
+ mRowListConstraints = constraints;
+ return this;
+ }
+
+ /** Constructs a {@link RowListWrapper} instance from this builder. */
+ public RowListWrapper build() {
+ return new RowListWrapper(this);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java
new file mode 100644
index 0000000..d2793e3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.template.view.model;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+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.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Template;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * A template that wraps {@link RowListWrapper}-based templates.
+ *
+ * <p>This template is used to to render full-screen homogeneous lists, or panes (which are also
+ * built with lists).
+ *
+ * @see #wrap
+ */
+public class RowListWrapperTemplate implements Template {
+ private final RowListWrapper mList;
+ @Nullable private final CarText mTitle;
+ @Nullable private final Action mHeaderAction;
+ @Nullable private final ActionStrip mActionStrip;
+ @Nullable private final List<Action> mActionList;
+ private final ActionsConstraints mActionsconstraints;
+
+ /** The original template being wrapped. */
+ private final Template mTemplate;
+
+ /** Returns the list used by the template. */
+ public RowListWrapper getList() {
+ return mList;
+ }
+
+ /** Returns the title of the template. */
+ @Nullable
+ public CarText getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the {@link Action} to display in the template's header or {@code null} if one is not to
+ * be displayed.
+ */
+ @Nullable
+ public Action getHeaderAction() {
+ return mHeaderAction;
+ }
+
+ /**
+ * Returns the {@link ActionStrip} to display in the template or {@code null} if one is not to be
+ * displayed.
+ */
+ @Nullable
+ public ActionStrip getActionStrip() {
+ return mActionStrip;
+ }
+
+ /**
+ * Returns the list of {@link Action}s to display in the template or {@code null} if one is not to
+ * be displayed.
+ */
+ @Nullable
+ public List<Action> getActionList() {
+ return mActionList;
+ }
+
+ /**
+ * Returns the constraints for the actions in the template.
+ *
+ * @see ActionsConstraints
+ */
+ public ActionsConstraints getActionsConstraints() {
+ return mActionsconstraints;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "RowListWrapperTemplate(" + mTemplate + ")";
+ }
+
+ /**
+ * Returns a {@link RowListWrapperTemplate} instance that wraps the given {@code template}.
+ *
+ * @throws IllegalArgumentException if the {@code template} is not of a type that can be wrapped
+ */
+ public static RowListWrapperTemplate wrap(Context context, Template template, boolean isRefresh) {
+ if (template instanceof PaneTemplate) {
+ PaneTemplate paneTemplate = (PaneTemplate) template;
+ Pane pane = paneTemplate.getPane();
+ return new RowListWrapperTemplate(
+ template,
+ RowListWrapper.wrap(context, pane)
+ .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_PANE)
+ .setIsRefresh(isRefresh)
+ .build(),
+ paneTemplate.getTitle(),
+ paneTemplate.getHeaderAction(),
+ paneTemplate.getActionStrip(),
+ paneTemplate.getPane().getActions(),
+ ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ } else if (template instanceof ListTemplate) {
+ ListTemplate listTemplate = (ListTemplate) template;
+ RowListWrapper.Builder listWrapperBuilder;
+ if (listTemplate.isLoading()) {
+ listWrapperBuilder =
+ RowListWrapper.wrap(context, ImmutableList.of())
+ .setIsLoading(true)
+ .setIsRefresh(isRefresh);
+ } else {
+ ItemList singleList = listTemplate.getSingleList();
+ listWrapperBuilder =
+ singleList == null
+ ? RowListWrapper.wrap(context, listTemplate.getSectionedLists())
+ .setIsRefresh(isRefresh)
+ : RowListWrapper.wrap(context, singleList).setIsRefresh(isRefresh);
+ }
+
+ return new RowListWrapperTemplate(
+ template,
+ listWrapperBuilder
+ .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_FULL_LIST)
+ .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+ .build(),
+ listTemplate.getTitle(),
+ listTemplate.getHeaderAction(),
+ listTemplate.getActionStrip(),
+ /* actionList= */ null,
+ ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+ } else {
+ throw new IllegalArgumentException(
+ "Unknown template class: " + template.getClass().getName());
+ }
+ }
+
+ /** Returns the template wrapped by this instance of a {@link RowListWrapperTemplate}. */
+ @VisibleForTesting
+ public Template getTemplate() {
+ return mTemplate;
+ }
+
+ private RowListWrapperTemplate(
+ Template template,
+ RowListWrapper list,
+ @Nullable CarText title,
+ @Nullable Action headerAction,
+ @Nullable ActionStrip actionStrip,
+ @Nullable List<Action> actionList,
+ ActionsConstraints actionsConstraints) {
+ mTemplate = template;
+ mList = list;
+ mTitle = title;
+ mHeaderAction = headerAction;
+ mActionStrip = actionStrip;
+ mActionList = actionList;
+ mActionsconstraints = actionsConstraints;
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java
new file mode 100644
index 0000000..decaf66
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java
@@ -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.apphost.template.view.model;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.Row;
+import com.android.car.libraries.apphost.distraction.constraints.RowConstraints;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A host side wrapper for {@link Row} which can include extra metadata such as a {@link Place}. */
+public class RowWrapper {
+ /** Represents flags that control some attributes of the row. */
+ // TODO(b/174601019): clean this up along with ListFlags
+ @IntDef(
+ value = {ROW_FLAG_NONE, ROW_FLAG_SHOW_DIVIDERS, ROW_FLAG_SECTION_HEADER},
+ flag = true)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RowFlags {}
+
+ /** No flags applied to the row. */
+ public static final int ROW_FLAG_NONE = (1 << 0);
+
+ /** Whether to show dividers around the row. */
+ public static final int ROW_FLAG_SHOW_DIVIDERS = (1 << 1);
+
+ /**
+ * Whether the row is a section header.
+ *
+ * <p>Sections are used to group rows in the UI, for example, by showing them all within a block
+ * of the same background color.
+ *
+ * <p>A section header is a string of text above the section with a title for it.
+ */
+ public static final int ROW_FLAG_SECTION_HEADER = (1 << 2);
+
+ /** The default flags to use for uniform lists. */
+ public static final int DEFAULT_UNIFORM_LIST_ROW_FLAGS = ROW_FLAG_SHOW_DIVIDERS;
+
+ private final Object mRow;
+ private final int mRowIndex;
+ private final Metadata mMetadata;
+ @Nullable private final CarText mSelectedText;
+ @RowFlags private final int mRowFlags;
+ @RowListWrapper.ListFlags private final int mListFlags;
+ private final RowConstraints mRowConstraints;
+ private boolean mIsHalfList;
+
+ /**
+ * The selection group this row belongs to, or {@code null} if the row does not belong to one.
+ *
+ * <p>Selection groups are used to establish mutually-exclusive scopes of row selection, for
+ * example, to implement radio button groups.
+ *
+ * <p>The selection index in the group is mutable and allows for automatically updating it the UI
+ * at the host without a round-trip to the client to change the selection there.
+ */
+ @Nullable private final SelectionGroup mSelectionGroup;
+
+ /**
+ * Whether the toggle is checked.
+ *
+ * <p>This field is mutable so that we can remember toggle changes on the host without having to
+ * round-trip to the client when toggle states change.
+ *
+ * <p>It is initialized with the initial value from the model coming from the client, then can
+ * mutate after.
+ */
+ private boolean mIsToggleChecked;
+
+ /** Returns a {@link Builder} that wraps a row with the provided index. */
+ public static Builder wrap(Object row, int rowIndex) {
+ return new Builder(row, rowIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + mRow + ", group: " + mSelectionGroup + "]";
+ }
+
+ /** Returns the actual {@link Row} object that this instance is wrapping. */
+ public Object getRow() {
+ return mRow;
+ }
+
+ /** Returns the absolute index of the row in the flattened container list. */
+ public int getRowIndex() {
+ return mRowIndex;
+ }
+
+ /** Returns the {@link Metadata} that is associated with the row. */
+ public Metadata getMetadata() {
+ return mMetadata;
+ }
+
+ /** Returns the {@link CarText} that should be displayed in the row when it has focus. */
+ @Nullable
+ public CarText getSelectedText() {
+ return mSelectedText;
+ }
+
+ /**
+ * Returns the flags that control how to render this row.
+ *
+ * @see Builder#setRowFlags(int)
+ */
+ @RowFlags
+ public int getRowFlags() {
+ return mRowFlags;
+ }
+
+ /**
+ * Returns the flags that control how to render the list this row belongs to.
+ *
+ * @see Builder#setListFlags
+ */
+ @RowListWrapper.ListFlags
+ public int getListFlags() {
+ return mListFlags;
+ }
+
+ /**
+ * Returns whether the row belongs to a "half" list.
+ *
+ * @see Builder#setIsHalfList(boolean)
+ */
+ public boolean isHalfList() {
+ return mIsHalfList;
+ }
+
+ /**
+ * Returns the selection group this row belongs to.
+ *
+ * @see Builder#setSelectionGroup(SelectionGroup)
+ */
+ @Nullable
+ public SelectionGroup getSelectionGroup() {
+ return mSelectionGroup;
+ }
+
+ /**
+ * Returns whether the toggle in the row, if there is one, is checked.
+ *
+ * @see Builder#setIsToggleChecked(boolean)
+ */
+ public boolean isToggleChecked() {
+ return mIsToggleChecked;
+ }
+
+ /** Checks the toggle in the row if unchecked, and vice-versa. */
+ public void switchToggleState() {
+ mIsToggleChecked = !mIsToggleChecked;
+ }
+
+ /**
+ * Returns the {@link RowConstraints} that define the restrictions to apply to the row.
+ *
+ * @see Builder#setRowConstraints(RowConstraints)
+ */
+ public RowConstraints getRowConstraints() {
+ return mRowConstraints;
+ }
+
+ private RowWrapper(Builder builder) {
+ mRow = builder.mRow;
+ mRowIndex = builder.mRowIndex;
+ mMetadata = builder.mEmptyMetadata;
+ mSelectedText = builder.mSelectedText;
+ mRowFlags = builder.mRowFlags;
+ mListFlags = builder.mListFlags;
+ mSelectionGroup = builder.mSelectionGroup;
+ mIsToggleChecked = builder.mIsToggleChecked;
+ mRowConstraints = builder.mRowConstraints;
+ mIsHalfList = builder.mIsHalfList;
+ }
+
+ /** The builder class for {@link RowWrapper}. */
+ public static class Builder {
+ private final Object mRow;
+ private final int mRowIndex;
+ private Metadata mEmptyMetadata = Metadata.EMPTY_METADATA;
+ @RowListWrapper.ListFlags private int mListFlags;
+ @RowFlags private int mRowFlags = ROW_FLAG_NONE;
+ @Nullable private SelectionGroup mSelectionGroup;
+ private boolean mIsToggleChecked;
+ @Nullable private CarText mSelectedText;
+ private RowConstraints mRowConstraints = RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+ private boolean mIsHalfList;
+
+ /** Sets the {@link Metadata} associated with this row. */
+ public Builder setMetadata(Metadata metadata) {
+ mEmptyMetadata = metadata;
+ return this;
+ }
+
+ /** Sets the text to display in the row when it is selected. */
+ public Builder setSelectedText(@Nullable CarText selectedText) {
+ mSelectedText = selectedText;
+ return this;
+ }
+
+ /** Sets the flags that control how to render this row. */
+ public Builder setRowFlags(@RowFlags int rowFlags) {
+ mRowFlags = rowFlags;
+ return this;
+ }
+
+ /** Sets the flags that control how to render the list this row belongs to. */
+ public Builder setListFlags(@RowListWrapper.ListFlags int listFlags) {
+ mListFlags = listFlags;
+ return this;
+ }
+
+ /**
+ * Set whether the list this row belongs to is a "half" list.
+ *
+ * <p>"Half list " is the term we use for lists that don't span the entire width of the screen
+ * (e.g. inside of a card in a map template). Note these don't necessarily take exactly half the
+ * width (depending on the screen width and how the card may adapt to it).
+ */
+ public Builder setIsHalfList(boolean isHalfList) {
+ mIsHalfList = isHalfList;
+ return this;
+ }
+
+ /** Sets the selection group this row belongs to. */
+ public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+ mSelectionGroup = selectionGroup;
+ return this;
+ }
+
+ /** Sets whether the toggle in the row, if there is one, should be displayed checked. */
+ public Builder setIsToggleChecked(boolean isToggleChecked) {
+ mIsToggleChecked = isToggleChecked;
+ return this;
+ }
+
+ /** Sets the {@link RowConstraints} that define the restrictions to apply to the row. */
+ public Builder setRowConstraints(RowConstraints rowConstraints) {
+ mRowConstraints = rowConstraints;
+ return this;
+ }
+
+ /** Constructs a {@link RowWrapper} instance from this builder. */
+ public RowWrapper build() {
+ return new RowWrapper(this);
+ }
+
+ private Builder(Object row, int rowIndex) {
+ mRow = row;
+ mRowIndex = rowIndex;
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java
new file mode 100644
index 0000000..39f8195
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.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.apphost.template.view.model;
+
+import androidx.car.app.model.OnSelectedDelegate;
+
+/**
+ * Represents a set of rows inside of a list that describe a mutually-exclusive selection group.
+ *
+ * <p>This can be used to describe multiple radio sub-lists within the same list.
+ */
+public class SelectionGroup {
+ private final int mStartIndex;
+ private final int mEndIndex;
+ private final OnSelectedDelegate mOnSelectedDelegate;
+
+ /**
+ * The currently selected index.
+ *
+ * <p>The selection index in the group is mutable and allows for automatically updating it the UI
+ * at the host without a round-trip to the client to change the selection there.
+ */
+ private int mSelectedIndex;
+
+ /**
+ * Returns an instance of a {@link SelectionGroup}.
+ *
+ * @param startIndex the index where the selection group starts, inclusive
+ * @param endIndex the index where the selection ends, inclusive
+ * @param selectedIndex the index of the item in the selection group to select
+ * @param onSelectedDelegate a delegate to invoke upon selection change events
+ */
+ public static SelectionGroup create(
+ int startIndex, int endIndex, int selectedIndex, OnSelectedDelegate onSelectedDelegate) {
+ return new SelectionGroup(startIndex, endIndex, selectedIndex, onSelectedDelegate);
+ }
+
+ /** Returns whether the item at the given index is selected. */
+ public boolean isSelected(int index) {
+ return index == mSelectedIndex;
+ }
+
+ /** Returns the index of the item that's currently selected in the group. */
+ public int getSelectedIndex() {
+ return mSelectedIndex;
+ }
+
+ /** Returns the index relative to the selection group. */
+ public int getRelativeIndex(int index) {
+ return index - mStartIndex;
+ }
+
+ /** Returns the delegate to invoke upon selection change events. */
+ public OnSelectedDelegate getOnSelectedDelegate() {
+ return mOnSelectedDelegate;
+ }
+
+ /** Sets the index of the item to select in the group. */
+ public void setSelectedIndex(int selectedIndex) {
+ checkSelectedIndexOutOfBounds(mStartIndex, mEndIndex, selectedIndex);
+ mSelectedIndex = selectedIndex;
+ }
+
+ @Override
+ public String toString() {
+ return "[start: " + mStartIndex + ", end: " + mEndIndex + ", selected: " + mSelectedIndex + "]";
+ }
+
+ private SelectionGroup(
+ int startIndex, int endIndex, int selectedIndex, OnSelectedDelegate onSelectedDelegate) {
+ checkSelectedIndexOutOfBounds(startIndex, endIndex, selectedIndex);
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ mSelectedIndex = selectedIndex;
+ mOnSelectedDelegate = onSelectedDelegate;
+ }
+
+ private static void checkSelectedIndexOutOfBounds(
+ int startIndex, int endIndex, int selectedIndex) {
+ if (selectedIndex < startIndex || selectedIndex > endIndex) {
+ throw new IndexOutOfBoundsException(
+ "Selected index "
+ + selectedIndex
+ + " not within bounds of ["
+ + startIndex
+ + ", "
+ + endIndex
+ + "]");
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java
new file mode 100644
index 0000000..4fdbe4e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+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.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/**
+ * Abstract base class for {@link TemplatePresenter}s which have a {@link
+ * androidx.car.app.SurfaceContainer}.
+ */
+public abstract class AbstractSurfaceTemplatePresenter extends AbstractTemplatePresenter
+ implements PanZoomManager.Delegate {
+ /** The time threshold between touch events for 30fps updates. */
+ private static final long TOUCH_UPDATE_THRESHOLD_MILLIS = 30;
+
+ /** The amount in pixels to pan with a rotary nudge. */
+ private static final float ROTARY_NUDGE_PAN_PIXELS = 50f;
+
+ private final OnGlobalLayoutListener mGlobalLayoutListener =
+ new OnGlobalLayoutListener() {
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void onGlobalLayout() {
+ if (mShouldUpdateVisibleArea) {
+ AbstractSurfaceTemplatePresenter.this.updateVisibleArea();
+ mShouldUpdateVisibleArea = false;
+ }
+ }
+ };
+
+ /** Gesture manager that handles pan and zoom gestures in map-based template presenters. */
+ private final PanZoomManager mPanZoomManager;
+
+ /**
+ * A boolean flag that indicates whether the visible area should be updated in the next layout
+ * phase.
+ */
+ private boolean mShouldUpdateVisibleArea;
+
+ /**
+ * Constructs a new instance of a {@link AbstractTemplatePresenter} with the given {@link
+ * Template}.
+ */
+ @SuppressWarnings({"nullness:method.invocation", "nullness:assignment", "nullness:argument"})
+ public AbstractSurfaceTemplatePresenter(
+ TemplateContext templateContext,
+ TemplateWrapper templateWrapper,
+ StatusBarState statusBarState) {
+ super(templateContext, templateWrapper, statusBarState);
+
+ mPanZoomManager =
+ new PanZoomManager(
+ templateContext, this, getTouchUpdateThresholdMillis(), getRotaryNudgePanPixels());
+ }
+
+ @Override
+ public void onPause() {
+ getView().getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
+ getView().setOnTouchListener(null);
+ getView().setOnGenericMotionListener(null);
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getView().getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
+
+ L.d(
+ LogTags.TEMPLATE,
+ "Pan and zoom is %s in %s",
+ isPanAndZoomEnabled() ? "ENABLED" : "DISABLED",
+ getTemplate());
+ if (isPanAndZoomEnabled()) {
+ getView().setOnTouchListener(mPanZoomManager);
+ getView().setOnGenericMotionListener(mPanZoomManager);
+ }
+ }
+
+ @Override
+ public boolean usesSurface() {
+ return true;
+ }
+
+ @Override
+ public boolean isFullScreen() {
+ return false;
+ }
+
+ /** Adjusts the {@code inset} according to the views visible on screen. */
+ public abstract void calculateAdditionalInset(Rect inset);
+
+ @Override
+ public void onPanModeChanged(boolean isInPanMode) {
+ // No-op by default
+ }
+
+ /** Returns whether the pan and zoom feature is enabled. */
+ public boolean isPanAndZoomEnabled() {
+ return false;
+ }
+
+ /** Returns the time threshold in milliseconds for processing touch events. */
+ public long getTouchUpdateThresholdMillis() {
+ return TOUCH_UPDATE_THRESHOLD_MILLIS;
+ }
+
+ /** Returns the amount in pixels to pan with a rotary nudge. */
+ public float getRotaryNudgePanPixels() {
+ return ROTARY_NUDGE_PAN_PIXELS;
+ }
+
+ /** Returns the {@link OnGlobalLayoutListener} instance attached to the view tree. */
+ @VisibleForTesting
+ public OnGlobalLayoutListener getGlobalLayoutListener() {
+ return mGlobalLayoutListener;
+ }
+
+ /** Returns the {@link PanZoomManager} instance associated with this presenter. */
+ protected PanZoomManager getPanZoomManager() {
+ return mPanZoomManager;
+ }
+
+ /** Requests an update to the surface's visible area information. */
+ protected void requestVisibleAreaUpdate() {
+ // Flip the flag so that we will update the visible area in our next layout phase. We cannot
+ // just update the visible area here because the views are not laid out when they are just
+ // inflated, which means that we cannot use the view coordinates to calculate where the views
+ // are not drawn.
+ mShouldUpdateVisibleArea = true;
+ }
+
+ private void updateVisibleArea() {
+ View rootView = getView();
+ Rect safeAreaInset = new Rect();
+ safeAreaInset.left = rootView.getLeft() + rootView.getPaddingLeft();
+ safeAreaInset.top = rootView.getTop() + rootView.getPaddingTop();
+ safeAreaInset.bottom = rootView.getBottom() - rootView.getPaddingBottom();
+ safeAreaInset.right = rootView.getRight() - rootView.getPaddingRight();
+ calculateAdditionalInset(safeAreaInset);
+ getTemplateContext().getSurfaceInfoProvider().setVisibleArea(safeAreaInset);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java
new file mode 100644
index 0000000..679b90e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import static android.view.View.VISIBLE;
+import static java.lang.Math.max;
+
+import android.graphics.Insets;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.ViewTreeObserver.OnTouchModeChangeListener;
+import android.view.WindowInsets;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleRegistry;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+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.util.List;
+
+/**
+ * Abstract base class for {@link TemplatePresenter}s which implements some of the common presenter
+ * functionality.
+ */
+public abstract class AbstractTemplatePresenter implements TemplatePresenter {
+ /**
+ * Test-only override for {@link #hasWindowFocus()}, since robolectric does not set the window
+ * focus properly.
+ */
+ @VisibleForTesting public Boolean mHasWindowFocusOverride;
+
+ private final TemplateContext mTemplateContext;
+ private final LifecycleRegistry mLifecycleRegistry;
+ private final StatusBarState mStatusBarState;
+
+ private TemplateWrapper mTemplateWrapper;
+
+ /** The last focused view before the presenter was refreshed. */
+ @Nullable private View mLastFocusedView;
+
+ /**
+ * Returns a callback called when the presenter view's touch mode changes.
+ *
+ * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+ */
+ private final OnTouchModeChangeListener mOnTouchModeChangeListener =
+ new OnTouchModeChangeListener() {
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ restoreFocus();
+ }
+ }
+ };
+
+ /**
+ * Returns a callback called when the presenter view's focus changes.
+ *
+ * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+ */
+ private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+ new OnGlobalFocusChangeListener() {
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (newFocus != null) {
+ setLastFocusedView(newFocus);
+ }
+ }
+ };
+
+ /**
+ * Constructs a new instance of a {@link AbstractTemplatePresenter} with the given {@link
+ * Template}.
+ */
+ // Suppress under-initialization checker warning for passing this to the LifecycleRegistry's
+ // ctor.
+ @SuppressWarnings({"nullness:assignment", "nullness:argument"})
+ public AbstractTemplatePresenter(
+ TemplateContext templateContext,
+ TemplateWrapper templateWrapper,
+ StatusBarState statusBarState) {
+ mTemplateContext = templateContext;
+ mTemplateWrapper = TemplateWrapper.copyOf(templateWrapper);
+ mStatusBarState = statusBarState;
+ mLifecycleRegistry = new LifecycleRegistry(this);
+ }
+
+ /** Sets the template this presenter will produce the views for. */
+ @Override
+ public void setTemplate(TemplateWrapper templateWrapper) {
+ mTemplateWrapper = TemplateWrapper.copyOf(templateWrapper);
+
+ onTemplateChanged();
+
+ if (!templateWrapper.isRefresh()) {
+ // Some presenters may get reused even if the template is not a refresh of the previous one.
+ // In those instances, we want the focus to be set to where the default focus should be
+ // instead of last focussed element. Specifically, we want to clear existing focus first,
+ // because if the previous focus was on a row item, and the list is reused and scrolled
+ // to the top, calling setDefaultFocus itself would not reset the focus back to the first
+ // row item.
+ getView().clearFocus();
+ setDefaultFocus();
+ } else {
+ View focusedView = getView().findFocus();
+ if (focusedView != null && focusedView.getVisibility() == VISIBLE) {
+ setLastFocusedView(focusedView);
+ } else {
+ setDefaultFocus();
+ }
+ }
+ }
+
+ /** Returns the template associated with this presenter. */
+ @Override
+ public Template getTemplate() {
+ return mTemplateWrapper.getTemplate();
+ }
+
+ /**
+ * Returns the {@link TemplateWrapper} instance that wraps the template associated with this
+ * presenter.
+ *
+ * @see #getTemplate()
+ */
+ @Override
+ public TemplateWrapper getTemplateWrapper() {
+ return mTemplateWrapper;
+ }
+
+ /** Returns the {@link TemplateContext} instance associated with this presenter. */
+ @Override
+ public TemplateContext getTemplateContext() {
+ return mTemplateContext;
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate() {
+ L.d(LogTags.TEMPLATE, "Presenter onCreate: %s", this);
+ mLifecycleRegistry.setCurrentState(State.CREATED);
+ }
+
+ @Override
+ @CallSuper
+ public void onDestroy() {
+ L.d(LogTags.TEMPLATE, "Presenter onDestroy: %s", this);
+ mLifecycleRegistry.setCurrentState(State.DESTROYED);
+ }
+
+ @Override
+ @CallSuper
+ public void onStart() {
+ L.d(LogTags.TEMPLATE, "Presenter onStart: %s", this);
+ mLifecycleRegistry.setCurrentState(State.STARTED);
+ }
+
+ @Override
+ @CallSuper
+ public void onStop() {
+ L.d(LogTags.TEMPLATE, "Presenter onStop: %s", this);
+ mLifecycleRegistry.setCurrentState(State.CREATED);
+ }
+
+ @Override
+ @CallSuper
+ public void onResume() {
+ L.d(LogTags.TEMPLATE, "Presenter onResume: %s", this);
+ mLifecycleRegistry.setCurrentState(State.RESUMED);
+ mTemplateContext.getStatusBarManager().setStatusBarState(mStatusBarState, getView());
+
+ ViewTreeObserver viewTreeObserver = getView().getViewTreeObserver();
+ viewTreeObserver.addOnTouchModeChangeListener(mOnTouchModeChangeListener);
+ viewTreeObserver.addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ }
+
+ @Override
+ @CallSuper
+ public void onPause() {
+ L.d(LogTags.TEMPLATE, "Presenter onPause: %s", this);
+ mLifecycleRegistry.setCurrentState(State.STARTED);
+
+ ViewTreeObserver viewTreeObserver = getView().getViewTreeObserver();
+ viewTreeObserver.removeOnTouchModeChangeListener(mOnTouchModeChangeListener);
+ viewTreeObserver.removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+
+ if (mTemplateContext.getColorContrastCheckState().checksContrast()) {
+ sendColorContrastTelemetryEvent(mTemplateContext, getTemplate().getClass().getSimpleName());
+ }
+ }
+
+ @Override
+ public void applyWindowInsets(WindowInsets windowInsets, int minimumTopPadding) {
+ 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();
+ }
+
+ View v = getView();
+ v.setPadding(leftInset, max(topInset, minimumTopPadding), rightInset, bottomInset);
+ }
+
+ @Override
+ public boolean setDefaultFocus() {
+ View defaultFocusedView = getDefaultFocusedView();
+ if (defaultFocusedView != null) {
+ defaultFocusedView.requestFocus();
+ setLastFocusedView(defaultFocusedView);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+ return false;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "["
+ + Integer.toHexString(hashCode())
+ + ": "
+ + mTemplateWrapper.getTemplate().getClass().getSimpleName()
+ + "]";
+ }
+
+ /** Indicates that the template set in the presenter has changed. */
+ public abstract void onTemplateChanged();
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycleRegistry;
+ }
+
+ @Override
+ public boolean handlesTemplateChangeAnimation() {
+ return false;
+ }
+
+ @Override
+ public boolean isFullScreen() {
+ return true;
+ }
+
+ @Override
+ public boolean usesSurface() {
+ return false;
+ }
+
+ /**
+ * Restores the presenter's focus to the last focused view.
+ *
+ * <p>Note: A bug in GMS core causes {@link View#isInTouchMode()} to return {@code true} even in
+ * rotary or touchpad mode (b/128031459). When {@link View#layout(int, int, int, int)} is called,
+ * the focus is cleared if {@link View#isInTouchMode()} returns {@code true}. Because the correct
+ * touch mode value is eventually set, we can work around this issue by setting the {@link
+ * #mLastFocusedView} in when the focus changes and restoring the focus when the touch mode is
+ * {@code false} in a {@link OnTouchModeChangeListener}.
+ *
+ * <p>We call {@link #setLastFocusedView(View)} in these places:
+ *
+ * <ul>
+ * <li>In {@link #setDefaultFocus()}: after the presenter is created.
+ * <li>In {@link #setTemplate(TemplateWrapper)}: when the presenter is updated.
+ * <li>In {@link #mOnGlobalFocusChangeListener}: when the user moves the focus in the presenter.
+ * </ul>
+ */
+ @VisibleForTesting
+ public void restoreFocus() {
+ View view = mLastFocusedView;
+ if (view != null) {
+ view.requestFocus();
+ }
+ }
+
+ /**
+ * Moves focus to one of the {@code toViews} if the focus is present in one of the {@code
+ * fromViews}.
+ *
+ * <p>The focus will move to the first view in {@code toViews} that can take focus.
+ *
+ * @return {@code true} if the focus has been moved, otherwise {@code false}
+ */
+ protected static boolean moveFocusIfPresent(List<View> fromViews, List<View> toViews) {
+ for (View fromView : fromViews) {
+ if (fromView.hasFocus()) {
+ for (View toView : toViews) {
+ if (toView.getVisibility() == VISIBLE && toView.requestFocus()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /** Returns whether the window containing the presenter's view has focus. */
+ protected boolean hasWindowFocus() {
+ if (mHasWindowFocusOverride != null) {
+ return mHasWindowFocusOverride;
+ }
+
+ return getView().hasWindowFocus();
+ }
+
+ /** Returns the view that should get focus by default. */
+ protected View getDefaultFocusedView() {
+ return getView();
+ }
+
+ /**
+ * Sets the presenter's last focused view.
+ *
+ * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+ */
+ private void setLastFocusedView(View focusedView) {
+ mLastFocusedView = focusedView;
+ }
+
+ private static void sendColorContrastTelemetryEvent(
+ TemplateContext templateContext, String templateClassName) {
+ TelemetryHandler telemetryHandler = templateContext.getTelemetryHandler();
+ telemetryHandler.logCarAppTelemetry(
+ TelemetryEvent.newBuilder(
+ templateContext.getColorContrastCheckState().getCheckPassed()
+ ? UiAction.COLOR_CONTRAST_CHECK_PASSED
+ : UiAction.COLOR_CONTRAST_CHECK_FAILED,
+ templateContext.getCarAppPackageInfo().getComponentName())
+ .setTemplateClassName(templateClassName));
+
+ // Reset color contrast check state
+ templateContext.getColorContrastCheckState().setCheckPassed(true);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java
new file mode 100644
index 0000000..53aa804
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.TEMPLATE_TOUCHED_OR_FOCUSED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.WINDOW_FOCUS_CHANGED;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.ViewTreeObserver.OnWindowFocusChangeListener;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+import androidx.annotation.MainThread;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import com.android.car.libraries.apphost.common.TemplateContext;
+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.google.common.base.Preconditions;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+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 abstract class AbstractTemplateView extends FrameLayout {
+ /**
+ * The {@link TemplatePresenter} for the template currently set in the view or {@code null} if
+ * none is set.
+ */
+ @Nullable private TemplatePresenter mCurrentPresenter;
+
+ /** The {@link Lifecycle} object of the parent of this view (e.g. the car activity hosting it). */
+ @MonotonicNonNull private Lifecycle mParentLifecycle;
+
+ /**
+ * An observer for the {@link #mParentLifecycle}, which is registered and unregistered when the
+ * view is attached and detached.
+ */
+ @Nullable private LifecycleObserver mLifecycleObserver;
+
+ /**
+ * Context for various {@link TemplatePresenter}s to retrieve important bits of information for
+ * presenting content.
+ */
+ @MonotonicNonNull private TemplateContext mTemplateContext;
+
+ /** {@link WindowInsets} to apply to templates. */
+ @MonotonicNonNull private WindowInsets mWindowInsets;
+
+ /**
+ * The window focus value in the last callback from the {@link OnWindowFocusChangeListener}.
+ *
+ * <p>We need to store this value because the listener is called even if the window focus state
+ * does not change, when the view focus moves.
+ */
+ private boolean mLastWindowFocusState;
+
+ /** A callback called when the template view's window focus changes. */
+ private final OnWindowFocusChangeListener mOnWindowFocusChangeListener =
+ new OnWindowFocusChangeListener() {
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ if (hasFocus != mLastWindowFocusState) {
+ // Dispatch the window focus event only when the window focus state changes.
+ dispatchWindowFocusEvent();
+ mLastWindowFocusState = hasFocus;
+ }
+ }
+ };
+
+ private final OnPreDrawListener mOnPreDrawListener =
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ TemplatePresenter presenter = mCurrentPresenter;
+ if (presenter != null) {
+ return presenter.onPreDraw();
+ }
+ return true;
+ }
+ };
+
+ protected AbstractTemplateView(Context context) {
+ this(context, null);
+ }
+
+ protected AbstractTemplateView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ protected AbstractTemplateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Returns the {@link SurfaceViewContainer} which holds the surface that 3p apps can use to render
+ * custom content.
+ */
+ protected abstract SurfaceViewContainer getSurfaceViewContainer();
+
+ /** Returns the {@link FrameLayout} container which holds the currently set template. */
+ protected abstract ViewGroup getTemplateContainer();
+
+ /**
+ * Returns the minimum top padding to use when laying out the UI.
+ *
+ * <p>This is used to ensure there is some spacing from top of the screen to the UI when there is
+ * no status bar (i.e. widescreen).
+ */
+ protected abstract int getMinimumTopPadding();
+
+ /**
+ * Returns a {@link TemplateTransitionManager} responsible for handling transitions between
+ * presenters
+ */
+ protected abstract TemplateTransitionManager getTransitionManager();
+
+ /** Returns the current {@link TemplateContext} or {@code null} if one has not been set. */
+ @Nullable
+ protected TemplateContext getTemplateContext() {
+ return mTemplateContext;
+ }
+
+ /**
+ * Returns a {@link SurfaceProvider} which can be used to retrieve the {@link
+ * android.view.Surface} that 3p apps can use to draw custom content.
+ */
+ public SurfaceProvider getSurfaceProvider() {
+ return getSurfaceViewContainer();
+ }
+
+ /**
+ * Sets the parent {@link Lifecycle} for this view.
+ *
+ * <p>This is normally the activity or fragment the view is attached to.
+ */
+ public void setParentLifecycle(Lifecycle parentLifecycle) {
+ mParentLifecycle = parentLifecycle;
+ }
+
+ /** Returns the parent {@link Lifecycle}. */
+ protected @Nullable Lifecycle getParentLifecycle() {
+ return mParentLifecycle;
+ }
+
+ /** Sets the {@link TemplateContext} for this view. */
+ public void setTemplateContext(TemplateContext templateContext) {
+ mTemplateContext = TemplateContext.from(templateContext, getContext());
+ }
+
+ /** Stores the window insets to apply to templates. */
+ public void setWindowInsets(WindowInsets windowInsets) {
+ mWindowInsets = windowInsets;
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.applyWindowInsets(windowInsets, getMinimumTopPadding());
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+ dispatchTouchFocusEvent();
+ if (mCurrentPresenter != null && mCurrentPresenter.onKeyUp(keyCode, keyEvent)) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, keyEvent);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ dispatchTouchFocusEvent();
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent motionEvent) {
+ dispatchTouchFocusEvent();
+ return super.onGenericMotionEvent(motionEvent);
+ }
+
+ @Override
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mParentLifecycle != null) {
+ initLifecycleObserver(mParentLifecycle);
+ }
+
+ ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+ viewTreeObserver.addOnWindowFocusChangeListener(mOnWindowFocusChangeListener);
+ viewTreeObserver.addOnPreDrawListener(mOnPreDrawListener);
+ }
+
+ /** Returns the presenter currently attached to this view. */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @Nullable
+ public TemplatePresenter getCurrentPresenter() {
+ return mCurrentPresenter;
+ }
+
+ /** Sets the {@link Template} to display in the view, or {@code null} to display nothing. */
+ @MainThread
+ public void setTemplate(TemplateWrapper templateWrapper) {
+ ThreadUtils.ensureMainThread();
+
+ // First convert the template to another template type if needed.
+ templateWrapper =
+ TemplateConverterRegistry.get().maybeConvertTemplate(getContext(), templateWrapper);
+
+ TemplatePresenter previousPresenter = mCurrentPresenter;
+ if (mCurrentPresenter != null) {
+ TemplatePresenter presenter = mCurrentPresenter;
+
+ Template template = templateWrapper.getTemplate();
+
+ // Allow the existing presenter to update the views if:
+ // 1) Both the previous and the new template are of the same class.
+ // 2) The new template is a refresh OR the presenter handles the template change
+ // animation.
+ boolean updatePresenter = presenter.getTemplate().getClass().equals(template.getClass());
+ updatePresenter &= templateWrapper.isRefresh() || presenter.handlesTemplateChangeAnimation();
+ if (updatePresenter) {
+ updatePresenter(presenter, templateWrapper);
+ return;
+ }
+
+ // The current presenter is not of the same type as the given template, so remove it. We
+ // will create a new presenter below and re-add it if needed.
+ // TODO(b/151953922): Test the ordering of pause, remove view, destroy.
+ pausePresenter(presenter);
+ stopPresenter(presenter);
+ destroyPresenter(presenter);
+ mCurrentPresenter = null;
+ }
+
+ TemplatePresenter presenter = createPresenter(templateWrapper);
+ mCurrentPresenter = presenter;
+ transition(presenter, previousPresenter);
+
+ if (presenter != null) {
+ presenter.setDefaultFocus();
+ }
+ }
+
+ private void transition(@Nullable TemplatePresenter to, @Nullable TemplatePresenter from) {
+ if (to != null) {
+ getTransitionManager()
+ .transition(getTemplateContainer(), getSurfaceViewContainer(), to, from);
+ } else {
+ getSurfaceViewContainer().setVisibility(GONE);
+ View previousView = from == null ? null : from.getView();
+ if (previousView != null) {
+ getTemplateContainer().removeView(previousView);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link WindowInsets} to apply to the templates presented by the view or {@code
+ * null} if not set.
+ *
+ * @see #setWindowInsets(WindowInsets)
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @Nullable
+ public WindowInsets getWindowInsets() {
+ return mWindowInsets;
+ }
+
+ @Override
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public void onDetachedFromWindow() {
+ ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+ viewTreeObserver.removeOnWindowFocusChangeListener(mOnWindowFocusChangeListener);
+ viewTreeObserver.removeOnPreDrawListener(mOnPreDrawListener);
+
+ // Stop the presenter, since its view is no longer visible.
+ TemplatePresenter presenter = mCurrentPresenter;
+ if (presenter != null) {
+ stopPresenter(presenter);
+ }
+
+ if (mLifecycleObserver != null && mParentLifecycle != null) {
+ mParentLifecycle.removeObserver(mLifecycleObserver);
+ mLifecycleObserver = null;
+ }
+
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * Let any listeners know that an(y) UI element within the template view has been interacted on,
+ * either via touch or focus events.
+ */
+ private void dispatchTouchFocusEvent() {
+ TemplateContext context = mTemplateContext;
+ if (context != null) {
+ context.getEventManager().dispatchEvent(TEMPLATE_TOUCHED_OR_FOCUSED);
+ }
+ }
+
+ /**
+ * Let any listeners know that the window that contains the template view has changed its focus
+ * state.
+ */
+ private void dispatchWindowFocusEvent() {
+ TemplateContext context = mTemplateContext;
+ if (context != null) {
+ context.getEventManager().dispatchEvent(WINDOW_FOCUS_CHANGED);
+ }
+ }
+
+ /** Updates the given presenter with the data from the given template. */
+ private static void updatePresenter(
+ TemplatePresenter presenter, TemplateWrapper templateWrapper) {
+ Preconditions.checkState(
+ presenter.getTemplate().getClass().equals(templateWrapper.getTemplate().getClass()));
+
+ L.d(LogTags.TEMPLATE, "Updating presenter: %s", presenter);
+ presenter.setTemplate(templateWrapper);
+ }
+
+ /** Pauses the given presenter. */
+ private static void pausePresenter(TemplatePresenter presenter) {
+ L.d(LogTags.TEMPLATE, "Pausing presenter: %s", presenter);
+
+ State currentState = presenter.getLifecycle().getCurrentState();
+ if (currentState.isAtLeast(State.RESUMED)) {
+ presenter.onPause();
+ }
+ }
+
+ /** Stops the given presenter. */
+ private static void stopPresenter(TemplatePresenter presenter) {
+ L.d(LogTags.TEMPLATE, "Stopping presenter: %s", presenter);
+
+ State currentState = presenter.getLifecycle().getCurrentState();
+ if (currentState.isAtLeast(State.STARTED)) {
+ presenter.onStop();
+ }
+ }
+
+ /** Destroys the given presenter. */
+ private static void destroyPresenter(TemplatePresenter presenter) {
+ L.d(LogTags.TEMPLATE, "Destroying presenter: %s", presenter);
+
+ presenter.onDestroy();
+ }
+
+ /**
+ * Creates and starts a new presenter for the given template or {@code null} if a presenter could
+ * not be found for it.
+ */
+ @Nullable
+ private TemplatePresenter createPresenter(TemplateWrapper templateWrapper) {
+ if (mTemplateContext == null) {
+ throw new IllegalStateException(
+ "templateContext is null when attempting to create a presenter");
+ }
+
+ TemplatePresenter presenter =
+ TemplatePresenterRegistry.get().createPresenter(mTemplateContext, templateWrapper);
+ if (presenter == null) {
+ L.w(
+ LogTags.TEMPLATE,
+ "No presenter available for template type: %s",
+ templateWrapper.getTemplate().getClass().getSimpleName());
+ return null;
+ }
+
+ L.d(LogTags.TEMPLATE, "Creating new presenter: %s", presenter);
+ presenter.onCreate();
+
+ if (mParentLifecycle != null) {
+ // Only start and resume it if our parent parent lifecycle is in those states. If not,
+ // we will
+ // switch to the state when/if the parent lifecycle reaches it later on.
+ State parentState = mParentLifecycle.getCurrentState();
+ if (parentState.isAtLeast(State.STARTED)) {
+ presenter.onStart();
+ }
+ if (parentState.isAtLeast(State.RESUMED)) {
+ presenter.onResume();
+ }
+ }
+
+ if (mWindowInsets != null) {
+ presenter.applyWindowInsets(mWindowInsets, getMinimumTopPadding());
+ }
+ return presenter;
+ }
+
+ /**
+ * Instantiates a parent lifecycle observer that forwards the relevant events to the current
+ * presenter.
+ */
+ private void initLifecycleObserver(Lifecycle parentLifecycle) {
+ mLifecycleObserver =
+ new DefaultLifecycleObserver() {
+ @Override
+ public void onStart(LifecycleOwner lifecycleOwner) {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onStart();
+ }
+ }
+
+ @Override
+ public void onStop(LifecycleOwner lifecycleOwner) {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onStop();
+ }
+ }
+
+ @Override
+ public void onResume(LifecycleOwner lifecycleOwner) {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onResume();
+ }
+ }
+
+ @Override
+ public void onPause(LifecycleOwner lifecycleOwner) {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onPause();
+ }
+ }
+
+ @Override
+ public void onDestroy(LifecycleOwner lifecycleOwner) {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onDestroy();
+ }
+ }
+ };
+ parentLifecycle.addObserver(mLifecycleObserver);
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java
new file mode 100644
index 0000000..ab61aa9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import static com.android.car.libraries.apphost.template.view.model.ActionStripWrapper.INVALID_FOCUSED_ACTION_INDEX;
+
+import android.annotation.SuppressLint;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnGenericMotionListener;
+import android.view.View.OnTouchListener;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import com.android.car.libraries.apphost.common.MapGestureManager;
+import com.android.car.libraries.apphost.common.SurfaceCallbackHandler;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+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 java.util.ArrayList;
+import java.util.List;
+
+/** A class that manages responses to the user's pan and zoom actions. */
+public class PanZoomManager implements OnGenericMotionListener, OnTouchListener {
+ /** A delegate class that responds to {@link PanZoomManager}'s actions and queries. */
+ public interface Delegate {
+ /** Called when the pan mode state changes. */
+ void onPanModeChanged(boolean isInPanMode);
+ }
+
+ private final TemplateContext mTemplateContext;
+
+ /** A delegate that responds to {@link PanZoomManager}'s actions and queries. */
+ private final Delegate mDelegate;
+
+ /** Gesture manager that handles gestures in map-based template presenters. */
+ private final MapGestureManager mMapGestureManager;
+
+ /** The amount in pixels to pan with a rotary nudge. */
+ private final float mRotaryNudgePanPixels;
+
+ /**
+ * Indicates the car app is in the pan mode.
+ *
+ * <p>In the pan mode, the pan UI and the map action strip are displayed, and other components
+ * such as the routing card and action strip are hidden.
+ */
+ private boolean mIsInPanMode;
+
+ /** Indicates whether the pan manager is enabled or not. */
+ private boolean mIsEnabled;
+
+ /** Construct a new instance of {@link PanZoomManager}. */
+ public PanZoomManager(
+ TemplateContext templateContext,
+ Delegate delegate,
+ long touchUpdateThresholdMillis,
+ float rotaryNudgePanPixels) {
+ mTemplateContext = templateContext;
+ mDelegate = delegate;
+ mMapGestureManager = new MapGestureManager(templateContext, touchUpdateThresholdMillis);
+ mRotaryNudgePanPixels = rotaryNudgePanPixels;
+ }
+
+ @Override
+ public boolean onGenericMotion(View v, MotionEvent event) {
+ // If we are not in the pan mode or the pan manager is disabled, do not intercept the motion
+ // events. Also, do not intercept rotary controller scrolls.
+ if (!mIsInPanMode || !mIsEnabled || event.getAction() == MotionEvent.ACTION_SCROLL) {
+ return false;
+ }
+
+ handleGesture(event);
+ return true;
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // Handle gestures only when the pan manager is enabled.
+ if (!mIsEnabled) {
+ return false;
+ }
+
+ handleGesture(event);
+ return true;
+ }
+
+ /** Handles the gesture from the given motion event. */
+ public void handleGesture(MotionEvent event) {
+ mMapGestureManager.handleGesture(event);
+ }
+
+ /**
+ * Handles the rotary inputs by translating them to pan events, if appropriate.
+ *
+ * @return {@code true} if the input was handled, {@code false} otherwise.
+ */
+ public boolean handlePanEventsIfNeeded(int keyCode) {
+ // When in the pan mode, use the rotary nudges for map panning.
+ if (mIsInPanMode) {
+ SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+ if (!handler.canStartNewGesture()) {
+ return false;
+ }
+
+ float distanceX = 0f;
+ float distanceY = 0f;
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ distanceX = -mRotaryNudgePanPixels;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ distanceX = mRotaryNudgePanPixels;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+ distanceY = -mRotaryNudgePanPixels;
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ distanceY = mRotaryNudgePanPixels;
+ }
+
+ if (distanceX != 0 || distanceY != 0) {
+ // each rotary nudge is treated as a single gesture.
+ handler.onScroll(distanceX, distanceY);
+ mTemplateContext
+ .getTelemetryHandler()
+ .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.ROTARY_PAN));
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Enables or disables the pan manager.
+ *
+ * <p>If the pan mode was active when the pan manager is disabled, it will become inactive.
+ */
+ public void setEnabled(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+
+ if (mIsInPanMode && !isEnabled) {
+ // If the user is in the pan mode but the feature is disabled, exit pan mode.
+ setPanMode(false);
+ }
+ }
+
+ /**
+ * Returns the map {@link ActionStripWrapper} from the given {@link ActionStrip}.
+ *
+ * <p>This method contains the special handling logic for {@link Action#PAN} buttons.
+ */
+ public ActionStripWrapper getMapActionStripWrapper(
+ TemplateContext templateContext, ActionStrip actionStrip) {
+ List<ActionWrapper> mapActions = new ArrayList<>();
+ int focusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+
+ int actionIndex = 0;
+ for (Action action : actionStrip.getActions()) {
+ ActionWrapper.Builder builder = new ActionWrapper.Builder(action);
+ if (action.getType() == Action.TYPE_PAN) {
+ if (templateContext.getInputConfig().hasTouch()) {
+ // Hide the pan button in touch screens.
+ continue;
+ } else {
+ // React to the pan button in the rotary and touchpad mode.
+ builder.setOnClickListener(() -> setPanMode(!mIsInPanMode));
+
+ // Keep the focus on the pan button if the user uses a touchpad and is in the pan mode,
+ // because the user cannot move the focus with the touchpad in the pan mode.
+ if (mIsInPanMode && templateContext.getInputConfig().hasTouchpadForUiNavigation()) {
+ focusedActionIndex = actionIndex;
+ }
+ }
+ }
+ mapActions.add(builder.build());
+ actionIndex++;
+ }
+
+ return new ActionStripWrapper.Builder(mapActions)
+ .setFocusedActionIndex(focusedActionIndex)
+ .build();
+ }
+
+ /** Returns whether the pan mode is active or not. */
+ public boolean isInPanMode() {
+ return mIsInPanMode;
+ }
+
+ /**
+ * Sets the pan mode.
+ *
+ * <p>When the pan mode changes, the delegate will be notified of the change.
+ */
+ @VisibleForTesting
+ void setPanMode(boolean isInPanMode) {
+ boolean panModeChanged = mIsInPanMode != isInPanMode;
+ mIsInPanMode = isInPanMode;
+
+ if (panModeChanged) {
+ mDelegate.onPanModeChanged(isInPanMode);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceProvider.java
new file mode 100644
index 0000000..d2b75f4
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceProvider.java
@@ -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.apphost.view;
+
+import android.view.Surface;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A provider for {@link Surface}s that can be used by 3p apps to render custom content. */
+public interface SurfaceProvider {
+ /** Listener interface for the {@link SurfaceProvider}. */
+ interface SurfaceProviderListener {
+ /**
+ * Notifies the listener that the surface was created.
+ *
+ * <p>Clients should use this callback to prepare for drawing.
+ */
+ void onSurfaceCreated();
+
+ /**
+ * Notifies the listener that the surface had some structural changes (format or size).
+ *
+ * <p>This is called at least once after {@link #onSurfaceCreated()}. Clients must update the
+ * imagery on the surface.
+ */
+ void onSurfaceChanged();
+
+ /**
+ * Notifies the listener that the surface is being destroyed.
+ *
+ * <p>After returning from this call clients should not try to access the surface anymore. The
+ * {@link SurfaceProvider} is still valid after this call and may be followed by a {@link
+ * #onSurfaceCreated()}.
+ */
+ void onSurfaceDestroyed();
+
+ /** Notifies the listener about a surface scroll touch event. */
+ void onSurfaceScroll(float distanceX, float distanceY);
+
+ /** Notifies the listener about a surface fling touch event. */
+ void onSurfaceFling(float velocityX, float velocityY);
+
+ /** Notifies the listener about a surface scale touch event. */
+ void onSurfaceScale(float focusX, float focusY, float scaleFactor);
+ }
+
+ /**
+ * Sets the listener which is called when {@link Surface} changes such as on creation, destruction
+ * or due to structural changes.
+ */
+ void setListener(@Nullable SurfaceProviderListener listener);
+
+ /** Returns the {@link Surface} that this provider manages. */
+ @Nullable Surface getSurface();
+
+ /** Returns the width of the surface,m in pixels. */
+ int getWidth();
+
+ /** Returns the height of the surface, in pixels. */
+ int getHeight();
+
+ /** The screen density expressed as dots-per-inch. */
+ int getDpi();
+
+ SurfaceProvider EMPTY =
+ new SurfaceProvider() {
+ @Override
+ public void setListener(@Nullable SurfaceProviderListener listener) {}
+
+ @Override
+ @Nullable
+ public Surface getSurface() {
+ return null;
+ }
+
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getDpi() {
+ return 0;
+ }
+ };
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceViewContainer.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceViewContainer.java
new file mode 100644
index 0000000..53d6fa0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/SurfaceViewContainer.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import androidx.annotation.VisibleForTesting;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Container of the {@link SurfaceView} which 3p apps can use to render custom content. For example,
+ * navigation apps can use it to draw a map.
+ */
+public class SurfaceViewContainer extends SurfaceView implements SurfaceProvider {
+ /** A listener for changes to {@link SurfaceView}. */
+ @Nullable private SurfaceProviderListener mListener;
+
+ /** Indicates whether the surface is ready for use. */
+ private boolean mIsSurfaceReady;
+
+ private final SurfaceHolder.Callback mSurfaceHolderCallback =
+ new SurfaceHolder.Callback() {
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mIsSurfaceReady = true;
+ notifySurfaceCreated();
+ }
+
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ notifySurfaceChanged();
+ }
+
+ @SuppressWarnings("nullness") // suppress under initialization warning for this
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mIsSurfaceReady = false;
+ notifySurfaceDestroyed();
+ }
+ };
+
+ /** Returns an instance of {@link SurfaceViewContainer}. */
+ public SurfaceViewContainer(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Returns an instance of {@link SurfaceViewContainer} with the given attribute set.
+ *
+ * @see android.view.View#View(Context, AttributeSet)
+ */
+ public SurfaceViewContainer(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Returns an instance of {@link SurfaceViewContainer} with the given attribute set and default
+ * style attribute.
+ *
+ * @see android.view.View#View(Context, AttributeSet, int)
+ */
+ public SurfaceViewContainer(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ @Nullable
+ public Surface getSurface() {
+ if (!mIsSurfaceReady) {
+ L.v(LogTags.TEMPLATE, "Surface is not ready for use");
+ return null;
+ }
+
+ return getHolder().getSurface();
+ }
+
+ @Override
+ public int getDpi() {
+ if (!mIsSurfaceReady) {
+ return 0;
+ }
+
+ return getResources().getDisplayMetrics().densityDpi;
+ }
+
+ @Override
+ public void setListener(@Nullable SurfaceProviderListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getHolder().addCallback(mSurfaceHolderCallback);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getHolder().removeCallback(mSurfaceHolderCallback);
+ }
+
+ /** Returns whether the surface is ready to be used. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public boolean isSurfaceReady() {
+ return mIsSurfaceReady;
+ }
+
+ private void notifySurfaceCreated() {
+ if (mListener != null) {
+ mListener.onSurfaceCreated();
+ }
+ }
+
+ private void notifySurfaceChanged() {
+ if (mListener != null) {
+ mListener.onSurfaceChanged();
+ }
+ }
+
+ private void notifySurfaceDestroyed() {
+ if (mListener != null) {
+ mListener.onSurfaceDestroyed();
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverter.java
new file mode 100644
index 0000000..01f843c
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import android.content.Context;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import java.util.Collection;
+
+/**
+ * Represents a type that can convert a {@link Template} instance into another type of template.
+ *
+ * <p>{@link TemplateConverter}s can be taken advantage to do N:1 conversions between template
+ * types. This allows converting different templates that are similar but for which we want
+ * different types in the client API to a common template that can be used internally be a single
+ * presenter, thus avoiding duplicating the presenter code.
+ */
+public interface TemplateConverter {
+
+ /** Changes the template instance in the template wrapper, if a mapping is necessary. */
+ TemplateWrapper maybeConvertTemplate(Context context, TemplateWrapper templateWrapper);
+
+ /** Returns the list of template types this converter supports. */
+ Collection<Class<? extends Template>> getSupportedTemplates();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverterRegistry.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverterRegistry.java
new file mode 100644
index 0000000..ba976cc
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateConverterRegistry.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.apphost.view;
+
+import android.content.Context;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A registry of {@link TemplateConverter} instances.
+ *
+ * <p>It is implemented as a {@link TemplateConverter} that wraps N other {@link
+ * TemplateConverter}s.
+ */
+public class TemplateConverterRegistry implements TemplateConverter {
+ private static final TemplateConverterRegistry INSTANCE = new TemplateConverterRegistry();
+
+ private final Map<Class<? extends Template>, TemplateConverter> mRegistry = new HashMap<>();
+ private final Set<Class<? extends Template>> mSupportedTemplates = new HashSet<>();
+
+ /** Returns a singleton instance of the {@link TemplateConverterRegistry}. */
+ public static TemplateConverterRegistry get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public TemplateWrapper maybeConvertTemplate(Context context, TemplateWrapper templateWrapper) {
+ TemplateConverter converter = mRegistry.get(templateWrapper.getTemplate().getClass());
+ if (converter != null) {
+ return converter.maybeConvertTemplate(context, templateWrapper);
+ }
+ return templateWrapper;
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return Collections.unmodifiableCollection(mSupportedTemplates);
+ }
+
+ /** Registers the given {@link TemplateConverter}. */
+ public void register(TemplateConverter converter) {
+ for (Class<? extends Template> clazz : converter.getSupportedTemplates()) {
+ mRegistry.put(clazz, converter);
+ mSupportedTemplates.add(clazz);
+ }
+ }
+
+ private TemplateConverterRegistry() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenter.java
new file mode 100644
index 0000000..fa6dcd1
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenter.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowInsets;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.LifecycleOwner;
+import com.android.car.libraries.apphost.common.TemplateContext;
+
+/**
+ * A presenter is responsible for connecting a {@link Template} model with an Android {@link View}.
+ *
+ * <p>In <a href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel">MVVM</a>
+ * terms, the {@link Template} is both the view as well as the data model, the {@link View} owned by
+ * is the view, and {@link TemplatePresenter} is the controller which connects them together.
+ *
+ * <p>A presenter has a lifecycle, and extends {@link LifecycleOwner} to allow registering observers
+ * to it.
+ *
+ * <p>Note the presenter's started and stopped states are dependent upon the parent lifecycle
+ * owner's (e.g. the activity or fragment the template view is attached to). This means for example
+ * that if the owner is not started at the time it is created, the presenter won't be started
+ * either. Conversely, if the parent is stopped and then started, the presenter will also change
+ * states accordingly.
+ */
+public interface TemplatePresenter extends LifecycleOwner {
+
+ /**
+ * Sets the {@link Template} instance for this presenter.
+ *
+ * <p>If the new template is of the same type as the one currently set, implementations should try
+ * to diff the data to apply a minimal view update when it would otherwise cause undesirable
+ * performance or visible UI artifacts. For example, when updating a list view, the diffing logic
+ * should detect which specific items changed and only update those rather than doing a full
+ * update of all items, which is important if using a {@link
+ * androidx.recyclerview.widget.RecyclerView} that may have special animations for the different
+ * adapter update operations.
+ */
+ void setTemplate(TemplateWrapper templateWrapper);
+
+ /** Returns the {@link Template} instance set in the template wrapper. */
+ Template getTemplate();
+
+ /** Returns the {@link TemplateWrapper} instance set in the presenter. */
+ TemplateWrapper getTemplateWrapper();
+
+ /** Returns the {@link TemplateContext} set in the presenter. */
+ TemplateContext getTemplateContext();
+
+ /**
+ * Returns the {@link View} instance representing the UI to display for the currently set {@link
+ * Template}.
+ */
+ View getView();
+
+ /** Applies the given {@code windowInsets} to the appropriate views. */
+ void applyWindowInsets(WindowInsets windowInsets, int minimumTopPadding);
+
+ /** Sets the default focus of the presenter's UI in the rotary or touchpad mode. */
+ boolean setDefaultFocus();
+
+ /**
+ * Called when a key event was received while the presenter is currently visible.
+ *
+ * @return {@code true} if the presenter handled the key event, otherwise {@code false}.
+ */
+ boolean onKeyUp(int keyCode, KeyEvent keyEvent);
+
+ /**
+ * Called when the view tree is about to be drawn. At this point, all views in the tree have been
+ * measured and given a frame. Presenters can use this to adjust their scroll bounds or even to
+ * request a new layout before drawing occurs.
+ */
+ boolean onPreDraw();
+
+ /**
+ * Notifies that the presenter instance has been created and its view is about to be added to the
+ * template view's hierarchy as the currently visible one.
+ *
+ * <p>Presenters can implement any initialization logic in here.
+ */
+ void onCreate();
+
+ /**
+ * Notifies that the presenter instance has been destroyed, and removed from the template view's
+ * hierarchy.
+ *
+ * <p>Presenters can implement any cleanup logic in here.
+ */
+ void onDestroy();
+
+ /**
+ * Notifies that the presenter is visible to the user.
+ *
+ * <p>Presenters can use method to implement any logic that was stopped during {@link #onStop}.
+ */
+ void onStart();
+
+ /**
+ * Notifies that the presenter is not visible to the user.
+ *
+ * <p>Presenters can use method to stop any logic that is not needed when the presenter is not
+ * visible, e.g. to conserve resources.
+ */
+ void onStop();
+
+ /** Notifies that the presenter is actively running. */
+ void onResume();
+
+ /** Notifies that the presenter is not actively running but still visible. */
+ void onPause();
+
+ /** Returns whether this presenter handles its own template change animations. */
+ boolean handlesTemplateChangeAnimation();
+
+ /**
+ * Returns whether this presenter is considered a full screen template.
+ *
+ * <p>Map and navigation templates are not full screen as they leave the space for map to be
+ * shown, and the UI elements only cover a smaller portion of the car screen.
+ */
+ boolean isFullScreen();
+
+ /**
+ * Returns whether this presenter uses the surface accessible via a {@link
+ * androidx.car.app.SurfaceContainer}.
+ */
+ boolean usesSurface();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterFactory.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterFactory.java
new file mode 100644
index 0000000..cdcbcd2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import java.util.Collection;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A provider of {@link TemplatePresenter} instances. */
+public interface TemplatePresenterFactory {
+
+ /**
+ * Returns a new instance of a {@link TemplatePresenter} for the given template or {@code null} if
+ * a presenter for the template type could not be found.
+ */
+ @Nullable TemplatePresenter createPresenter(
+ TemplateContext templateContext, TemplateWrapper template);
+
+ /** Returns the collection of templates this factory supports. */
+ Collection<Class<? extends Template>> getSupportedTemplates();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterRegistry.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterRegistry.java
new file mode 100644
index 0000000..b86e2c4
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplatePresenterRegistry.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view;
+
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A registry of {@link TemplatePresenterFactory} instances.
+ *
+ * <p>It is implemented as a {@link TemplatePresenterFactory} that wraps N other factories.
+ */
+public class TemplatePresenterRegistry implements TemplatePresenterFactory {
+ private static final TemplatePresenterRegistry INSTANCE = new TemplatePresenterRegistry();
+
+ private final Map<Class<? extends Template>, TemplatePresenterFactory> mRegistry =
+ new HashMap<>();
+ private final Set<Class<? extends Template>> mSupportedTemplates = new HashSet<>();
+
+ /** Returns a singleton instance of the {@link TemplatePresenterRegistry}. */
+ public static TemplatePresenterRegistry get() {
+ return INSTANCE;
+ }
+
+ @Override
+ @Nullable
+ public TemplatePresenter createPresenter(
+ TemplateContext templateContext, TemplateWrapper templateWrapper) {
+
+ TemplatePresenterFactory factory = mRegistry.get(templateWrapper.getTemplate().getClass());
+ return factory == null ? null : factory.createPresenter(templateContext, templateWrapper);
+ }
+
+ @Override
+ public Collection<Class<? extends Template>> getSupportedTemplates() {
+ return Collections.unmodifiableCollection(mSupportedTemplates);
+ }
+
+ /** Registers the given {@link TemplatePresenterFactory}. */
+ public void register(TemplatePresenterFactory factory) {
+ for (Class<? extends Template> clazz : factory.getSupportedTemplates()) {
+ mRegistry.put(clazz, factory);
+ mSupportedTemplates.add(clazz);
+ }
+ }
+
+ /** Clears the registry of any registered factories. */
+ public void clear() {
+ mRegistry.clear();
+ mSupportedTemplates.clear();
+ }
+
+ private TemplatePresenterRegistry() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateTransitionManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateTransitionManager.java
new file mode 100644
index 0000000..2d241fb
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/TemplateTransitionManager.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.apphost.view;
+
+import android.view.View;
+import android.view.ViewGroup;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Controls transitions between different presenters. */
+public interface TemplateTransitionManager {
+ /** Handles the transition between one template presenter and another. */
+ void transition(
+ ViewGroup root, View surface, TemplatePresenter to, @Nullable TemplatePresenter from);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ActionButtonListParams.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ActionButtonListParams.java
new file mode 100644
index 0000000..b6a5efa
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ActionButtonListParams.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import android.graphics.Color;
+import androidx.annotation.ColorInt;
+
+/** Encapsulates parameters that configure the way action button list instances are rendered. */
+public class ActionButtonListParams {
+
+ private final int mMaxActions;
+ private final boolean mAllowOemReordering;
+ private final boolean mAllowOemColorOverride;
+ private final boolean mAllowAppColor;
+ @ColorInt private final int mSurroundingColor;
+
+ /** Returns a builder of {@link ActionButtonListParams}. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static Builder builder(ActionButtonListParams params) {
+ return new Builder()
+ .setMaxActions(params.getMaxActions())
+ .setOemReorderingAllowed(params.allowOemReordering())
+ .setOemColorOverrideAllowed(params.allowOemColorOverride())
+ .setAllowAppColor(params.allowAppColor())
+ .setSurroundingColor(params.getSurroundingColor());
+ }
+
+ /** Returns the maximum number of action buttons in the list. */
+ public int getMaxActions() {
+ return mMaxActions;
+ }
+ /**
+ * Returns the surrounding color against which the button will be displayed.
+ *
+ * <p>This color is used to compare the contrast between the surrounding color and the button
+ * background color.
+ *
+ * @see Builder#setSurroundingColor(int)
+ */
+ @ColorInt
+ public int getSurroundingColor() {
+ return mSurroundingColor;
+ }
+
+ /** Returns whether the button can have app-defined colors. */
+ public boolean allowAppColor() {
+ return mAllowAppColor;
+ }
+
+ /** Returns whether the buttons can be re-ordered by OEMs or not. */
+ public boolean allowOemReordering() {
+ return mAllowOemReordering;
+ }
+
+ /** Returns whether the button colors can be overridden by OEMs. */
+ public boolean allowOemColorOverride() {
+ return mAllowOemColorOverride;
+ }
+
+ private ActionButtonListParams(
+ int maxActions,
+ boolean allowOemReordering,
+ boolean allowOemColorOverride,
+ boolean allowAppColor,
+ @ColorInt int surroundingColor) {
+ mMaxActions = maxActions;
+ mAllowOemReordering = allowOemReordering;
+ mAllowOemColorOverride = allowOemColorOverride;
+ mAllowAppColor = allowAppColor;
+ mSurroundingColor = surroundingColor;
+ }
+
+ /** A builder of {@link ActionButtonListParams} instances. */
+ public static class Builder {
+ private int mMaxActions = 0;
+ private boolean mAllowOemReordering = false;
+ private boolean mAllowOemColorOverride = false;
+ private boolean mAllowAppColor = false;
+ @ColorInt private int mSurroundingColor = Color.TRANSPARENT;
+
+ /** Sets the maximum number of action buttons in the list. */
+ public Builder setMaxActions(int maxActions) {
+ mMaxActions = maxActions;
+ return this;
+ }
+
+ /** Sets whether the buttons can be re-ordered by OEMs or not. */
+ public Builder setOemReorderingAllowed(boolean allowOemReordering) {
+ mAllowOemReordering = allowOemReordering;
+ return this;
+ }
+
+ /** Sets whether the button colors can be overridden by OEMs. */
+ public Builder setOemColorOverrideAllowed(boolean allowOemColorOverride) {
+ mAllowOemColorOverride = allowOemColorOverride;
+ return this;
+ }
+
+ /** Sets whether the button can have app-defined colors. */
+ public Builder setAllowAppColor(boolean allowAppColor) {
+ mAllowAppColor = allowAppColor;
+ return this;
+ }
+
+ /**
+ * Sets the surrounding color against which the button will be displayed.
+ *
+ * <p>This color is used to compare the contrast between the surrounding color and the button
+ * background color.
+ *
+ * <p>By default, the surrounding color is assumed to be transparent.
+ */
+ public Builder setSurroundingColor(@ColorInt int surroundingColor) {
+ mSurroundingColor = surroundingColor;
+ return this;
+ }
+
+ /** Constructs a {@link ActionButtonListParams} instance defined by this builder. */
+ public ActionButtonListParams build() {
+ return new ActionButtonListParams(
+ mMaxActions,
+ mAllowOemReordering,
+ mAllowOemColorOverride,
+ mAllowAppColor,
+ mSurroundingColor);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextParams.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextParams.java
new file mode 100644
index 0000000..09bd11f
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextParams.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+
+/** Encapsulates parameters that configure the way car text instances are rendered. */
+public class CarTextParams {
+ /** Default params which should be used for most text in all templates. */
+ public static final CarTextParams DEFAULT =
+ new CarTextParams(
+ /* colorSpanConstraints= */ CarColorConstraints.NO_COLOR,
+ /* allowClickableSpans= */ false,
+ /* imageBoundingBox= */ null,
+ /* maxImages= */ 0,
+ // No need to pass icon tint since no images are allowed.
+ /* defaultIconTint= */ Color.TRANSPARENT,
+ /* backgroundColor= */ Color.TRANSPARENT,
+ /* ignoreAppIconTint= */ false);
+
+ @Nullable private final Rect mImageBoundingBox;
+ private final int mMaxImages;
+ @ColorInt private final int mDefaultIconTint;
+ private final boolean mIgnoreAppIconTint;
+ private final CarColorConstraints mColorSpanConstraints;
+ private final boolean mAllowClickableSpans;
+ @ColorInt private final int mBackgroundColor;
+
+ /** Returns a builder of {@link CarTextParams}. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static Builder builder(CarTextParams params) {
+ return new Builder()
+ .setColorSpanConstraints(params.getColorSpanConstraints())
+ .setAllowClickableSpans(params.getAllowClickableSpans())
+ .setImageBoundingBox(params.getImageBoundingBox())
+ .setMaxImages(params.getMaxImages())
+ .setDefaultIconTint(params.getDefaultIconTint())
+ .setBackgroundColor(params.getBackgroundColor())
+ .setIgnoreAppIconTint(params.ignoreAppIconTint());
+ }
+
+ /**
+ * Returns the bounding box for a span image.
+ *
+ * <p>Images are scaled to fit within this bounding box.
+ */
+ @Nullable
+ Rect getImageBoundingBox() {
+ return mImageBoundingBox;
+ }
+
+ /** Returns the maximum number of image spans to allow in the text. */
+ int getMaxImages() {
+ return mMaxImages;
+ }
+
+ /** Returns the constraints on the color spans in the text. */
+ CarColorConstraints getColorSpanConstraints() {
+ return mColorSpanConstraints;
+ }
+
+ /** Returns whether clickable spans are allowed in the text. */
+ boolean getAllowClickableSpans() {
+ return mAllowClickableSpans;
+ }
+
+ /**
+ * Returns the default tint color to apply to the icon if one is not specified explicitly.
+ *
+ * @see Builder#setDefaultIconTint(int)
+ */
+ @ColorInt
+ int getDefaultIconTint() {
+ return mDefaultIconTint;
+ }
+
+ /** Returns whether the app-provided icon tint should be ignored. */
+ public boolean ignoreAppIconTint() {
+ return mIgnoreAppIconTint;
+ }
+
+ /**
+ * Returns the background color against which the text will be displayed.
+ *
+ * @see Builder#setBackgroundColor(int)
+ */
+ @ColorInt
+ int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ private CarTextParams(
+ CarColorConstraints colorSpanConstraints,
+ boolean allowClickableSpans,
+ @Nullable Rect imageBoundingBox,
+ int maxImages,
+ @ColorInt int defaultIconTint,
+ @ColorInt int backgroundColor,
+ boolean ignoreAppIconTint) {
+ mColorSpanConstraints = colorSpanConstraints;
+ mAllowClickableSpans = allowClickableSpans;
+ mImageBoundingBox = imageBoundingBox;
+ mMaxImages = maxImages;
+ mDefaultIconTint = defaultIconTint;
+ mBackgroundColor = backgroundColor;
+ mIgnoreAppIconTint = ignoreAppIconTint;
+ }
+
+ /** A builder of {@link CarTextParams} instances. */
+ public static class Builder {
+ private CarColorConstraints mColorSpanConstraints = CarColorConstraints.NO_COLOR;
+ private boolean mAllowClickableSpans;
+ @Nullable private Rect mImageBoundingBox;
+ private int mMaxImages;
+ @ColorInt private int mDefaultIconTint = Color.TRANSPARENT;
+ @ColorInt private int mBackgroundColor = Color.TRANSPARENT;
+ private boolean mIgnoreAppIconTint;
+
+ /**
+ * Sets the constraints on the color spans in the text.
+ *
+ * <p>By default, no color spans are allowed in the text.
+ *
+ * @see #getColorSpanConstraints()
+ */
+ public Builder setColorSpanConstraints(CarColorConstraints colorSpanConstraints) {
+ mColorSpanConstraints = colorSpanConstraints;
+ return this;
+ }
+
+ /**
+ * Sets whether clickable spans are allowed in the text.
+ *
+ * <p>By default, no clickable spans are allowed in the text.
+ *
+ * @see #getAllowClickableSpans()
+ */
+ public Builder setAllowClickableSpans(boolean allowClickableSpans) {
+ mAllowClickableSpans = allowClickableSpans;
+ return this;
+ }
+
+ /**
+ * Sets the bounding box for the image spans.
+ *
+ * <p>By default, no bounding box is specified.
+ *
+ * @see #getImageBoundingBox()
+ */
+ public Builder setImageBoundingBox(@Nullable Rect imageBoundingBox) {
+ mImageBoundingBox = imageBoundingBox;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of image spans to allow for the text.
+ *
+ * <p>By default, no images are allowed in the text.
+ *
+ * @see #getMaxImages()
+ */
+ public Builder setMaxImages(int maxImages) {
+ mMaxImages = maxImages;
+ return this;
+ }
+
+ /**
+ * Sets the default tint to use for the images in the span that set their tint to {@link
+ * CarColor#DEFAULT}.
+ *
+ * <p>This tint may vary depending on where the spans are rendered, and can be specified here.
+ *
+ * <p>By default, this tint is transparent.
+ */
+ public Builder setDefaultIconTint(@ColorInt int defaultIconTint) {
+ mDefaultIconTint = defaultIconTint;
+ return this;
+ }
+
+ /** Determines if the app-provided icon tint should be ignored. */
+ public Builder setIgnoreAppIconTint(boolean ignoreAppIconTint) {
+ mIgnoreAppIconTint = ignoreAppIconTint;
+ return this;
+ }
+
+ /**
+ * Sets the background color against which the text will be displayed.
+ *
+ * <p>This color is used only for the color contrast check, and will not be applied on the text
+ * background.
+ *
+ * <p>By default, the background color is assumed to be transparent.
+ */
+ public Builder setBackgroundColor(@ColorInt int backgroundColor) {
+ mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /** Constructs a {@link CarTextParams} instance defined by this builder. */
+ public CarTextParams build() {
+ if (mImageBoundingBox == null && mMaxImages > 0) {
+ throw new IllegalStateException(
+ "A bounding box needs to be provided if images are allowed in the text");
+ }
+
+ return new CarTextParams(
+ mColorSpanConstraints,
+ mAllowClickableSpans,
+ mImageBoundingBox,
+ mMaxImages,
+ mDefaultIconTint,
+ mBackgroundColor,
+ mIgnoreAppIconTint);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextUtils.java
new file mode 100644
index 0000000..dc3ab47
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/CarTextUtils.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import static androidx.car.app.model.CarIconSpan.ALIGN_BASELINE;
+import static androidx.car.app.model.CarIconSpan.ALIGN_BOTTOM;
+import static androidx.car.app.model.CarIconSpan.ALIGN_CENTER;
+import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_CENTER_Y_INSIDE;
+import static com.android.car.libraries.apphost.view.common.ImageUtils.SCALE_INSIDE;
+import static java.util.Objects.requireNonNull;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.view.View;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarIconSpan;
+import androidx.car.app.model.CarSpan;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ClickableSpan;
+import androidx.car.app.model.Distance;
+import androidx.car.app.model.DistanceSpan;
+import androidx.car.app.model.DurationSpan;
+import androidx.car.app.model.ForegroundCarColorSpan;
+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.view.common.ImageUtils.ScaleType;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Utilities for handling {@link CarText} instances. */
+public class CarTextUtils {
+ /**
+ * An internal flag that indicates that the main text should be converted instead of a variant.
+ */
+ private static final int USE_MAIN_TEXT = -1;
+
+ /**
+ * Returns {@code true} if there is enough color contrast between all {@link
+ * ForegroundCarColorSpan}s in the given {@code carText} and the given {@code backgroundColor},
+ * otherwise {@code false}.
+ */
+ public static boolean checkColorContrast(
+ TemplateContext templateContext, CarText carText, @ColorInt int backgroundColor) {
+ List<CharSequence> texts = new ArrayList<>();
+ texts.add(carText.toCharSequence());
+ texts.addAll(carText.getVariants());
+
+ for (CharSequence text : texts) {
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+
+ for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) {
+ if (span instanceof ForegroundCarColorSpan) {
+ ForegroundCarColorSpan colorSpan = (ForegroundCarColorSpan) span;
+ CarColor foregroundCarColor = colorSpan.getColor();
+ if (!CarColorUtils.checkColorContrast(
+ templateContext, foregroundCarColor, backgroundColor)) {
+ return false;
+ }
+ }
+
+ if (span instanceof CarIconSpan) {
+ CarIconSpan carIconSpan = (CarIconSpan) span;
+ CarIcon icon = carIconSpan.getIcon();
+ if (icon != null) {
+ CarColor tint = icon.getTint();
+ if (tint != null
+ && !CarColorUtils.checkColorContrast(templateContext, tint, backgroundColor)) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a {@link CharSequence} from a {@link CarText} instance, with default {@link
+ * CarTextParams} that disallow images in text spans.
+ *
+ * @see #toCharSequenceOrEmpty(TemplateContext, CarText, CarTextParams)
+ */
+ public static CharSequence toCharSequenceOrEmpty(
+ TemplateContext templateContext, @Nullable CarText carText) {
+ return toCharSequenceOrEmpty(templateContext, carText, CarTextParams.DEFAULT);
+ }
+
+ /**
+ * Returns a {@link CharSequence} from a {@link CarText} instance, or an empty string if the input
+ * {@link CarText} is {@code null}.
+ */
+ public static CharSequence toCharSequenceOrEmpty(
+ TemplateContext templateContext, @Nullable CarText carText, CarTextParams params) {
+ return toCharSequenceOrEmpty(templateContext, carText, params, USE_MAIN_TEXT);
+ }
+
+ /**
+ * Returns a {@link CharSequence} from a {@link CarText} instance's variant at the given index, or
+ * an empty string if the input {@link CarText} is {@code null}.
+ *
+ * <p>if {@code variantIndex} is equal to {@link #USE_MAIN_TEXT}, the main text will be used.
+ */
+ public static CharSequence toCharSequenceOrEmpty(
+ TemplateContext templateContext,
+ @Nullable CarText carText,
+ CarTextParams params,
+ int variantIndex) {
+ CharSequence s = toCharSequence(templateContext, carText, params, variantIndex);
+ return s == null ? "" : s;
+ }
+
+ /**
+ * Reconstitutes a {@link CharSequence} from a {@link CarText} instance.
+ *
+ * <p>The client converts {@link CharSequence}s containing our custom car spans into {@link
+ * CarText}s that get marshaled to the host. These spans may contain standard images or icons in
+ * them. This method does the inverse conversion to generate char sequences that resolve the
+ * actual color resources to use when rendering the text.
+ */
+ @Nullable
+ private static CharSequence toCharSequence(
+ TemplateContext templateContext,
+ @Nullable CarText carText,
+ CarTextParams params,
+ int variantIndex) {
+ if (carText == null) {
+ return null;
+ }
+
+ CharSequence charSequence;
+ if (variantIndex == USE_MAIN_TEXT) {
+ charSequence = carText.toCharSequence();
+ } else {
+ List<CharSequence> variants = carText.getVariants();
+ if (variantIndex >= variants.size()) {
+ return null;
+ }
+ charSequence = variants.get(variantIndex);
+ }
+
+ if (!(charSequence instanceof Spanned)) {
+ // The API should always return a spanned, but in case it does not, we'll convert the
+ // char
+ // sequence to string and log a warning, to prevent an invalid cast exception that would
+ // crash the host.
+ L.w(LogTags.TEMPLATE, "Expecting spanned char sequence, will default to string");
+ return charSequence.toString();
+ }
+
+ Spanned spanned = (Spanned) charSequence;
+
+ // Separate style and replacement spans.
+ List<SpanWrapper> styleSpans = new ArrayList<>();
+ List<SpanWrapper> replacementSpans = new ArrayList<>();
+ for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) {
+ if (span instanceof CarSpan) {
+ CarSpan carSpan = (CarSpan) span;
+ SpanWrapper wrapper =
+ new SpanWrapper(
+ carSpan,
+ spanned.getSpanStart(span),
+ spanned.getSpanEnd(span),
+ spanned.getSpanFlags(span));
+ if (carSpan instanceof DistanceSpan
+ || carSpan instanceof DurationSpan
+ || carSpan instanceof CarIconSpan) {
+ replacementSpans.add(wrapper);
+ } else if (carSpan instanceof ForegroundCarColorSpan || carSpan instanceof ClickableSpan) {
+ styleSpans.add(wrapper);
+ } else {
+ L.e(LogTags.TEMPLATE, "Ignoring non unsupported span type: %s", span);
+ }
+ } else {
+ L.e(LogTags.TEMPLATE, "Ignoring span not of CarSpan type: %s", span);
+ }
+ }
+
+ // Apply style spans first, and then the replacement spans, in order to apply the correct
+ // styling span range to the replacement texts.
+ SpannableStringBuilder sb = new SpannableStringBuilder(charSequence.toString());
+ setStyleSpans(templateContext, styleSpans, sb, params);
+ setReplacementSpans(templateContext, replacementSpans, sb, params);
+
+ return sb;
+ }
+
+ /**
+ * Sets the spans that change the text style.
+ *
+ * <p>Supports {@link ForegroundCarColorSpan}. Unsupported spans are ignored.
+ */
+ private static void setStyleSpans(
+ TemplateContext templateContext,
+ List<SpanWrapper> styleSpans,
+ SpannableStringBuilder sb,
+ CarTextParams params) {
+ final CarColorConstraints colorSpanConstraints = params.getColorSpanConstraints();
+ final boolean allowClickableSpans = params.getAllowClickableSpans();
+ for (SpanWrapper wrapper : styleSpans) {
+ if (wrapper.mCarSpan instanceof ForegroundCarColorSpan) {
+ if (colorSpanConstraints.equals(CarColorConstraints.NO_COLOR)) {
+ L.w(LogTags.TEMPLATE, "Color spans not allowed, dropping color: %s", wrapper);
+ } else {
+ setColorSpan(
+ templateContext,
+ wrapper,
+ sb,
+ (ForegroundCarColorSpan) wrapper.mCarSpan,
+ colorSpanConstraints,
+ params.getBackgroundColor());
+ }
+ } else if (wrapper.mCarSpan instanceof ClickableSpan) {
+ if (!allowClickableSpans) {
+ L.w(LogTags.TEMPLATE, "Clickable spans not allowed, dropping click listener");
+ } else {
+ setClickableSpan(templateContext, wrapper, sb, (ClickableSpan) wrapper.mCarSpan);
+ }
+ } else {
+ L.e(LogTags.TEMPLATE, "Ignoring unsupported span: %s", wrapper);
+ }
+ }
+ }
+
+ /**
+ * Sets the spans that replace the text.
+ *
+ * <p>Supported spans are:
+ *
+ * <ul>
+ * <li>{@link DistanceSpan}
+ * <li>{@link DurationSpan}
+ * <li>{@link CarIconSpan}
+ * </ul>
+ *
+ * Unsupported spans are ignored.
+ *
+ * <p>Only spans that do not overlap with any other replacement spans will be applied.
+ */
+ private static void setReplacementSpans(
+ TemplateContext templateContext,
+ List<SpanWrapper> replacementSpans,
+ SpannableStringBuilder sb,
+ CarTextParams params) {
+ // Only apply disjoint spans.
+ List<SpanWrapper> spans = new ArrayList<>();
+ for (SpanWrapper wrapper : replacementSpans) {
+ if (isDisjoint(wrapper, replacementSpans)) {
+ spans.add(wrapper);
+ }
+ }
+
+ // Apply replacement spans from right to left.
+ Collections.sort(spans, (s1, s2) -> s2.mStart - s1.mStart);
+ final int maxImages = params.getMaxImages();
+ int imageCount = 0;
+ for (SpanWrapper wrapper : spans) {
+ CarSpan span = wrapper.mCarSpan;
+ if (span instanceof DistanceSpan) {
+ Distance distance = ((DistanceSpan) span).getDistance();
+ if (distance == null) {
+ L.w(LogTags.TEMPLATE, "Distance span is missing its distance: %s", span);
+ } else {
+ String distanceText =
+ DistanceUtils.convertDistanceToDisplayString(templateContext, distance);
+ sb.replace(wrapper.mStart, wrapper.mEnd, distanceText);
+ }
+ } else if (span instanceof DurationSpan) {
+ DurationSpan durationSpan = (DurationSpan) span;
+ String durationText =
+ DateTimeUtils.formatDurationString(
+ templateContext, Duration.ofSeconds(durationSpan.getDurationSeconds()));
+ sb.replace(wrapper.mStart, wrapper.mEnd, durationText);
+ } else if (span instanceof CarIconSpan) {
+ if (++imageCount > maxImages) {
+ L.w(LogTags.TEMPLATE, "Span over max image count, dropping image: %s", span);
+ } else {
+ setImageSpan(templateContext, params, wrapper, sb, (CarIconSpan) span);
+ }
+ } else {
+ L.e(
+ LogTags.TEMPLATE,
+ "Ignoring unsupported span found of type: %s",
+ span.getClass().getCanonicalName());
+ }
+ }
+ }
+
+ private static boolean isDisjoint(SpanWrapper wrapper, List<SpanWrapper> spans) {
+ for (SpanWrapper otherWrapper : spans) {
+ if (wrapper.equals(otherWrapper)) {
+ continue;
+ }
+
+ if (wrapper.mStart < otherWrapper.mEnd && wrapper.mEnd > otherWrapper.mStart) {
+ // The wrapper overlaps with the other wrapper.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void setImageSpan(
+ TemplateContext templateContext,
+ CarTextParams params,
+ SpanWrapper wrapper,
+ SpannableStringBuilder sb,
+ CarIconSpan carIconSpan) {
+ L.d(LogTags.TEMPLATE, "Converting car image: %s", wrapper);
+
+ Rect boundingBox = requireNonNull(params.getImageBoundingBox());
+
+ // Get the desired alignment for span coming from the app.
+ int alignment = carIconSpan.getAlignment();
+ if (alignment != ALIGN_BASELINE && alignment != ALIGN_BOTTOM && alignment != ALIGN_CENTER) {
+ L.e(LogTags.TEMPLATE, "Invalid alignment value, will default to baseline");
+ alignment = ALIGN_BASELINE;
+ }
+
+ // Determine how to scale the span image.
+ @ScaleType int scaleType;
+ int spanAlignment;
+ switch (alignment) {
+ case ALIGN_BOTTOM:
+ spanAlignment = ImageSpan.ALIGN_BOTTOM;
+ scaleType = SCALE_INSIDE;
+ break;
+ case ALIGN_CENTER:
+ // API 29 introduces a native ALIGN_BOTTOM ImageSpan option, but in order to supoprt
+ // APIs down to our minimum, we implement center alignment by using a
+ // center_y_inside
+ // scale type. This makes the icon be center aligned with the bounding box on the Y
+ // axis. Since our bounding boxes are configured to match the height of a line of
+ // text,
+ // makes the icon display as center aligned.
+ spanAlignment = ImageSpan.ALIGN_BOTTOM;
+ scaleType = SCALE_CENTER_Y_INSIDE;
+ break;
+ case ALIGN_BASELINE: // fall-through
+ default:
+ spanAlignment = ImageSpan.ALIGN_BASELINE;
+ scaleType = SCALE_INSIDE;
+ break;
+ }
+
+ CarIcon icon = carIconSpan.getIcon();
+ if (icon == null) {
+ L.e(LogTags.TEMPLATE, "Icon span doesn't contain an icon");
+ return;
+ }
+
+ ImageViewParams imageParams =
+ ImageViewParams.builder()
+ .setDefaultTint(params.getDefaultIconTint())
+ .setBackgroundColor(params.getBackgroundColor())
+ .setIgnoreAppTint(params.ignoreAppIconTint())
+ .build();
+ Bitmap bitmap =
+ ImageUtils.getBitmapFromIcon(
+ templateContext,
+ icon,
+ boundingBox.width(),
+ boundingBox.height(),
+ imageParams,
+ scaleType);
+ if (bitmap == null) {
+ L.e(LogTags.TEMPLATE, "Failed to get bitmap for icon span");
+ } else {
+ sb.setSpan(
+ new ImageSpan(templateContext, bitmap, spanAlignment),
+ wrapper.mStart,
+ wrapper.mEnd,
+ wrapper.mFlags);
+ }
+ }
+
+ private static void setColorSpan(
+ TemplateContext templateContext,
+ SpanWrapper wrapper,
+ SpannableStringBuilder sb,
+ ForegroundCarColorSpan carColorSpan,
+ CarColorConstraints colorSpanConstraints,
+ @ColorInt int backgroundColor) {
+ L.d(LogTags.TEMPLATE, "Converting foreground color span: %s", wrapper);
+
+ @ColorInt
+ int color =
+ CarColorUtils.resolveColor(
+ templateContext,
+ carColorSpan.getColor(),
+ /* isDark= */ false,
+ /* defaultColor= */ Color.WHITE,
+ colorSpanConstraints,
+ backgroundColor);
+ if (color == Color.WHITE) {
+ // If the ForegroundCarColoSpan is of the default color, we do not need to create a span
+ // as the view will just use its default color to render.
+ return;
+ }
+
+ try {
+ sb.setSpan(new ForegroundColorSpan(color), wrapper.mStart, wrapper.mEnd, wrapper.mFlags);
+ } catch (RuntimeException e) {
+ L.e(LogTags.TEMPLATE, e, "Failed to create foreground color span: %s", wrapper);
+ }
+ }
+
+ private static void setClickableSpan(
+ TemplateContext templateContext,
+ SpanWrapper wrapper,
+ SpannableStringBuilder sb,
+ ClickableSpan clickableSpan) {
+ L.d(LogTags.TEMPLATE, "Converting clickable span: %s", wrapper);
+
+ OnClickDelegate onClickDelegate = clickableSpan.getOnClickDelegate();
+ android.text.style.ClickableSpan span =
+ new android.text.style.ClickableSpan() {
+ @Override
+ public void onClick(@NonNull View widget) {
+ CommonUtils.dispatchClick(templateContext, onClickDelegate);
+ }
+ };
+
+ try {
+ sb.setSpan(span, wrapper.mStart, wrapper.mEnd, wrapper.mFlags);
+ } catch (RuntimeException e) {
+ L.e(LogTags.TEMPLATE, e, "Failed to create clickable span: %s", wrapper);
+ }
+ }
+
+ /** A simple convenient structure to contain a span with its associated metadata. */
+ private static class SpanWrapper {
+ CarSpan mCarSpan;
+ int mStart;
+ int mEnd;
+ int mFlags;
+
+ SpanWrapper(CarSpan carSpan, int start, int end, int flags) {
+ mCarSpan = carSpan;
+ mStart = start;
+ mEnd = end;
+ mFlags = flags;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "[" + mCarSpan + ": " + mStart + ", " + mEnd + ", flags: " + mFlags + "]";
+ }
+ }
+
+ private CarTextUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DateTimeUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DateTimeUtils.java
new file mode 100644
index 0000000..60fc021
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DateTimeUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.car.app.model.DateTimeWithZone;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+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.text.DateFormat;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.TimeZone;
+
+/** Utilities for formatting and manipulating dates and times. */
+@SuppressWarnings("NewApi") // java.time APIs used throughout are OK through de-sugaring.
+public class DateTimeUtils {
+
+ /** Returns a string from a duration in order to display it in the UI. */
+ public static String formatDurationString(TemplateContext context, Duration duration) {
+ long days = duration.toDays();
+ long hours = duration.minusDays(days).toHours();
+ long minutes = duration.minusDays(days).minusHours(hours).toMinutes();
+ HostResourceIds resIds = context.getHostResourceIds();
+
+ String result = "";
+ if (days > 0) {
+ if (hours == 0) {
+ result = context.getString(resIds.getDurationInDaysStringFormat(), days);
+ } else {
+ result = context.getString(resIds.getDurationInDaysAndHoursStringFormat(), days, hours);
+ }
+ } else if (hours > 0) {
+ if (minutes == 0) {
+ result = context.getString(resIds.getDurationInHoursStringFormat(), hours);
+ } else {
+ result =
+ context.getString(resIds.getDurationInHoursAndMinutesStringFormat(), hours, minutes);
+ }
+ } else {
+ result = context.getString(resIds.getDurationInMinutesStringFormat(), minutes);
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a string to display in the UI from an arrival time at a destination that may be in a
+ * different time zone than the one given by {@code currentZoneId).
+ *
+ * <p>If the time zone offset at the destination is not the same as the current time zone, an
+ * abbreviated time zone string is added, for example "5:38 PM PST".
+ */
+ public static String formatArrivalTimeString(
+ @NonNull TemplateContext context,
+ @NonNull DateTimeWithZone timeAtDestination,
+ @NonNull ZoneId currentZoneId) {
+ // Get the offsets for the current and destination time zones.
+ long destinationTimeUtcMillis = timeAtDestination.getTimeSinceEpochMillis();
+
+ int currentOffsetSeconds =
+ currentZoneId
+ .getRules()
+ .getOffset(Instant.ofEpochMilli(destinationTimeUtcMillis))
+ .getTotalSeconds();
+ int destinationOffsetSeconds = timeAtDestination.getZoneOffsetSeconds();
+
+ DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(context);
+
+ if (currentOffsetSeconds == destinationOffsetSeconds) {
+ // The destination is in the same time zone, so we don't need to display the time zone
+ // string.
+ dateFormat.setTimeZone(TimeZone.getTimeZone(currentZoneId));
+ return dateFormat.format(destinationTimeUtcMillis);
+ } else {
+ // The destination is in a different timezone: calculate its zone offset and use it to
+ // format
+ // the time.
+ TimeZone destinationZone;
+ try {
+ destinationZone = TimeZone.getTimeZone(ZoneOffset.ofTotalSeconds(destinationOffsetSeconds));
+ } catch (DateTimeException e) {
+ // This should never happen as the client library has checks to prevent this.
+ L.e(LogTags.TEMPLATE, e, "Failed to get destination time zone, will use system default");
+ destinationZone = TimeZone.getDefault();
+ }
+
+ dateFormat.setTimeZone(destinationZone);
+ String timeAtDestinationString = dateFormat.format(destinationTimeUtcMillis);
+ String zoneShortName = timeAtDestination.getZoneShortName();
+
+ if (TextUtils.isEmpty(zoneShortName)) {
+ // This should never really happen, the client library has checks to enforce a non
+ // empty
+ // zone name.
+ L.w(LogTags.TEMPLATE, "Time zone name is empty when formatting date time");
+ return timeAtDestinationString;
+ } else {
+ return context
+ .getResources()
+ .getString(
+ context.getHostResourceIds().getTimeAtDestinationWithTimeZoneStringFormat(),
+ timeAtDestinationString,
+ zoneShortName);
+ }
+ }
+ }
+
+ private DateTimeUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DistanceUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DistanceUtils.java
new file mode 100644
index 0000000..b93b285
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/DistanceUtils.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.apphost.view.common;
+
+import static androidx.car.app.model.Distance.UNIT_FEET;
+import static androidx.car.app.model.Distance.UNIT_KILOMETERS;
+import static androidx.car.app.model.Distance.UNIT_KILOMETERS_P1;
+import static androidx.car.app.model.Distance.UNIT_METERS;
+import static androidx.car.app.model.Distance.UNIT_MILES;
+import static androidx.car.app.model.Distance.UNIT_MILES_P1;
+import static androidx.car.app.model.Distance.UNIT_YARDS;
+
+import androidx.car.app.model.Distance;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import java.text.DecimalFormat;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/** Utilities for handling {@link Distance} instances. */
+public class DistanceUtils {
+
+ private static final DecimalFormat FORMAT_OPTIONAL_TENTH = new DecimalFormat("#0.#");
+ private static final DecimalFormat FORMAT_MANDATORY_TENTH = new DecimalFormat("#0.0");
+
+ /** Converts a {@link Distance} to a display string for the UI. */
+ @NonNull
+ public static String convertDistanceToDisplayString(
+ @NonNull TemplateContext context, @NonNull Distance distance) {
+ int displayUnit = distance.getDisplayUnit();
+ HostResourceIds resIds = context.getHostResourceIds();
+
+ String formattedDistance = convertDistanceToDisplayStringNoUnit(context, distance);
+ switch (displayUnit) {
+ case UNIT_METERS:
+ return context.getString(resIds.getDistanceInMetersStringFormat(), formattedDistance);
+ case UNIT_KILOMETERS:
+ case UNIT_KILOMETERS_P1:
+ return context.getString(resIds.getDistanceInKilometersStringFormat(), formattedDistance);
+ case UNIT_FEET:
+ return context.getString(resIds.getDistanceInFeetStringFormat(), formattedDistance);
+ case UNIT_MILES:
+ case UNIT_MILES_P1:
+ return context.getString(resIds.getDistanceInMilesStringFormat(), formattedDistance);
+ case UNIT_YARDS:
+ return context.getString(resIds.getDistanceInYardsStringFormat(), formattedDistance);
+ default:
+ throw new UnsupportedOperationException("Unsupported distance unit type: " + displayUnit);
+ }
+ }
+
+ /** Converts a {@link Distance} to a display string without units. */
+ @NonNull
+ public static String convertDistanceToDisplayStringNoUnit(
+ @NonNull TemplateContext context, @NonNull Distance distance) {
+ int displayUnit = distance.getDisplayUnit();
+ double displayDistance = distance.getDisplayDistance();
+ DecimalFormat format =
+ (displayUnit == Distance.UNIT_KILOMETERS_P1 || displayUnit == Distance.UNIT_MILES_P1)
+ ? FORMAT_MANDATORY_TENTH
+ : FORMAT_OPTIONAL_TENTH;
+ return format.format(displayDistance);
+ }
+
+ /** Converts {@link Distance} to meters. */
+ public static int getMeters(Distance distance) {
+ int displayUnit = distance.getDisplayUnit();
+ switch (displayUnit) {
+ case UNIT_METERS:
+ return (int) Math.round(distance.getDisplayDistance());
+ case UNIT_KILOMETERS:
+ case UNIT_KILOMETERS_P1:
+ return (int) Math.round(distance.getDisplayDistance() * 1000.0d);
+ case UNIT_FEET:
+ return (int) Math.round(distance.getDisplayDistance() * 0.3048d);
+ case UNIT_MILES:
+ case UNIT_MILES_P1:
+ return (int) Math.round(distance.getDisplayDistance() * 1609.34d);
+ case UNIT_YARDS:
+ return (int) Math.round(distance.getDisplayDistance() * 0.9144d);
+ default:
+ throw new UnsupportedOperationException("Unsupported distance unit type: " + displayUnit);
+ }
+ }
+
+ private DistanceUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageUtils.java
new file mode 100644
index 0000000..c51bea5
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageUtils.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import static android.graphics.Color.TRANSPARENT;
+
+import static androidx.core.graphics.drawable.IconCompat.TYPE_RESOURCE;
+import static androidx.core.graphics.drawable.IconCompat.TYPE_URI;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+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.CarIconConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.common.ImageViewParams.ImageLoadCallback;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.CustomTarget;
+import com.bumptech.glide.request.target.Target;
+import com.bumptech.glide.request.transition.Transition;
+
+import java.util.function.Consumer;
+
+/** Assorted image utilities. */
+public final class ImageUtils {
+
+ /** Represents different ways of scaling bitmaps. */
+ @IntDef(
+ value = {
+ SCALE_FIT_CENTER,
+ SCALE_CENTER_Y_INSIDE,
+ SCALE_INSIDE,
+ SCALE_CENTER_XY_INSIDE,
+ })
+ public @interface ScaleType {}
+
+ /**
+ * Scales an image so that it fits centered within a bounding box, while maintaining its aspect
+ * ratio, and ensuring that at least one of the axis will match exactly the size of the bounding
+ * box. This means images may be down-scaled or up-scaled. The smaller dimension of the image
+ * will be centered within the bounding box.
+ */
+ @ScaleType public static final int SCALE_FIT_CENTER = 0;
+
+ /**
+ * This scale type is similar to {@link #SCALE_INSIDE} with the difference that the resulting
+ * bitmap will always have a height equals to the bounding box's, and the image will be drawn
+ * center-aligned vertically if smaller than the bounding box height, with the space at either
+ * side padded with transparent pixels.
+ */
+ @ScaleType public static final int SCALE_CENTER_Y_INSIDE = 1;
+
+ /**
+ * Scales an image so that it fits within a bounding box, while maintaining its aspect ratio,
+ * but images smaller than the bounding box do not get up-scaled.
+ */
+ @ScaleType public static final int SCALE_INSIDE = 2;
+
+ /**
+ * Similar to {@link #SCALE_FIT_CENTER} but the resulting bitmap never be up-scaled, only
+ * down-scaled (if needed).
+ */
+ @ScaleType public static final int SCALE_CENTER_XY_INSIDE = 3;
+
+ // Suppressing nullness check because AndroidX @Nullable can't be used to annotate generic types
+ @SuppressWarnings("nullness:argument")
+ private static class ImageTarget extends CustomTarget<Drawable> {
+ private final Consumer<Drawable> mImageTarget;
+
+ ImageTarget(int width, int height, Consumer<Drawable> imageTarget) {
+ super(width, height);
+ this.mImageTarget = imageTarget;
+ }
+
+ @Override
+ public void onLoadFailed(@Nullable Drawable errorDrawable) {
+ mImageTarget.accept(errorDrawable);
+ }
+
+ @Override
+ public void onResourceReady(
+ @NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
+ mImageTarget.accept(resource);
+ }
+
+ @Override
+ public void onLoadCleared(@Nullable Drawable placeholder) {
+ mImageTarget.accept(placeholder);
+ }
+ }
+
+ /** Sets the image source in an {@link ImageView} from a {@link CarIcon}. */
+ public static boolean setImageSrc(
+ TemplateContext templateContext,
+ @Nullable CarIcon carIcon,
+ ImageView imageView,
+ ImageViewParams viewParams) {
+ if (carIcon == null) {
+ L.e(LogTags.TEMPLATE, "Failed to load image from a null icon");
+ return false;
+ }
+
+ try {
+ viewParams.getConstraints().validateOrThrow(carIcon);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ L.e(LogTags.TEMPLATE, e, "Failed to load image from an invalid icon: %s", carIcon);
+ return false;
+ }
+
+ int type = carIcon.getType();
+
+ // If the icon is custom, check that it is of a supported type.
+ if (type == CarIcon.TYPE_CUSTOM) {
+ IconCompat iconCompat = carIcon.getIcon();
+
+ if (iconCompat == null) {
+ L.e(LogTags.TEMPLATE, "Failed to get a valid backing icon for: %s", carIcon);
+ return setImageDrawable(imageView, null);
+ } else if (iconCompat.getType() == TYPE_URI) { // a custom icon of type URI.
+ return setImageSrcFromUri(
+ templateContext,
+ iconCompat.getUri(),
+ imageView,
+ carIcon.getTint(),
+ viewParams);
+ } else { // a custom icon not of type URI.
+ return setImageDrawable(
+ imageView, getIconDrawable(templateContext, carIcon, viewParams));
+ }
+ }
+
+ // a standard icon
+ return setImageDrawable(imageView, getIconDrawable(templateContext, carIcon, viewParams));
+ }
+
+ /** Sets the image source in an {@link Consumer<Drawable>} from a {@link CarIcon}. */
+ // TODO(b/183990524): See if this method could be unified with setImageSrc()
+ // Suppressing nullness check because AndroidX @Nullable can't be used to annotate generic types
+ // (see imageTarget parameter)
+ @SuppressWarnings("nullness:argument")
+ public static boolean setImageTargetSrc(
+ TemplateContext templateContext,
+ @Nullable CarIcon carIcon,
+ Consumer<Drawable> imageTarget,
+ ImageViewParams viewParams,
+ int width,
+ int height) {
+ if (carIcon == null) {
+ L.e(LogTags.TEMPLATE, "Failed to load image from a null icon");
+ return false;
+ }
+
+ try {
+ viewParams.getConstraints().validateOrThrow(carIcon);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ L.e(LogTags.TEMPLATE, e, "Failed to load image from an invalid icon: %s", carIcon);
+ return false;
+ }
+
+ int type = carIcon.getType();
+
+ // If the icon is custom, check that it is of a supported type.
+ if (type == CarIcon.TYPE_CUSTOM) {
+ IconCompat iconCompat = carIcon.getIcon();
+
+ if (iconCompat == null) {
+ L.e(LogTags.TEMPLATE, "Failed to get a valid backing icon for: %s", carIcon);
+ imageTarget.accept(null);
+ return false;
+ } else if (iconCompat.getType() == TYPE_URI) { // a custom icon of type URI.
+ getRequestFromUri(
+ templateContext, iconCompat.getUri(), carIcon.getTint(), viewParams)
+ .into(new ImageTarget(width, height, imageTarget));
+ return true;
+ } else { // a custom icon not of type URI.
+ imageTarget.accept(getIconDrawable(templateContext, carIcon, viewParams));
+ return true;
+ }
+ }
+
+ // a standard icon
+ imageTarget.accept(getIconDrawable(templateContext, carIcon, viewParams));
+ return true;
+ }
+
+ /**
+ * Returns a bitmap containing the given {@link IconCompat}.
+ *
+ * <p>This method cannot be used for icons of type URI which require asynchronous loading.
+ */
+ @Nullable
+ public static Bitmap getBitmapFromIcon(
+ TemplateContext templateContext,
+ CarIcon icon,
+ int targetWidth,
+ int targetHeight,
+ ImageViewParams viewParams,
+ @ScaleType int scaleType) {
+ Drawable drawable = getIconDrawable(templateContext, icon, viewParams);
+ return drawable == null
+ ? null
+ : getBitmapFromDrawable(
+ drawable,
+ targetWidth,
+ targetHeight,
+ templateContext.getResources().getDisplayMetrics().densityDpi,
+ scaleType);
+ }
+
+ /** Returns a bitmap containing the given label using the given paint. */
+ public static Bitmap getBitmapFromString(String label, Paint textPaint) {
+ Rect bounds = new Rect();
+ textPaint.getTextBounds(label, 0, label.length(), bounds);
+
+ // TODO(b/149182818): robolectric always returns empty bound. Bypass with a 1x1 bitmap.
+ // See https://github.com/robolectric/robolectric/issues/4343 for public bug.
+ if (bounds.width() <= 0 || bounds.height() <= 0) {
+ bounds.set(0, 0, 1, 1);
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawText(
+ label,
+ bounds.width() / 2.f,
+ bounds.height() / 2.f - (textPaint.descent() + textPaint.ascent()) / 2.f,
+ textPaint);
+ return bitmap;
+ }
+
+ /**
+ * Converts the {@code drawable} to a {@link Bitmap}.
+ *
+ * <p>The output {@link Bitmap} will be scaled to the input {@code targetWidth} and {@code
+ * targetHeight} if the drawable's size does not match up.
+ */
+ public static Bitmap getBitmapFromDrawable(
+ Drawable drawable, int maxWidth, int maxHeight, int density, @ScaleType int scaleType) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+
+ float widthScale = ((float) maxWidth) / width;
+ float heightScale = ((float) maxHeight) / height;
+
+ float scale = Math.min(widthScale, heightScale);
+
+ if (scaleType == SCALE_INSIDE
+ || scaleType == SCALE_CENTER_Y_INSIDE
+ || scaleType == SCALE_CENTER_XY_INSIDE) {
+ // Scale down if necessary. Do not scale up.
+ scale = Math.min(1.f, scale);
+ }
+
+ int scaledWidth = (int) (width * scale);
+ int scaledHeight = (int) (height * scale);
+
+ int bitmapWidth = scaledWidth;
+ int bitmapHeight = scaledHeight;
+ switch (scaleType) {
+ case SCALE_FIT_CENTER:
+ case SCALE_CENTER_XY_INSIDE:
+ bitmapWidth = maxWidth;
+ bitmapHeight = maxHeight;
+ break;
+ case SCALE_CENTER_Y_INSIDE:
+ bitmapHeight = maxHeight;
+ break;
+ case SCALE_INSIDE:
+ default:
+ break;
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Config.ARGB_8888);
+ bitmap.setDensity(density);
+ Canvas canvas = new Canvas(bitmap);
+
+ float dx = 0;
+ float dy = 0;
+ // Center-align the image horizontally/vertically if we have to.
+ switch (scaleType) {
+ case SCALE_FIT_CENTER:
+ case SCALE_CENTER_XY_INSIDE:
+ dx = Math.max(0.f, (maxWidth - scaledWidth) / 2.f);
+ dy = Math.max(0.f, (maxHeight - scaledHeight) / 2.f);
+ break;
+ case SCALE_CENTER_Y_INSIDE:
+ dy = Math.max(0.f, (maxHeight - scaledHeight) / 2.f);
+ break;
+ case SCALE_INSIDE:
+ default:
+ break;
+ }
+ canvas.translate(dx, dy);
+ canvas.scale(scale, scale);
+ drawable.setFilterBitmap(true);
+ drawable.setBounds(0, 0, width, height);
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ @DrawableRes
+ @VisibleForTesting
+ static int drawableIdFromCarIconType(int type, HostResourceIds hostResourceIds) {
+ switch (type) {
+ case CarIcon.TYPE_ALERT:
+ return hostResourceIds.getAlertIconDrawable();
+ case CarIcon.TYPE_ERROR:
+ return hostResourceIds.getErrorIconDrawable();
+ case CarIcon.TYPE_BACK:
+ return hostResourceIds.getBackIconDrawable();
+ case CarIcon.TYPE_PAN:
+ return hostResourceIds.getPanIconDrawable();
+ case CarIcon.TYPE_APP_ICON:
+ case CarIcon.TYPE_CUSTOM:
+ default:
+ L.w(LogTags.TEMPLATE, "Can't find drawable for icon type: %d", type);
+ return 0;
+ }
+ }
+
+ /** Returns the {@link CarIcon} that should be used for an {@link Action}. */
+ @Nullable
+ public static CarIcon getIconFromAction(Action action) {
+ CarIcon icon = action.getIcon();
+ if (icon == null && action.isStandard()) {
+ int type = action.getType();
+ icon = ImageUtils.getIconForStandardAction(type);
+ if (icon == null) {
+ L.e(LogTags.TEMPLATE, "Failed to get icon for standard action: %s", action);
+ }
+ }
+
+ return icon;
+ }
+
+ /** Returns the {@link CarIcon} corresponding to an action type. */
+ @Nullable
+ private static CarIcon getIconForStandardAction(int type) {
+ switch (type) {
+ case Action.TYPE_APP_ICON:
+ return CarIcon.APP_ICON;
+ case Action.TYPE_BACK:
+ return CarIcon.BACK;
+ case Action.TYPE_PAN:
+ return CarIcon.PAN;
+ case Action.TYPE_CUSTOM:
+ default:
+ L.e(LogTags.TEMPLATE, "Not a standard action: %s", type);
+ return null;
+ }
+ }
+
+ /**
+ * Sets the drawable to the image view.
+ *
+ * <p>Returns {@code true} if the view sets an image, and {@code false} if it clears the image
+ * (by setting a {@code null} drawable).
+ */
+ private static boolean setImageDrawable(ImageView imageView, @Nullable Drawable drawable) {
+ imageView.setImageDrawable(drawable);
+ return drawable != null;
+ }
+
+ private static boolean setImageSrcFromUri(
+ TemplateContext templateContext,
+ Uri uri,
+ ImageView imageView,
+ @Nullable CarColor tint,
+ ImageViewParams viewParams) {
+ getRequestFromUri(templateContext, uri, tint, viewParams).into(imageView);
+ return true;
+ }
+
+ private static RequestBuilder<Drawable> getRequestFromUri(
+ TemplateContext templateContext,
+ Uri uri,
+ @Nullable CarColor tint,
+ ImageViewParams viewParams) {
+ return Glide.with(templateContext)
+ .load(uri)
+ .placeholder(viewParams.getPlaceholderDrawable())
+ .listener(
+ new RequestListener<Drawable>() {
+ @Override
+ public boolean onLoadFailed(
+ @Nullable GlideException e,
+ Object model,
+ Target<Drawable> target,
+ boolean isFirstResource) {
+ ImageLoadCallback callback = viewParams.getImageLoadCallback();
+ if (callback != null) {
+ callback.onLoadFailed(e);
+ } else {
+ L.e(
+ LogTags.TEMPLATE,
+ e,
+ "Failed to load the image for URI: %s",
+ uri);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onResourceReady(
+ Drawable resource,
+ Object model,
+ Target<Drawable> target,
+ DataSource dataSource,
+ boolean isFirstResource) {
+ // If tint is specified in the icon, overwrite the backing icon's
+ // tint.
+ @ColorInt
+ int tintInt = getTintForIcon(templateContext, tint, viewParams);
+ if (tintInt != TRANSPARENT) {
+ resource.mutate();
+ resource.setTint(tintInt);
+ resource.setTintMode(Mode.SRC_IN);
+ }
+
+ ImageLoadCallback callback = viewParams.getImageLoadCallback();
+ if (callback != null) {
+ // TODO(b/156279162): Consider transition from placeholder image
+ target.onResourceReady(resource, /* transition= */ null);
+ callback.onImageReady();
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ /**
+ * Returns the tint to use for a given {@link CarColor} tint, or {@link Color#TRANSPARENT} if
+ * not tint should be applied.
+ */
+ @ColorInt
+ private static int getTintForIcon(
+ TemplateContext templateContext, @Nullable CarColor tint, ImageViewParams params) {
+ @ColorInt int defaultTint = params.getDefaultTint();
+ boolean forceTinting = params.getForceTinting();
+ boolean isDark = params.getIsDark();
+
+ if (tint != null && params.ignoreAppTint()) {
+ tint = CarColor.DEFAULT;
+ }
+
+ if (tint != null || forceTinting) {
+ return CarColorUtils.resolveColor(
+ templateContext,
+ tint,
+ isDark,
+ defaultTint,
+ CarColorConstraints.UNCONSTRAINED,
+ params.getBackgroundColor());
+ }
+ return TRANSPARENT;
+ }
+
+ /**
+ * Returns a drawable for a {@link CarIcon}.
+ *
+ * <p>This method should not be used for icons of type URI.
+ *
+ * @return {@code null} if it failed to get the icon, or if the icon type is a URI.
+ */
+ @Nullable
+ public static Drawable getIconDrawable(
+ TemplateContext templateContext, CarIcon carIcon, ImageViewParams viewParams) {
+ int type = carIcon.getType();
+ if (type == CarIcon.TYPE_APP_ICON) {
+ return templateContext.getCarAppPackageInfo().getRoundAppIcon();
+ }
+
+ CarIconConstraints constraints = viewParams.getConstraints();
+ try {
+ constraints.validateOrThrow(carIcon);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ L.e(LogTags.TEMPLATE, e, "Failed to load drawable from an invalid icon: %s", carIcon);
+ return null;
+ }
+
+ // Either a custom icon, or a standard icon other than the app icon: get its backing icon.
+ IconCompat iconCompat = getBackingIconCompat(templateContext, carIcon);
+ if (iconCompat == null) {
+ return null;
+ }
+
+ // If tint is specified in the icon, overwrite the backing icon's tint.
+ @ColorInt int tintInt = getTintForIcon(templateContext, carIcon.getTint(), viewParams);
+
+ // Load the resource drawables from the app using the configuration context so that we get
+ // them
+ // with the right target DPI and theme attributes are resolved correctly.
+ if (iconCompat.getType() == TYPE_RESOURCE) {
+ String iconPackageName = iconCompat.getResPackage();
+ if (iconPackageName == null) {
+ // If an app sends an IconCompat created with an androidx.core version before 1.4,
+ // the
+ // package name will be null.
+ L.w(
+ LogTags.TEMPLATE,
+ "Failed to load drawable from an icon with an unknown package name: %s",
+ carIcon);
+ return null;
+ }
+
+ String packageName =
+ templateContext.getCarAppPackageInfo().getComponentName().getPackageName();
+
+ // Remote resource from the app?
+ if (iconPackageName.equals(packageName)) {
+ return loadAppResourceDrawable(templateContext, iconCompat, tintInt);
+ }
+ }
+
+ if (tintInt != TRANSPARENT) {
+ iconCompat.setTint(tintInt);
+ iconCompat.setTintMode(Mode.SRC_IN);
+ }
+
+ return iconCompat.loadDrawable(templateContext);
+ }
+
+ @Nullable
+ private static Drawable loadAppResourceDrawable(
+ TemplateContext templateContext, IconCompat iconCompat, @ColorInt int tintInt) {
+ String packageName =
+ templateContext.getCarAppPackageInfo().getComponentName().getPackageName();
+
+ int density = templateContext.getResources().getDisplayMetrics().densityDpi;
+ @SuppressLint("ResourceType")
+ @DrawableRes
+ int resId = iconCompat.getResId();
+
+ Context configurationContext = templateContext.getAppConfigurationContext();
+ if (configurationContext == null) {
+ L.e(
+ LogTags.TEMPLATE,
+ "Failed to load drawable for %d, configuration unavailable",
+ resId);
+ return null;
+ }
+
+ L.d(
+ LogTags.TEMPLATE,
+ "Loading resource drawable with id %d for density %d from package %s",
+ resId,
+ density,
+ packageName);
+
+ // Load the drawable passing the density explicitly.
+ // The IconCompat#loadDrawable path /should/ be able to do this, but it does not.
+ // See b/159103561 for details. A side effect of us branching off this code path is that
+ // the tint set in the IconCompat instance is not honored.
+ Drawable drawable =
+ configurationContext
+ .getResources()
+ .getDrawableForDensity(resId, density, configurationContext.getTheme());
+ if (drawable == null) {
+ L.e(LogTags.TEMPLATE, "Failed to load drawable for %d", resId);
+ return null;
+ }
+
+ if (tintInt != TRANSPARENT) {
+ drawable.mutate();
+ DrawableCompat.setTintList(drawable, ColorStateList.valueOf(tintInt));
+ DrawableCompat.setTintMode(drawable, Mode.SRC_IN);
+ }
+
+ return drawable;
+ }
+
+ @Nullable
+ private static IconCompat getBackingIconCompat(
+ TemplateContext templateContext, CarIcon carIcon) {
+ IconCompat iconCompat;
+ int type = carIcon.getType();
+ if (type == CarIcon.TYPE_CUSTOM) {
+ iconCompat = carIcon.getIcon();
+ if (iconCompat == null) {
+ L.e(LogTags.TEMPLATE, "Custom icon without backing icon: %s", carIcon);
+ return null;
+ }
+ } else { // a standard icon
+ @DrawableRes
+ int resId = drawableIdFromCarIconType(type, templateContext.getHostResourceIds());
+ if (resId == 0) {
+ L.e(LogTags.TEMPLATE, "Failed to find resource id for standard icon: %s", carIcon);
+ return null;
+ }
+
+ iconCompat = IconCompat.createWithResource(templateContext, resId);
+ if (iconCompat == null) {
+ L.e(LogTags.TEMPLATE, "Failed to load standard icon: %s", carIcon);
+ return null;
+ }
+ }
+
+ return iconCompat;
+ }
+
+ private ImageUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageViewParams.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageViewParams.java
new file mode 100644
index 0000000..f6cbc41
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/common/ImageViewParams.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apphost.view.common;
+
+import static android.graphics.Color.TRANSPARENT;
+
+import android.graphics.drawable.Drawable;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarIcon;
+import com.android.car.libraries.apphost.distraction.constraints.CarIconConstraints;
+
+/** Encapsulates parameters that configure the way image view instances are rendered. */
+public final class ImageViewParams {
+ /** Callback for events related to image loading. */
+ public interface ImageLoadCallback {
+ /** Notifies that the load of the image failed. */
+ void onLoadFailed(@Nullable Throwable e);
+
+ /** Notifies that the images was successfully loaded. */
+ void onImageReady();
+ }
+
+ public static final ImageViewParams DEFAULT = ImageViewParams.builder().build();
+
+ @ColorInt private final int mDefaultTint;
+ private final boolean mForceTinting;
+ private final boolean mIgnoreAppTint;
+ private final boolean mIsDark;
+ private final CarIconConstraints mConstraints;
+ @Nullable private final Drawable mPlaceholderDrawable;
+ @Nullable private final ImageLoadCallback mImageLoadCallback;
+ @ColorInt private final int mBackgroundColor;
+
+ /**
+ * Returns the default tint color to apply to the image if one is not specified explicitly.
+ *
+ * @see Builder#setDefaultTint(int)
+ */
+ @ColorInt
+ public int getDefaultTint() {
+ return mDefaultTint;
+ }
+
+ /**
+ * Returns whether the default tint will be used when a {@link CarIcon} does not specify a tint.
+ *
+ * @see Builder#setForceTinting(boolean)
+ */
+ public boolean getForceTinting() {
+ return mForceTinting;
+ }
+
+ /** Returns whether the app-provided tint should be ignored. */
+ public boolean ignoreAppTint() {
+ return mIgnoreAppTint;
+ }
+
+ /**
+ * Returns whether to use the dark-variant of the tint color if one is provided.
+ *
+ * @see Builder#setIsDark(boolean)
+ */
+ public boolean getIsDark() {
+ return mIsDark;
+ }
+
+ /**
+ * Returns the {@link CarIconConstraints} to enforce when loading the image.
+ *
+ * @see Builder#setCarIconConstraints(CarIconConstraints)
+ */
+ public CarIconConstraints getConstraints() {
+ return mConstraints;
+ }
+
+ /**
+ * Returns the placeholder drawable to show while the image is loading or {@code null} to not show
+ * a placeholder image.
+ *
+ * @see Builder#setPlaceholderDrawable(Drawable)
+ */
+ @Nullable
+ public Drawable getPlaceholderDrawable() {
+ return mPlaceholderDrawable;
+ }
+
+ /**
+ * Returns the callback called when the image loading succeeds or fails or {@code null} if one is
+ * not set.
+ *
+ * @see Builder#setImageLoadCallback(ImageLoadCallback)
+ */
+ @Nullable
+ public ImageLoadCallback getImageLoadCallback() {
+ return mImageLoadCallback;
+ }
+
+ /**
+ * Sets the background color against which the text will be displayed.
+ *
+ * <p>This color is used only for the color contrast check, and will not be applied on the text
+ * background.
+ *
+ * <p>By default, the background color is assumed to be transparent.
+ */
+ @ColorInt
+ public int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ /** Returns a builder of {@link ImageViewParams}. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private ImageViewParams(
+ @ColorInt int defaultTint,
+ boolean forceTinting,
+ boolean isDark,
+ CarIconConstraints constraints,
+ @Nullable Drawable placeholderDrawable,
+ @Nullable ImageLoadCallback imageLoadCallback,
+ boolean ignoreAppTint,
+ @ColorInt int backgroundColor) {
+ mDefaultTint = defaultTint;
+ mForceTinting = forceTinting;
+ mIsDark = isDark;
+ mConstraints = constraints;
+ mPlaceholderDrawable = placeholderDrawable;
+ mImageLoadCallback = imageLoadCallback;
+ mIgnoreAppTint = ignoreAppTint;
+ mBackgroundColor = backgroundColor;
+ }
+
+ /** A builder of {@link ImageViewParams} instances. */
+ public static class Builder {
+ @ColorInt private int mDefaultTint = TRANSPARENT;
+ private boolean mForceTinting;
+ private boolean mIgnoreAppTint;
+ private boolean mIsDark;
+ private CarIconConstraints mConstraints = CarIconConstraints.DEFAULT;
+ @Nullable private Drawable mPlaceholderDrawable;
+ @Nullable private ImageLoadCallback mImageLoadCallback;
+ @ColorInt private int mBackgroundColor = TRANSPARENT;
+
+ /**
+ * Sets the tint to use by default.
+ *
+ * <p>If not set, the initial value is {@code TRANSPARENT}.
+ *
+ * <p>The default tint is used if a {@link CarIcon}'s tint is {@link
+ * androidx.car.app.model.CarColor#DEFAULT}, or the icon does not specify a tint and {@code
+ * #setForceTinting(true)} is called.
+ */
+ public Builder setDefaultTint(@ColorInt int defaultTint) {
+ mDefaultTint = defaultTint;
+ return this;
+ }
+
+ /**
+ * Determines if the default tint will be used when a {@link CarIcon} does not specify a tint.
+ *
+ * <p>The default value is {@code false}.
+ *
+ * @see {@link #setDefaultTint(int)} for details on when the default tint is used
+ */
+ public Builder setForceTinting(boolean forceTinting) {
+ mForceTinting = forceTinting;
+ return this;
+ }
+
+ /** Determines if the app-provided icon tint should be ignored. */
+ public Builder setIgnoreAppTint(boolean ignoreAppTint) {
+ mIgnoreAppTint = ignoreAppTint;
+ return this;
+ }
+
+ /**
+ * Sets whether to use the dark-variant of the tint color if one is provided.
+ *
+ * <p>The default value is {@code false}.
+ */
+ public Builder setIsDark(boolean isDark) {
+ mIsDark = isDark;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CarIconConstraints} to enforce when loading the image.
+ *
+ * <p>The default value is {@link CarIconConstraints#DEFAULT}.
+ */
+ public Builder setCarIconConstraints(CarIconConstraints constraints) {
+ mConstraints = constraints;
+ return this;
+ }
+
+ /**
+ * Sets the placeholder drawable to show while the image is loading.
+ *
+ * <p>The placeholder does not show for synchronously loaded images.
+ */
+ public Builder setPlaceholderDrawable(@Nullable Drawable placeholderDrawable) {
+ mPlaceholderDrawable = placeholderDrawable;
+ return this;
+ }
+
+ /**
+ * Sets a callback called when the image loading succeeds or fails.
+ *
+ * <p>The callback is ignored for synchronously loaded images.
+ */
+ public Builder setImageLoadCallback(@Nullable ImageLoadCallback imageLoadCallback) {
+ mImageLoadCallback = imageLoadCallback;
+ return this;
+ }
+
+ /**
+ * Sets the background color against which the text will be displayed.
+ *
+ * <p>This color is used only for the color contrast check, and will not be applied on the text
+ * background.
+ *
+ * <p>By default, the background color is assumed to be transparent.
+ */
+ public Builder setBackgroundColor(@ColorInt int backgroundColor) {
+ mBackgroundColor = backgroundColor;
+ return this;
+ }
+
+ /** Constructs a {@link ImageViewParams} instance defined by this builder. */
+ public ImageViewParams build() {
+ return new ImageViewParams(
+ mDefaultTint,
+ mForceTinting,
+ mIsDark,
+ mConstraints,
+ mPlaceholderDrawable,
+ mImageLoadCallback,
+ mIgnoreAppTint,
+ mBackgroundColor);
+ }
+ }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/widget/map/AbstractMapViewContainer.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/widget/map/AbstractMapViewContainer.java
new file mode 100644
index 0000000..9220a2e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/widget/map/AbstractMapViewContainer.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.apphost.view.widget.map;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import com.android.car.libraries.apphost.common.MapViewContainer;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A layout that wraps a single map view */
+public abstract class AbstractMapViewContainer extends FrameLayout
+ implements LifecycleOwner, DefaultLifecycleObserver, MapViewContainer {
+
+ /** Returns an {@link AbstractMapViewContainer} instance. */
+ public AbstractMapViewContainer(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /** Returns an {@link AbstractMapViewContainer} instance. */
+ public AbstractMapViewContainer(Context context) {
+ this(context, null, 0, 0);
+ }
+
+ /**
+ * Sets the {@link TemplateContext} to provide hosts and presenters.
+ *
+ * @param templateContext TemplateContext
+ */
+ public abstract void setTemplateContext(TemplateContext templateContext);
+}
diff --git a/Host/app/build.gradle b/Host/app/build.gradle
new file mode 100644
index 0000000..139ef01
--- /dev/null
+++ b/Host/app/build.gradle
@@ -0,0 +1,63 @@
+plugins {
+ id 'com.android.application'
+ id 'dagger.hilt.android.plugin'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ applicationId "com.android.car.templates.host"
+ minSdk 29
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ res.srcDirs = [
+ 'src/main/java/com/android/car/templates/host/res',
+ 'src/main/java/com/android/car/templates/host/view/widgets/maps/res',
+
+ 'src/main/java/com/android/car/templates/host/di/res',
+ ]
+ }
+ }
+}
+
+dependencies {
+ implementation "androidx.car.app:app:1.2.0-beta02"
+ implementation 'com.google.guava:guava:30.1.1-jre'
+
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation project(path: ':app:renderer')
+ implementation project(path: ':app:apphost')
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
+
+
+ implementation "com.google.dagger:hilt-android:2.40.5"
+ kapt 'com.google.dagger:hilt-compiler:2.40.5'
+ runtimeOnly 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
+
+}
diff --git a/Host/app/proguard-rules.pro b/Host/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Host/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/Host/app/renderer/build.gradle b/Host/app/renderer/build.gradle
new file mode 100644
index 0000000..d4871bc
--- /dev/null
+++ b/Host/app/renderer/build.gradle
@@ -0,0 +1,86 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+ id 'dagger.hilt.android.plugin'
+ id "com.google.protobuf"
+
+}
+
+
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ minSdk 29
+ targetSdk 31
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ res.srcDirs = [
+ 'src/main/java/com/android/car/libraries/templates/host/internal/res',
+ 'src/main/java/com/android/car/libraries/templates/host/internal/debug/res',
+ 'src/main/java/com/android/car/libraries/templates/host/internal/debug/styles/res',
+ 'src/main/java/com/android/car/libraries/templates/host/overlayable/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/common/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/presenters/navigation/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/presenters/maps/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/presenters/common/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/widgets/common/res',
+ 'src/main/java/com/android/car/libraries/templates/host/view/widgets/navigation/res',
+
+ ]
+ }
+ }
+
+}
+
+dependencies {
+ implementation "androidx.car.app:app:1.2.0-beta02"
+ implementation "androidx.car.app:app-automotive:1.2.0-beta02"
+ implementation 'com.google.guava:guava:30.1.1-jre'
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'com.android.car.ui:car-ui-lib:2.0.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0")
+
+ implementation "androidx.recyclerview:recyclerview:1.2.1"
+ // For control over item selection of both touch and mouse driven selection
+ implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
+ implementation 'com.google.zxing:core:3.4.1'
+
+
+ implementation "com.google.dagger:hilt-android:2.40.5"
+ kapt 'com.google.dagger:hilt-compiler:2.40.5'
+
+ implementation files('./libs/libnavigation_state_kt_proto_lite.ktproto.jar')
+ implementation files('./libs/libnavigation_state_proto-lite.jar')
+ implementation 'com.google.protobuf:protobuf-kotlin:3.19.4'
+
+ implementation project(path: ':app:apphost')
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ implementation files(gradle.ext.lib_car_system_stubs)
+
+}
diff --git a/Host/app/renderer/consumer-rules.pro b/Host/app/renderer/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Host/app/renderer/consumer-rules.pro
diff --git a/Host/app/renderer/libs/libnavigation_state_kt_proto_lite.ktproto.jar b/Host/app/renderer/libs/libnavigation_state_kt_proto_lite.ktproto.jar
new file mode 100644
index 0000000..a6163b6
--- /dev/null
+++ b/Host/app/renderer/libs/libnavigation_state_kt_proto_lite.ktproto.jar
Binary files differ
diff --git a/Host/app/renderer/libs/libnavigation_state_proto-lite.jar b/Host/app/renderer/libs/libnavigation_state_proto-lite.jar
new file mode 100644
index 0000000..ae4807f
--- /dev/null
+++ b/Host/app/renderer/libs/libnavigation_state_proto-lite.jar
Binary files differ
diff --git a/Host/app/renderer/proguard-rules.pro b/Host/app/renderer/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Host/app/renderer/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/Host/app/renderer/src/main/AndroidManifest.xml b/Host/app/renderer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a1258a4
--- /dev/null
+++ b/Host/app/renderer/src/main/AndroidManifest.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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.libraries.templates.host">
+
+ <application>
+ <activity
+ android:name=".internal.debug.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/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>
diff --git a/Host/app/renderer/src/main/res/layout/fragment_blank.xml b/Host/app/renderer/src/main/res/layout/fragment_blank.xml
new file mode 100644
index 0000000..6b2a64d
--- /dev/null
+++ b/Host/app/renderer/src/main/res/layout/fragment_blank.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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="match_parent"
+ tools:context=".BlankFragment">
+
+ <!-- TODO: Update blank fragment layout -->
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/hello_blank_fragment" />
+
+</FrameLayout>
diff --git a/Host/app/renderer/src/main/res/values/strings.xml b/Host/app/renderer/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1c9056a
--- /dev/null
+++ b/Host/app/renderer/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+ <!-- TODO: Remove or change this placeholder text -->
+ <string name="hello_blank_fragment">Hello blank fragment</string>
+</resources>
diff --git a/Host/app/src/main/AndroidManifest.xml b/Host/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c04a1b8
--- /dev/null
+++ b/Host/app/src/main/AndroidManifest.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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.car.templates.host"
+ android:versionCode="1"
+ android:versionName="0.1">
+
+ <uses-permission android:name="android.car.permission.TEMPLATE_RENDERER" />
+
+ <!-- Required to start foreground services -->
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+
+ <!-- Needed to start the host on boot completed -->
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+ <!-- Required for Cluster support -->
+ <uses-permission android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER" />
+ <uses-permission android:name="android.car.permission.CAR_NAVIGATION_MANAGER" />
+
+ <queries>
+ <provider android:authorities="androidx.car.app.connection"
+ tools:replace="android:authorities"/>
+ <provider
+ android:name="com.android.car.ui.plugin.PluginNameProvider"
+ android:authorities="com.android.car.ui.plugin"
+ tools:ignore="ExportedContentProvider,MissingClass"
+ tools:replace="android:authorities"/>
+ </queries>
+
+
+
+ <application
+ android:label="@string/app_name"
+ android:name="com.android.car.templates.host.TemplatesHostApplication"
+ android:icon="@drawable/ic_android">
+
+ <service android:name="com.android.car.libraries.templates.host.RendererService"
+ android:exported="true"
+ android:label="RendererService"
+ android:enabled="true"
+ android:foregroundServiceType="location"
+ tools:ignore="ExportedService">
+ <intent-filter>
+ <action android:name="android.car.template.host.RendererService" />
+ </intent-filter>
+ </service>
+
+ <receiver android:name="com.android.car.libraries.templates.host.BootCompleteReceiver"
+ android:exported="false"
+ android:directBootAware="true">
+ <intent-filter>
+ <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/BlankFragment.kt b/Host/app/src/main/java/com/android/car/templates/host/BlankFragment.kt
new file mode 100644
index 0000000..1808fec
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/BlankFragment.kt
@@ -0,0 +1,59 @@
+package com.android.car.templates.host
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+// TODO: Rename parameter arguments, choose names that match
+// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
+private const val ARG_PARAM1 = "param1"
+private const val ARG_PARAM2 = "param2"
+
+/**
+ * A simple [Fragment] subclass.
+ * Use the [BlankFragment.newInstance] factory method to
+ * create an instance of this fragment.
+ */
+class BlankFragment : Fragment() {
+ // TODO: Rename and change types of parameters
+ private var param1: String? = null
+ private var param2: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ param1 = it.getString(ARG_PARAM1)
+ param2 = it.getString(ARG_PARAM2)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_blank, container, false)
+ }
+
+ companion object {
+ /**
+ * Use this factory method to create a new instance of
+ * this fragment using the provided parameters.
+ *
+ * @param param1 Parameter 1.
+ * @param param2 Parameter 2.
+ * @return A new instance of fragment BlankFragment.
+ */
+ // TODO: Rename and change types and number of parameters
+ @JvmStatic
+ fun newInstance(param1: String, param2: String) =
+ BlankFragment().apply {
+ arguments = Bundle().apply {
+ putString(ARG_PARAM1, param1)
+ putString(ARG_PARAM2, param2)
+ }
+ }
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/TemplatesHostApplication.java b/Host/app/src/main/java/com/android/car/templates/host/TemplatesHostApplication.java
new file mode 100644
index 0000000..debca84
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/TemplatesHostApplication.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host;
+
+import android.app.Application;
+import dagger.hilt.android.HiltAndroidApp;
+
+/** This application class mark the whole application as Hilt application */
+@HiltAndroidApp(Application.class)
+public class TemplatesHostApplication extends Hilt_TemplatesHostApplication {}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/HostResourceIdsImpl.java b/Host/app/src/main/java/com/android/car/templates/host/di/HostResourceIdsImpl.java
new file mode 100644
index 0000000..cb19697
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/HostResourceIdsImpl.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.di;
+
+import androidx.annotation.NonNull;
+import com.android.car.libraries.apphost.common.ApiIncompatibilityType;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.android.components.ServiceComponent;
+import com.android.car.templates.host.R;
+
+/** The service level module to provide resources to AOSP Templates Host */
+@Module
+@InstallIn(ServiceComponent.class)
+public class HostResourceIdsImpl implements HostResourceIds {
+
+ @Provides
+ static HostResourceIds provideHostResourceIds() {
+ return new HostResourceIdsImpl();
+ }
+
+ private HostResourceIdsImpl() {}
+
+ @Override
+ public int getAlertIconDrawable() {
+ return com.android.car.templates.host.R.drawable.default_alert_icon;
+ }
+
+ @Override
+ public int getErrorIconDrawable() {
+ return com.android.car.templates.host.R.drawable.default_error_icon;
+ }
+
+ @Override
+ public int getBackIconDrawable() {
+ return com.android.car.templates.host.R.drawable.default_back_icon;
+ }
+
+ @Override
+ public int getPanIconDrawable() {
+ return com.android.car.templates.host.R.drawable.default_ic_pan_button;
+ }
+
+ @Override
+ public int getRefreshIconDrawable() {
+ return com.android.car.templates.host.R.drawable.default_ic_refresh_button;
+ }
+
+ @Override
+ public int getRedColor() {
+ return R.color.default_standard_red;
+ }
+
+ @Override
+ public int getRedDarkColor() {
+ return R.color.default_standard_red_dark;
+ }
+
+ @Override
+ public int getGreenColor() {
+ return R.color.default_standard_green;
+ }
+
+ @Override
+ public int getGreenDarkColor() {
+ return R.color.default_standard_green_dark;
+ }
+
+ @Override
+ public int getBlueColor() {
+ return R.color.default_standard_blue;
+ }
+
+ @Override
+ public int getBlueDarkColor() {
+ return R.color.default_standard_blue_dark;
+ }
+
+ @Override
+ public int getYellowColor() {
+ return R.color.default_standard_yellow;
+ }
+
+ @Override
+ public int getYellowDarkColor() {
+ return R.color.default_standard_yellow_dark;
+ }
+
+ @Override
+ public int getDefaultPrimaryColor() {
+ return R.color.default_primary_color;
+ }
+
+ @Override
+ public int getDefaultPrimaryDarkColor() {
+ return R.color.default_primary_dark_color;
+ }
+
+ @Override
+ public int getDefaultSecondaryColor() {
+ return R.color.default_secondary_color;
+ }
+
+ @Override
+ public int getDefaultSecondaryDarkColor() {
+ return R.color.default_secondary_dark_color;
+ }
+
+ @Override
+ public int getDistanceInMetersStringFormat() {
+ return R.string.meter_text;
+ }
+
+ @Override
+ public int getDistanceInKilometersStringFormat() {
+ return R.string.kilometer_text;
+ }
+
+ @Override
+ public int getDistanceInFeetStringFormat() {
+ return R.string.feet_text;
+ }
+
+ @Override
+ public int getDistanceInMilesStringFormat() {
+ return R.string.mile_text;
+ }
+
+ @Override
+ public int getDistanceInYardsStringFormat() {
+ return R.string.yard_text;
+ }
+
+ @Override
+ public int getTimeAtDestinationWithTimeZoneStringFormat() {
+ return R.string.time_at_destination_with_time_zone;
+ }
+
+ @Override
+ public int getDurationInDaysStringFormat() {
+ return R.string.duration_in_days;
+ }
+
+ @Override
+ public int getDurationInDaysAndHoursStringFormat() {
+ return R.string.duration_in_days_hours;
+ }
+
+ @Override
+ public int getDurationInHoursStringFormat() {
+ return R.string.duration_in_hours;
+ }
+
+ @Override
+ public int getDurationInHoursAndMinutesStringFormat() {
+ return R.string.duration_in_hours_minutes;
+ }
+
+ @Override
+ public int getDurationInMinutesStringFormat() {
+ return R.string.duration_in_minutes;
+ }
+
+ @Override
+ public int getAnrMessage() {
+ return R.string.anr_message;
+ }
+
+ @Override
+ public int getAnrWait() {
+ return R.string.anr_wait;
+ }
+
+ @Override
+ public int getAnrWaiting() {
+ return R.string.anr_waiting;
+ }
+
+ @Override
+ public int getAppApiIncompatibleText(@NonNull ApiIncompatibilityType apiIncompatibilityType) {
+ return apiIncompatibilityType == ApiIncompatibilityType.APP_TOO_OLD
+ ? R.string.app_api_too_old
+ : R.string.host_api_too_old;
+ }
+
+ @Override
+ public int getClientErrorText() {
+ return R.string.client_error_text;
+ }
+
+ @Override
+ public int getMissingPermissionText() {
+ return R.string.missing_permission_text;
+ }
+
+ @Override
+ public int getExitText() {
+ return R.string.exit_text;
+ }
+
+ @Override
+ public int getParkedOnlyActionText() {
+ return R.string.parked_only_action;
+ }
+
+ @Override
+ public int getSearchHintText() {
+ return R.string.search_hint;
+ }
+
+ @Override
+ public int getSearchHintDisabledText() {
+ return R.string.search_hint_disabled;
+ }
+
+ @Override
+ public int getDrivingStateMessageText() {
+ return R.string.driving_state_message;
+ }
+
+ @Override
+ public int getTemplateListNoItemsText() {
+ return R.string.template_list_no_items;
+ }
+
+ @Override
+ public int getLongMessageTemplateDisabledActionText() {
+ return R.string.long_message_disabled_action_text;
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/MapViewContainerModule.java b/Host/app/src/main/java/com/android/car/templates/host/di/MapViewContainerModule.java
new file mode 100644
index 0000000..35dda4e
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/MapViewContainerModule.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.di;
+
+import com.android.car.libraries.templates.host.di.MapViewContainerFactory;
+import com.android.car.templates.host.view.widgets.maps.MapViewStubContainer;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.android.components.ServiceComponent;
+
+/** A service level module for map related providers */
+@Module
+@InstallIn(ServiceComponent.class)
+public final class MapViewContainerModule {
+ @Provides
+ static MapViewContainerFactory provideMapViewContainerFactory() {
+ return MapViewStubContainer::create;
+ }
+
+ private MapViewContainerModule() {}
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerModule.java b/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerModule.java
new file mode 100644
index 0000000..ae0e23a
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerModule.java
@@ -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.templates.host.di;
+
+import com.android.car.libraries.templates.host.di.TelemetryHandlerFactory;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.android.components.ServiceComponent;
+
+/** A service level module for telemetry related providers */
+@Module
+@InstallIn(ServiceComponent.class)
+final class TelemetryHandlerModule {
+ @Provides
+ static TelemetryHandlerFactory provideTelemetryHandlerFactory() {
+ return TelemetryHandlerStub::create;
+ }
+
+ private TelemetryHandlerModule() {}
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerStub.java b/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerStub.java
new file mode 100644
index 0000000..8b57a5b
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/TelemetryHandlerStub.java
@@ -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.templates.host.di;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Log;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+final class TelemetryHandlerStub extends TelemetryHandler {
+
+ /** Returns a new instance of {@link TelemetryHandlerStub}. */
+ public static TelemetryHandler create(Context context, ComponentName componentName) {
+ return new TelemetryHandlerStub();
+ }
+
+ @Override
+ public void logCarAppTelemetry(TelemetryEvent.Builder logEventBuilder) {
+ TelemetryEvent event = logEventBuilder.build();
+ Log.d(
+ LogTags.APP_HOST,
+ "TelemetryHandlerStub log event for "
+ + event.getComponentName()
+ + " on "
+ + event.getAction().name());
+ }
+
+ private TelemetryHandlerStub() {}
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/ThemeManagerImpl.java b/Host/app/src/main/java/com/android/car/templates/host/di/ThemeManagerImpl.java
new file mode 100644
index 0000000..cf6f3bd
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/ThemeManagerImpl.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.templates.host.di;
+
+import android.content.Context;
+import com.android.car.libraries.templates.host.R;
+import com.android.car.libraries.templates.host.di.ThemeManager;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.android.components.ServiceComponent;
+
+/** The service level module to provide {@link ThemeManager}. */
+@Module
+@InstallIn(ServiceComponent.class)
+class ThemeManagerImpl implements ThemeManager {
+
+ @Provides
+ static ThemeManager provideThemeManager() {
+ return new ThemeManagerImpl();
+ }
+
+ private ThemeManagerImpl() {}
+
+ /** Applies appropriate theme to the given context. */
+ @Override
+ public void applyTheme(Context context) {
+ context.getTheme().applyStyle(R.style.Theme_Template, true);
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/config/AndroidManifest.xml b/Host/app/src/main/java/com/android/car/templates/host/di/config/AndroidManifest.xml
new file mode 100644
index 0000000..ee63c19
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/config/AndroidManifest.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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.templates.host.di.config">
+
+ <uses-sdk android:minSdkVersion="29" />
+</manifest>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/config/FeaturesConfigStub.java b/Host/app/src/main/java/com/android/car/templates/host/di/config/FeaturesConfigStub.java
new file mode 100644
index 0000000..37d252c
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/config/FeaturesConfigStub.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.templates.host.di.config;
+
+import com.android.car.libraries.templates.host.di.FeaturesConfig;
+
+/** Stub implementation of {@link FeaturesConfig} that just works as a pass-through */
+final class FeaturesConfigStub implements FeaturesConfig {
+ @Override
+ public boolean isClusterActivityEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isNavPanZoomEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isPoiRoutePreviewPanZoomEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isPoiContentRefreshEnabled() {
+ return false;
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/config/HostApiLevelConfigStub.java b/Host/app/src/main/java/com/android/car/templates/host/di/config/HostApiLevelConfigStub.java
new file mode 100644
index 0000000..c5b7b01
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/config/HostApiLevelConfigStub.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.templates.host.di.config;
+
+import android.content.ComponentName;
+import com.android.car.libraries.templates.host.di.HostApiLevelConfig;
+
+/** Stub implementation of {@link HostApiLevelConfig} that just works as a pass-through */
+final class HostApiLevelConfigStub implements HostApiLevelConfig {
+
+ @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/src/main/java/com/android/car/templates/host/di/config/StubModule.java b/Host/app/src/main/java/com/android/car/templates/host/di/config/StubModule.java
new file mode 100644
index 0000000..ec671b5
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/config/StubModule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.di.config;
+
+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.UxreConfig;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.android.components.ServiceComponent;
+
+/** The service level module to privde configs to AOSP Templates Host */
+@Module
+@InstallIn(ServiceComponent.class)
+class StubModule {
+ @Provides
+ static FeaturesConfig provideFeaturesConfig() {
+ return new FeaturesConfigStub();
+ }
+
+ @Provides
+ static UxreConfig provideUxreConfig() {
+ return new UxreConfigStub();
+ }
+
+ @Provides
+ static HostApiLevelConfig provideHostApiLevelConfig() {
+ return new HostApiLevelConfigStub();
+ }
+
+ private StubModule() {}
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/di/config/UxreConfigStub.java b/Host/app/src/main/java/com/android/car/templates/host/di/config/UxreConfigStub.java
new file mode 100644
index 0000000..600a11f
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/config/UxreConfigStub.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.di.config;
+
+import com.android.car.libraries.templates.host.di.UxreConfig;
+
+/** Stub implementation of {@link UxreConfig} that just works as a pass-through */
+final class UxreConfigStub implements UxreConfig {
+ @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/src/main/java/com/android/car/templates/host/di/res/values/strings.xml b/Host/app/src/main/java/com/android/car/templates/host/di/res/values/strings.xml
new file mode 100644
index 0000000..50e23eb
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/di/res/values/strings.xml
@@ -0,0 +1,117 @@
+<?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:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- SearchTemplate Hint [CHAR_LIMIT=20] -->
+ <string name="search_hint">Search</string>
+
+ <!-- SearchTemplate Hint during the disabled state [CHAR_LIMIT=30] -->
+ <string name="search_hint_disabled">Not while driving</string>
+
+ <!-- VoiceTemplate state description for listening [CHAR_LIMIT=20] -->
+ <string name="state_listening">Listening</string>
+
+ <!-- VoiceTemplate state description for speaking [CHAR_LIMIT=20] -->
+ <string name="state_speaking">Speaking</string>
+
+ <!-- VoiceTemplate state description for mic muted [CHAR_LIMIT=50] -->
+ <string name="state_muted">Muted</string>
+
+ <!-- VoiceTemplate content description for microphone image [CHAR_LIMIT=20] -->
+ <string name="mic_icon" translatable="false">Microphone icon</string>
+
+ <!-- Text for meters abbreviation [CHAR_LIMIT=10] -->
+ <string name="meter_text">%s m</string>
+
+ <!-- Text for kilometers abbreviation [CHAR_LIMIT=10] -->
+ <string name="kilometer_text">%s km</string>
+
+ <!-- Text for feet abbreviation [CHAR_LIMIT=10] -->
+ <string name="feet_text">%s ft</string>
+
+ <!-- Text for miles abbreviation [CHAR_LIMIT=10] -->
+ <string name="mile_text">%s mi</string>
+
+ <!-- Text for yards abbreviation [CHAR_LIMIT=10] -->
+ <string name="yard_text">%s yd</string>
+
+ <!-- Message when lists have no items [CHAR_LIMIT=20] -->
+ <string name="template_list_no_items">No items</string>
+
+
+ <!-- A message shown while navigating to indicate when the user will arrive at their
+ destination. This combines a time of arrival with the short abbreviation of the
+ time zone it is in. Keep this short, as this is always present on-screen during
+ navigation. [CHAR_LIMIT=10] -->
+ <string name="time_at_destination_with_time_zone">
+ <xliff:g id="time_at_destination_with_time_zone" example="5:18 PM">%1$s</xliff:g>\u00A0<xliff:g id="short_time_zone" example="PST">%2$s</xliff:g>
+ </string>
+
+ <!-- The duration string that shows days only (e.g. "2 d"). [CHAR_LIMIT=10] -->
+ <string name="duration_in_days"><xliff:g example="2" id="days_count">%1$d</xliff:g> d</string>
+
+ <!-- The duration string that shows days and hours (e.g. "2 d 15 hr"). [CHAR_LIMIT=10] -->
+ <string name="duration_in_days_hours"><xliff:g example="2" id="days_count">%1$d</xliff:g> d <xliff:g example="15" id="hours_count">%2$d</xliff:g> hr</string>
+
+ <!-- The duration string that shows hours only (e.g. "23 hr"). [CHAR_LIMIT=10] -->
+ <string name="duration_in_hours"><xliff:g example="23" id="hours_count">%1$d</xliff:g> hr</string>
+
+ <!-- The duration string that shows hours and minutes (e.g. "23 hr 15 min"). [CHAR_LIMIT=10] -->
+ <string name="duration_in_hours_minutes"><xliff:g example="23" id="hours_count">%1$d</xliff:g> hr <xliff:g example="15" id="minutes_count">%2$d</xliff:g> min</string>
+
+ <!-- The duration string that shows minutes only (e.g. "15 min"). [CHAR_LIMIT=10] -->
+ <string name="duration_in_minutes"><xliff:g example="15" id="minutes_count">%1$d</xliff:g> min</string>
+
+ <!-- Toast message for user selecting action that can only be selected when parked [CHAR_LIMIT=40] -->
+ <string name="parked_only_action">Not available when driving</string>
+
+ <!-- Error message for client app exception [CHAR_LIMIT=70] -->
+ <string name="client_error_text"><xliff:g name="app">%s</xliff:g> has encountered an unexpected error</string>
+
+ <!-- Error message for client app exception [CHAR_LIMIT=10] -->
+ <string name="exit_text">Exit</string>
+
+ <!-- Error message for app requiring an older host than supported [CHAR_LIMIT=80] -->
+ <string name="app_api_too_old">This version of <xliff:g name="app">%s</xliff:g> is not supported. Please update.</string>
+
+ <!-- Error message for app requiring a newer host than available [CHAR_LIMIT=80] -->
+ <string name="host_api_too_old"><xliff:g name="app">%s</xliff:g> requires a newer version of Android Automotive Templates Host.</string>
+
+ <!-- Error message for application not responding [CHAR_LIMIT=100] -->
+ <string name="anr_message"><xliff:g name="app">%s</xliff:g> isn\'t responding</string>
+
+ <!-- Button text for waiting for ANR [CHAR_LIMIT=15] -->
+ <string name="anr_wait">Wait</string>
+
+ <!-- Error message for waiting for application to respond [CHAR_LIMIT=20] -->
+ <string name="anr_waiting">Waiting…</string>
+
+ <!-- Error message for the application not having required permission [CHAR_LIMIT=100] -->
+ <string name="missing_permission_text"><xliff:g name="app">%s</xliff:g> needs a required permission</string>
+
+ <!-- CarUxRestrictions Utility -->
+ <string name="car_app_ellipsis" translatable="false">&#8230;</string>
+
+ <!-- Message to indicate that the vehicle must be parked to continue using the app. [CHAR_LIMIT=50] -->
+ <string name="driving_state_message">You can\'t use this feature while driving</string>
+
+ <!-- Text for the rerouting state [CHAR_LIMIT=20] -->
+ <string name="rerouting_text">Rerouting</string>
+
+ <!-- Toast message for disabled action button in long message template. [CHAR_LIMIT=40] -->
+ <string name="long_message_disabled_action_text">Scroll to continue</string>
+</resources>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_background.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_background.xml
new file mode 100644
index 0000000..4aaee56
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_ripple.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_action_button_ripple.xml
new file mode 100644
index 0000000..ba31afc
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_background.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_background.xml
new file mode 100644
index 0000000..e6b3670
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_foreground.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_edit_text_foreground.xml
new file mode 100644
index 0000000..4451f8d
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/default_ic_pan_button.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_ic_pan_button.xml
new file mode 100644
index 0000000..171d5c2
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/default_ic_refresh_button.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/default_ic_refresh_button.xml
new file mode 100644
index 0000000..6384396
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/drawable/ic_android.xml b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/ic_android.xml
new file mode 100644
index 0000000..db80c86
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/res/drawable/ic_android.xml
@@ -0,0 +1,74 @@
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.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="48dp" android:viewportHeight="192"
+ android:viewportWidth="192" android:width="48dp"
+ xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillAlpha="0" android:fillColor="#FF000000"
+ android:pathData="M0,0h192v192H0z" android:strokeAlpha="0"/>
+ <path android:pathData="M110.2,111.22l49.8,49.8V59.39l-49.8,49.8z">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="45.221" android:endY="110.207"
+ android:startX="163.779" android:startY="110.207" android:type="linear">
+ <item android:color="#FFFFE000" android:offset="0"/>
+ <item android:color="#FFFFBD00" android:offset="0.409"/>
+ <item android:color="#FFFFA500" android:offset="0.775"/>
+ <item android:color="#FFFF9C00" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:pathData="M110.2,109.19L49,48h-7.05c-5.5,0 -9.95,4.22 -9.95,9.72V92.9c0,3.55 2.88,6.43 6.43,6.43 1.56,0 2.99,-0.56 4.1,-1.48l1.05,-0.83c3.01,-2.64 6.96,-4.23 11.28,-4.23 9.46,0 17.14,7.73 17.14,17.2 0,9.47 -7.68,17.08 -17.14,17.08 -4.32,0 -8.27,-1.59 -11.28,-4.23l-1.05,-0.83c-1.11,-0.92 -2.54,-1.48 -4.1,-1.48 -3.55,0 -6.43,2.88 -6.43,6.43v34.76c0,5.5 4.45,10.28 9.95,10.28h7.47l60.78,-60.78v-2.03z">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="10.114" android:endY="143.447"
+ android:startX="105.972" android:startY="47.589" android:type="linear">
+ <item android:color="#FF00A0FF" android:offset="0"/>
+ <item android:color="#FF00A1FF" android:offset="0.007"/>
+ <item android:color="#FF00BEFF" android:offset="0.26"/>
+ <item android:color="#FF00D2FF" android:offset="0.512"/>
+ <item android:color="#FF00DFFF" android:offset="0.76"/>
+ <item android:color="#FF00E3FF" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:pathData="M160,60.4v-2.68c0,-5.5 -4.45,-9.72 -9.95,-9.72h-37.02c-3.55,0 -6.43,-2.77 -6.43,-6.32 0,-1.56 0.56,-3.1 1.48,-4.21l0.83,-1.05c2.64,-3.01 4.23,-6.96 4.23,-11.28C113.14,15.68 105.47,8 96,8s-17.14,7.69 -17.14,17.15c0,4.32 1.59,8.28 4.23,11.29l0.83,1.08c0.92,1.11 1.48,2.6 1.48,4.16 0,3.55 -2.88,6.32 -6.43,6.32H47.99l62.21,62.21L160,60.4z">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="93.393" android:endY="40.253"
+ android:startX="35.008" android:startY="-18.132" android:type="linear">
+ <item android:color="#FF32A071" android:offset="0"/>
+ <item android:color="#FF2DA771" android:offset="0.069"/>
+ <item android:color="#FF15CF74" android:offset="0.476"/>
+ <item android:color="#FF06E775" android:offset="0.801"/>
+ <item android:color="#FF00F076" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:pathData="M48.4,172h101.65c5.5,0 9.95,-4.78 9.95,-10.28v-1.71l-49.8,-49.81L48.4,172z">
+ <aapt:attr name="android:fillColor">
+ <gradient android:endX="4.794" android:endY="268.414"
+ android:startX="135.545" android:startY="137.663" android:type="linear">
+ <item android:color="#FFFF3A44" android:offset="0"/>
+ <item android:color="#FFC31162" android:offset="1"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path android:fillAlpha="0.25" android:fillColor="#FFFFFF"
+ android:pathData="M96,9c9.31,0 16.86,7.43 17.12,16.66 0.01,-0.18 0.02,-0.35 0.02,-0.52C113.14,15.68 105.47,8 96,8s-17.14,7.69 -17.14,17.15c0,0.18 0.02,0.35 0.02,0.52C79.14,16.44 86.69,9 96,9zM41.95,49h37.02c3.55,0 6.43,-2.77 6.43,-6.32 0,-0.12 -0.03,-0.24 -0.03,-0.36 -0.32,3.24 -3.07,5.68 -6.4,5.68H41.95c-5.5,0 -9.95,4.22 -9.95,9.72v1c0,-5.5 4.45,-9.72 9.95,-9.72zM38.43,121.53c1.56,0 2.99,0.56 4.1,1.48l1.05,0.83c3.01,2.64 6.96,4.23 11.28,4.23 9.46,0 17.14,-7.61 17.14,-17.08 0,-0.17 -0.02,-0.33 -0.03,-0.5 -0.27,9.23 -7.82,16.58 -17.11,16.58 -4.32,0 -8.27,-1.59 -11.28,-4.23l-1.05,-0.83c-1.11,-0.92 -2.54,-1.48 -4.1,-1.48 -3.55,0 -6.43,2.88 -6.43,6.43v1c0,-3.55 2.88,-6.43 6.43,-6.43zM150.05,48h-37.02c-3.34,0 -6.08,-2.45 -6.4,-5.69 -0.01,0.12 -0.03,0.24 -0.03,0.37 0,3.55 2.88,6.32 6.43,6.32h37.02c5.5,0 9.95,4.22 9.95,9.72v-1c0,-5.5 -4.45,-9.72 -9.95,-9.72z" android:strokeAlpha="0.25"/>
+ <path android:fillAlpha="0.12" android:fillColor="#231F20"
+ android:pathData="M83.92,37.52c0.85,1.02 1.37,2.37 1.45,3.8 0.02,-0.21 0.03,-0.42 0.03,-0.64 0,-1.56 -0.56,-3.05 -1.48,-4.16l-0.83,-1.08c-2.53,-2.89 -4.08,-6.65 -4.21,-10.77 0,0.16 -0.02,0.31 -0.02,0.48 0,4.32 1.59,8.28 4.23,11.29l0.83,1.08zM108.08,36.47c-0.92,1.11 -1.48,2.65 -1.48,4.21 0,0.21 0.01,0.43 0.03,0.63 0.08,-1.43 0.6,-2.82 1.45,-3.84l0.83,-1.05c2.64,-3.01 4.23,-6.96 4.23,-11.28 0,-0.16 -0.02,-0.32 -0.02,-0.48 -0.12,4.11 -1.67,7.87 -4.21,10.76l-0.83,1.05zM38.43,99.33c1.56,0 2.99,-0.56 4.1,-1.48l1.05,-0.83c3.01,-2.64 6.96,-4.23 11.28,-4.23 9.29,0 16.85,7.46 17.11,16.7 0,-0.17 0.03,-0.33 0.03,-0.5 0,-9.47 -7.68,-17.2 -17.14,-17.2 -4.32,0 -8.27,1.59 -11.28,4.23l-1.05,0.83c-1.11,0.92 -2.54,1.48 -4.1,1.48 -3.55,0 -6.43,-2.88 -6.43,-6.43v1c0,3.55 2.88,6.43 6.43,6.43z" android:strokeAlpha="0.12"/>
+ <path android:fillAlpha="0.2" android:fillColor="#231F20"
+ android:pathData="M150.05,171H49.4l-1,1h101.65c5.5,0 9.95,-4.78 9.95,-10.28v-1c0,5.5 -4.45,10.28 -9.95,10.28z" android:strokeAlpha="0.2"/>
+ <path android:fillAlpha="0.12" android:fillColor="#231F20"
+ android:pathData="M41.95,171c-5.5,0 -9.95,-4.78 -9.95,-10.28v1c0,5.5 4.45,10.28 9.95,10.28h6.45l1,-1h-7.45z" android:strokeAlpha="0.12"/>
+</vector>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/res/layout/fragment_blank.xml b/Host/app/src/main/java/com/android/car/templates/host/res/layout/fragment_blank.xml
new file mode 100644
index 0000000..6b2a64d
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/res/layout/fragment_blank.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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="match_parent"
+ tools:context=".BlankFragment">
+
+ <!-- TODO: Update blank fragment layout -->
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/hello_blank_fragment" />
+
+</FrameLayout>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/res/values-night/colors.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values-night/colors.xml
new file mode 100644
index 0000000..24bad14
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values-night/colors_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values-night/colors_overlayable.xml
new file mode 100644
index 0000000..f7a1c51
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/attrs.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/attrs.xml
new file mode 100644
index 0000000..07b3c19
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/bools_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/bools_overlayable.xml
new file mode 100644
index 0000000..22e64fe
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/colors.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/colors.xml
new file mode 100644
index 0000000..0ca7b10
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/colors_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/colors_overlayable.xml
new file mode 100644
index 0000000..e9c2531
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/dimens_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/dimens_overlayable.xml
new file mode 100644
index 0000000..dd56d1e
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/drawable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/drawable.xml
new file mode 100644
index 0000000..f363b45
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/drawable_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/drawable_overlayable.xml
new file mode 100644
index 0000000..042bbf4
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/integers.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/integers.xml
new file mode 100644
index 0000000..0a589d5
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/integers_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/integers_overlayable.xml
new file mode 100644
index 0000000..0123000
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/overlayable.xml
new file mode 100644
index 0000000..289f8bc
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/res/values/strings.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/strings.xml
new file mode 100644
index 0000000..a925526
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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>
+ <!-- Application name [CHAR LIMIT=50] -->
+ <string name="app_name" translatable="false">AOSP Templates Host</string>
+ <!-- TODO: Remove or change this placeholder text -->
+ <string name="hello_blank_fragment">Hello blank fragment</string>
+</resources>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/res/values/styles_overlayable.xml b/Host/app/src/main/java/com/android/car/templates/host/res/values/styles_overlayable.xml
new file mode 100644
index 0000000..2162364
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/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/src/main/java/com/android/car/templates/host/view/widgets/maps/MapStub.java b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapStub.java
new file mode 100644
index 0000000..4bb11c1
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapStub.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.view.widgets.maps;
+
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarToast;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarLocation;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/** A mock map used to simulate all the APIs of a actual map. */
+public class MapStub {
+ /** A listener that is called when any parameter of this map changes. */
+ private OnMapUpdateListener mOnMapUpdateListener;
+
+ private final UiSettings mUiSettings;
+ private final ArrayList<Marker> mMarkers = new ArrayList<>();
+
+ private OnMarkerClickListener mOnMarkerClickListener;
+ private CarLocation mCarLocation;
+
+ private boolean mIsLocationEnabled;
+ private boolean mHasAnimation;
+
+ private float mMaxZoomPreference;
+ private int mAnimationDuration;
+ private int mPaddingLeft;
+ private int mPaddingRight;
+ private int mPaddingTop;
+ private int mPaddingBottom;
+
+ /** Instantiates a new mock map. */
+ public MapStub() {
+ mUiSettings = new UiSettings();
+ }
+
+ /** sets OnMapUpdateListener for this map. */
+ public void setOnMapUpdateListener(OnMapUpdateListener onMapUpdateListener) {
+ mOnMapUpdateListener = onMapUpdateListener;
+ }
+
+ /** sets MaxZoomPreference for this map. */
+ public void setMaxZoomPreference(float maxZoomPreference) {
+ Log.d(LogTags.TEMPLATE, "MaxZoomPreference is updated, " + maxZoomPreference);
+ mMaxZoomPreference = maxZoomPreference;
+ update();
+ }
+
+ /** sets OnMarkerClickListener for this map. */
+ public void setOnMarkerClickListener(MapStub.OnMarkerClickListener listener) {
+ Log.d(LogTags.TEMPLATE, "setOnMarkerClickListener");
+ mOnMarkerClickListener = listener;
+ update();
+ }
+
+ /** Enables location. */
+ public void setMyLocationEnabled(boolean enabled) {
+ Log.d(LogTags.TEMPLATE, "setMyLocationEnabled: " + enabled);
+ mIsLocationEnabled = enabled;
+ update();
+ }
+
+ /** Sets paddings. */
+ public void setPadding(int left, int top, int right, int bottom) {
+ Log.d(LogTags.TEMPLATE, "setPaddings: " + left + ", " + top + ", " + right + ", " + bottom);
+ mPaddingLeft = left;
+ mPaddingRight = right;
+ mPaddingTop = top;
+ mPaddingBottom = bottom;
+ update();
+ }
+
+ /** Add a marker. */
+ public void addMarker(Marker marker) {
+ if (marker == null) {
+ return;
+ }
+ Log.d(LogTags.TEMPLATE, "add a marker" + marker);
+ mMarkers.add(marker);
+ update();
+ }
+
+ /** Remove a marker. */
+ public boolean removeMarker(Marker marker) {
+ if (marker == null || !mMarkers.contains(marker)) {
+ return false;
+ }
+ Log.d(LogTags.TEMPLATE, "remove a marker" + marker);
+ mMarkers.remove(marker);
+ update();
+ return true;
+ }
+
+ /** Returns UiSettings. */
+ public UiSettings getUiSettings() {
+ return mUiSettings;
+ }
+
+ /** Updates the camera. */
+ public void animateCamera(CarLocation location, int animationDuration) {
+ Log.d(LogTags.TEMPLATE, "move camera with animation to " + location);
+ mCarLocation = location;
+ mHasAnimation = true;
+ mAnimationDuration = animationDuration;
+ update();
+ }
+
+ /** Updates the camera. */
+ public void moveCamera(CarLocation location) {
+ Log.d(LogTags.TEMPLATE, "Move Camera to: " + location);
+ mCarLocation = location;
+ mHasAnimation = false;
+ mAnimationDuration = 0;
+ update();
+ }
+
+ private void update() {
+ mOnMapUpdateListener.onMapUpdate();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder mapInfo = new StringBuilder();
+ String newLine = System.lineSeparator();
+ mapInfo
+ .append(String.format("UiSettings: %s", mUiSettings))
+ .append(newLine)
+ .append(String.format("setMaxZoomPreference to be %f", mMaxZoomPreference))
+ .append(newLine)
+ .append(String.format("setOnMarkerClickListener %s", mOnMarkerClickListener))
+ .append(newLine)
+ .append(String.format("setMyLocationEnabled: %s", mIsLocationEnabled))
+ .append(newLine)
+ .append(
+ String.format(
+ "setPaddings: %d, %d, %d, %d",
+ mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom))
+ .append(newLine)
+ .append(String.format("Markers: %s", Arrays.toString(mMarkers.toArray())))
+ .append(newLine)
+ .append(String.format("move camera to %s", mCarLocation))
+ .append(newLine)
+ .append(
+ (mHasAnimation
+ ? String.format("with animation (duration: %d)", mAnimationDuration)
+ : "without animation"));
+ return mapInfo.toString();
+ }
+
+ /** A listener that is called when any parameter of this map changes. */
+ public interface OnMapUpdateListener {
+ /** Called when map is updated. */
+ void onMapUpdate();
+ }
+
+ /** A mock Marker class. */
+ public static class Marker {
+ private final CarLocation mLocation;
+ @Nullable private final CarColor mMarkerColor;
+
+ /** Instantiates a marker */
+ public Marker(CarLocation location, @Nullable CarColor color) {
+ mLocation = location;
+ mMarkerColor = color;
+ }
+
+ @Override
+ public String toString() {
+ return "Marker location: " + mLocation + ", " + "color: " + mMarkerColor;
+ }
+ }
+
+ /** A mock OnMarkerClickListener class. */
+ public static class OnMarkerClickListener implements View.OnTouchListener {
+
+ /** Called when a touch event is dispatched. */
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float positionX = event.getX();
+ float positionY = event.getY();
+ Log.d(LogTags.TEMPLATE, "OnMarkerClickListener: X: " + positionX + ", Y: " + positionY);
+ ((TemplateContext) v.getContext())
+ .getToastController()
+ .showToast(
+ "OnMarkerClickListener: X: " + positionX + ", Y: " + positionY, CarToast.LENGTH_LONG);
+ return true;
+ }
+ }
+
+ /** A mock class for UiSettings. */
+ public static class UiSettings {
+ private boolean mIsMyLocationButtonEnabled;
+ private boolean mIsAllGesturesEnabled;
+
+ public void setMyLocationButtonEnabled(boolean enabled) {
+ mIsMyLocationButtonEnabled = enabled;
+ }
+
+ public void setAllGesturesEnabled(boolean enabled) {
+ mIsAllGesturesEnabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "IsMyLocationButtonEnabled: "
+ + mIsMyLocationButtonEnabled
+ + ", IsAllGesturesEnabled: "
+ + mIsAllGesturesEnabled;
+ }
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStub.java b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStub.java
new file mode 100644
index 0000000..523d855
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStub.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.view.widgets.maps;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** A Map view that holds mock map. */
+public class MapViewStub extends FrameLayout {
+ private MapStub mMap;
+ private TextView mTextView;
+
+ public MapViewStub(@NonNull Context context) {
+ super(context);
+ }
+
+ public MapViewStub(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Instantiates a new map view.
+ *
+ * @see android.content.res.Resources.Theme#obtainStyledAttributes
+ */
+ public MapViewStub(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /** Instantiates a new map view. */
+ @SuppressWarnings("nullness:argument")
+ public MapViewStub(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ LayoutInflater.from(context).inflate(com.android.car.templates.host.R.layout.map_stub_layout, this);
+ mTextView = findViewById(com.android.car.templates.host.R.id.map_info);
+ mMap = new MapStub();
+ mMap.setOnMapUpdateListener(() -> mTextView.setText(mMap.toString()));
+ }
+
+ /**
+ * Return a mock map. This method is a substitute of getMapAsync() in other map implementation.
+ */
+ public MapStub getMap() {
+ return mMap;
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStubContainer.java b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStubContainer.java
new file mode 100644
index 0000000..3f4bf7f
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/MapViewStubContainer.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.templates.host.view.widgets.maps;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarToast;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.view.widget.map.AbstractMapViewContainer;
+import com.android.car.templates.host.view.widgets.maps.MapStub.Marker;
+import com.google.common.collect.ImmutableList;
+import dagger.hilt.android.qualifiers.ApplicationContext;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A layout that wraps a single map view and encapsulates the logic to manipulate it. */
+public class MapViewStubContainer extends AbstractMapViewContainer {
+ // Strings indicating the reason for a view update used for logging purposes.
+ private static final String UPDATE_REASON_SET_PLACES = "set_places";
+ private static final String UPDATE_REASON_SET_ANCHOR = "set_anchor";
+ private static final String UPDATE_REASON_MAP_INSETS = "map_insets";
+ private static final String UPDATE_REASON_ON_CREATE = "on_create";
+ private static final String UPDATE_REASON_ON_START = "on_start";
+ private static final int NUMBER_OF_MARKERS = 8;
+
+ @SuppressWarnings("nullness")
+ private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+
+ private final MapViewStub mMapView;
+ /** The max zoom level to ever reach in the map view. */
+ private final float mMaxZoomLevel;
+
+ private final ArrayList<Marker> mMarkers = new ArrayList<>(NUMBER_OF_MARKERS);
+
+ private TemplateContext mTemplateContext;
+ private MapStub mMap;
+
+ /** {@code true} when the view is started, {@code false} when stopped. */
+ private boolean mIsStarted;
+
+ private boolean mIsAnchorDirty;
+ private boolean mArePlacesDirty;
+
+ @Nullable private Marker mAnchorMarker;
+ private Place mAnchor;
+
+ /** whether the map should show the current location. */
+ private boolean mCurrentLocationEnabled = false;
+
+ /** A list with the places displayed on the map. */
+ private List<Place> mPlaces = ImmutableList.of();
+
+ /**
+ * Whether the view has ever completed a successful update. We use this to know whether the camera
+ * needs to be animated or not.
+ */
+ private boolean mHasUpdated = false;
+
+ /**
+ * Instantiates a new map view container.
+ *
+ * @see android.content.res.Resources.Theme#obtainStyledAttributes
+ */
+ @SuppressWarnings("nullness")
+ public MapViewStubContainer(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ LayoutInflater.from(context).inflate(com.android.car.templates.host.R.layout.map_view_stub, this);
+ mMapView = findViewById(com.android.car.templates.host.R.id.map_view);
+
+ TypedValue outValue = new TypedValue();
+ getResources().getValue(com.android.car.templates.host.R.dimen.map_max_zoom_level, outValue, true);
+ mMaxZoomLevel = outValue.getFloat();
+
+ mLifecycleRegistry.addObserver(this);
+ }
+
+ /** Instantiates a new map view container. */
+ public MapViewStubContainer(@ApplicationContext Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Instantiates a new map view container.
+ *
+ * @see android.content.res.Resources.Theme#obtainStyledAttributes
+ */
+ public MapViewStubContainer(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Instantiates a new map view container.
+ *
+ * @see android.content.res.Resources.Theme#obtainStyledAttributes
+ */
+ public MapViewStubContainer(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /** Returns an AOSPMapViewContainer */
+ public static AbstractMapViewContainer create(Context context, int theme) {
+ return (AbstractMapViewContainer)
+ View.inflate(
+ new ContextThemeWrapper(context, theme), com.android.car.templates.host.R.layout.map_view_stub_container_layout, null);
+ }
+
+ /** Returns an AOSPMapViewContainer */
+ public static MapViewStubContainer create(Context context) {
+ return (MapViewStubContainer)
+ View.inflate(context, com.android.car.templates.host.R.layout.map_view_stub_container_layout, null);
+ }
+
+ @Override
+ public void setTemplateContext(TemplateContext templateContext) {
+ mTemplateContext = templateContext;
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycleRegistry;
+ }
+
+ @NonNull
+ @Override
+ public LifecycleRegistry getLifecycleRegistry() {
+ return mLifecycleRegistry;
+ }
+
+ @Override
+ public void onCreate(LifecycleOwner owner) {
+ mMap = mMapView.getMap();
+
+ // Set the maximum zoom level, so that when we update the camera, it doesn't go past
+ // this value. The camera update logic we use tries to set the camera at the maximum
+ // level of zoom possible given a set of places to bind it to.
+ mMap.setMaxZoomPreference(mMaxZoomLevel);
+
+ // Updates the insets of the map to ensure the markers aren't drawn behind any other
+ // widgets on the screen.
+ updateMapInsets(mMap);
+
+ // Returns true to disable marker click events globally. This disables the default
+ // behavior where clicking on a marker centers it on the map.
+ mMap.setOnMarkerClickListener(new MapStub.OnMarkerClickListener());
+
+ MapStub.UiSettings uiSettings = mMap.getUiSettings();
+ uiSettings.setMyLocationButtonEnabled(false);
+ uiSettings.setAllGesturesEnabled(false);
+
+ mMap.setMyLocationEnabled(mCurrentLocationEnabled);
+ update(UPDATE_REASON_ON_CREATE);
+ }
+
+ @Override
+ public void onStart(LifecycleOwner owner) {
+ mIsStarted = true;
+ update(UPDATE_REASON_ON_START);
+ }
+
+ @Override
+ public void onStop(LifecycleOwner owner) {
+ mIsStarted = false;
+ }
+
+ @Override
+ public void setCurrentLocationEnabled(boolean enable) {
+ mCurrentLocationEnabled = true;
+ mMap.setMyLocationEnabled(mCurrentLocationEnabled);
+ }
+
+ /** Sets the map anchor. The camera will be adjusted to include the anchor marker if necessary. */
+ @Override
+ @SuppressWarnings("nullness:assignment")
+ public void setAnchor(@Nullable Place anchor) {
+ mIsAnchorDirty = true;
+ mAnchor = anchor;
+ update(UPDATE_REASON_SET_ANCHOR);
+ }
+
+ /**
+ * Sets the places to display in the map. The camera will be moved to the region that contains all
+ * the places.
+ */
+ @Override
+ public void setPlaces(List<Place> places) {
+ if (mPlaces.containsAll(places) && places.containsAll(mPlaces)) {
+ return;
+ }
+ mArePlacesDirty = true;
+ mPlaces = places;
+ update(UPDATE_REASON_SET_PLACES);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public List<Place> getPlaces() {
+ return mPlaces;
+ }
+
+ private void update(String updateReason) {
+ // Three conditions need to happen before we update the view:
+ // 1. the map needs to be initialized,
+ // 2. the view must have gone through a layout pass (so that it can calculate the viewport
+ // rect for the camera),
+ // 3. the view must be in STARTED state.
+ if (mMap == null || !mMapView.isLaidOut() || !mIsStarted) {
+ return;
+ }
+
+ Log.d(LogTags.TEMPLATE, "Updating map view, reason: " + updateReason);
+
+ // Do not animate the camera the very first time, but animate it in any subsequent updates.
+ updateCamera(/* animate= */ mHasUpdated);
+ updateAnchorMarker();
+ updatePlaceMarkers();
+ mHasUpdated = true;
+ }
+
+ private void updatePlaceMarkers() {
+ if (mMap == null || !mArePlacesDirty) {
+ return;
+ }
+
+ Log.d(LogTags.TEMPLATE, "Updating map location markers");
+
+ // Clean up the existing markers.
+ for (MapStub.Marker marker : mMarkers) {
+ mMap.removeMarker(marker);
+ }
+ mMarkers.clear();
+
+ // Add the new ones.
+ for (Place place : mPlaces) {
+ PlaceMarker marker = place.getMarker();
+ CarLocation location = place.getLocation();
+
+ if (location != null) {
+ // Skip null (invisible) markers.
+ if (marker != null) {
+ mMap.addMarker(new MapStub.Marker(location, marker.getColor()));
+ }
+ } else {
+ Log.w(LogTags.TEMPLATE, "Place location is expected but not set: " + place);
+ }
+ }
+
+ mArePlacesDirty = false;
+ }
+
+ private void updateAnchorMarker() {
+ if (mMap == null || !mIsAnchorDirty) {
+ return;
+ }
+ MapStub map = mMap;
+
+ Log.d(LogTags.TEMPLATE, "Updating map anchor marker");
+
+ // Clean up the existing marker.
+ if (mAnchorMarker != null) {
+ map.removeMarker(mAnchorMarker);
+ }
+
+ // Add the new one.
+ if (mAnchor != null) {
+ PlaceMarker marker = mAnchor.getMarker();
+ CarLocation location = mAnchor.getLocation();
+
+ if (location != null) {
+ if (marker != null) {
+ map.addMarker(new MapStub.Marker(location, requireNonNull(marker.getColor())));
+ }
+ } else {
+ Log.w(LogTags.TEMPLATE, "Anchor location is expected but not set: " + mAnchor);
+ }
+
+ mIsAnchorDirty = false;
+ }
+ }
+
+ private void updateCamera(boolean animate) {
+ boolean hasPlace = false;
+ CarLocation location = mAnchor != null ? mAnchor.getLocation() : null;
+
+ if (location != null) {
+ hasPlace = true;
+ } else {
+ Log.w(
+ LogTags.TEMPLATE,
+ "Anchor location is expected but not set, excluding from camera: " + mAnchor);
+ }
+
+ LocationMediator mediator =
+ requireNonNull(mTemplateContext.getAppHostService(LocationMediator.class));
+ if (!hasPlace) {
+ // Try to maintain the previous camera location if available.
+ CarLocation anchor = mediator.getCameraAnchor();
+ if (anchor == null) {
+ return;
+ }
+
+ location = anchor;
+ }
+
+ mediator.setCameraAnchor(location);
+ if (animate) {
+ mMap.animateCamera(requireNonNull(location), CarToast.LENGTH_SHORT);
+ } else {
+ mMap.moveCamera(requireNonNull(location));
+ }
+ }
+
+ private void updateMapInsets(MapStub map) {
+ if (mTemplateContext == null) {
+ return;
+ }
+ Rect stableArea = mTemplateContext.getSurfaceInfoProvider().getStableArea();
+ stableArea = stableArea != null ? stableArea : new Rect(0, 0, getWidth(), getHeight());
+
+ if (map != null) {
+ map.setPadding(
+ stableArea.left,
+ stableArea.top,
+ getWidth() - stableArea.right,
+ getHeight() - stableArea.bottom);
+ update(UPDATE_REASON_MAP_INSETS);
+ }
+ }
+}
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_stub_layout.xml b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_stub_layout.xml
new file mode 100644
index 0000000..50051be
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_stub_layout.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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/map_info"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:textSize="@dimen/map_text_size" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub.xml b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub.xml
new file mode 100644
index 0000000..0106df6
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub.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.
+-->
+
+<com.android.car.templates.host.view.widgets.maps.MapViewStub
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/map_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub_container_layout.xml b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub_container_layout.xml
new file mode 100644
index 0000000..0d42dc1
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/layout/map_view_stub_container_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.
+-->
+
+<com.android.car.templates.host.view.widgets.maps.MapViewStubContainer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/map_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:descendantFocusability="blocksDescendants"
+ android:focusable="false"
+ app:markerAppearance="?templateMapMarkerAppearance">
+
+
+</com.android.car.templates.host.view.widgets.maps.MapViewStubContainer>
diff --git a/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/values/dimens.xml b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/values/dimens.xml
new file mode 100644
index 0000000..0c3f320
--- /dev/null
+++ b/Host/app/src/main/java/com/android/car/templates/host/view/widgets/maps/res/values/dimens.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>
+ <!-- The max level of zoom to ever reach in the map view.
+ A value of 15 maps roughly to street level and 20 maps to building level.
+ See https://developers.google.com/maps/documentation/android-sdk/views. -->
+ <item name="map_max_zoom_level" format="float" type="dimen">17</item>
+ <dimen name="map_text_size">40sp</dimen>
+</resources>
diff --git a/Host/build.gradle b/Host/build.gradle
new file mode 100644
index 0000000..3ec9f3b
--- /dev/null
+++ b/Host/build.gradle
@@ -0,0 +1,18 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+
+ dependencies {
+ classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'
+ }
+}
+
+plugins {
+ id 'com.android.application' version '7.2.0-alpha06' apply false
+ id 'com.android.library' version '7.2.0-alpha06' apply false
+ id 'org.jetbrains.kotlin.android' version '1.6.0' apply false
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/Host/gradle.properties b/Host/gradle.properties
new file mode 100644
index 0000000..d5e8af3
--- /dev/null
+++ b/Host/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
diff --git a/Host/gradle/wrapper/gradle-wrapper.jar b/Host/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/Host/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/Host/gradle/wrapper/gradle-wrapper.properties b/Host/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..d681304
--- /dev/null
+++ b/Host/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Feb 08 16:11:22 PST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/Host/gradlew b/Host/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/Host/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/Host/gradlew.bat b/Host/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/Host/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Host/local.properties b/Host/local.properties
new file mode 100644
index 0000000..1a291d6
--- /dev/null
+++ b/Host/local.properties
@@ -0,0 +1,2 @@
+# Please set the system sdk path
+sdk.dir=${path_to_android-system-sdk}
diff --git a/Host/settings.gradle b/Host/settings.gradle
new file mode 100644
index 0000000..5c580ea
--- /dev/null
+++ b/Host/settings.gradle
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+gradle.ext.aaosLatestSDK = 31
+
+rootProject.name = "AOSPTemplateshost"
+include ':app'
+include ':app:apphost'
+include ':app:renderer'
+
+gradle.ext.lib_car_system_stubs = rootDir.absolutePath + "../../../../../../prebuilts/sdk/" + gradle.ext.aaosLatestSDK + "/system/android.car-system-stubs.jar"
+gradle.ext.lib_system_stubs = rootDir.absolutePath + "../../../../../../prebuilts/sdk/" + gradle.ext.aaosLatestSDK + "/system/android.jar"
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..fac56ee
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+igorr@google.com
+babakbo@google.com
+calhuang@google.com \ No newline at end of file