@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +214 -29
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +21 -2
  8. package/ios/Types.swift +3 -0
  9. package/ios/extensions/UIImage+Toggle.swift +5 -2
  10. package/ios/hybrid/HybridAutoPlay.swift +28 -16
  11. package/ios/hybrid/HybridMapTemplate.swift +22 -5
  12. package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
  13. package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
  14. package/ios/templates/AutoPlayTemplate.swift +24 -2
  15. package/ios/templates/GridTemplate.swift +57 -57
  16. package/ios/templates/InformationTemplate.swift +84 -9
  17. package/ios/templates/ListTemplate.swift +66 -11
  18. package/ios/templates/MapTemplate.swift +83 -68
  19. package/ios/templates/MessageTemplate.swift +72 -6
  20. package/ios/templates/Parser.swift +564 -69
  21. package/ios/templates/SearchTemplate.swift +1 -1
  22. package/ios/templates/TemplateStore.swift +5 -15
  23. package/ios/utils/SymbolFont.swift +13 -28
  24. package/lib/Constants.d.ts +4 -0
  25. package/lib/Constants.js +6 -0
  26. package/lib/index.d.ts +1 -0
  27. package/lib/index.js +1 -0
  28. package/lib/specs/MapTemplate.nitro.d.ts +3 -1
  29. package/lib/templates/GridTemplate.d.ts +23 -3
  30. package/lib/templates/GridTemplate.js +8 -0
  31. package/lib/templates/InformationTemplate.d.ts +38 -12
  32. package/lib/templates/ListTemplate.d.ts +16 -47
  33. package/lib/templates/MapTemplate.d.ts +26 -2
  34. package/lib/templates/MapTemplate.js +16 -2
  35. package/lib/templates/MessageTemplate.d.ts +52 -11
  36. package/lib/templates/MessageTemplate.js +1 -1
  37. package/lib/utils/NitroOptionsPanel.d.ts +103 -0
  38. package/lib/utils/NitroOptionsPanel.js +63 -0
  39. package/lib/utils/NitroSection.d.ts +89 -2
  40. package/lib/utils/NitroSection.js +49 -2
  41. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
  42. package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
  43. package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
  44. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
  45. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
  46. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
  47. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
  48. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
  49. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
  50. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
  51. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
  52. package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
  53. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
  54. package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
  55. package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
  56. package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
  57. package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
  58. package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
  59. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
  60. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
  61. package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
  62. package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
  63. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
  64. package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
  79. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
  80. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  81. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
  82. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
  83. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
  84. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
  85. package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
  86. package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
  87. package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
  88. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
  89. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
  90. package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
  91. package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
  92. package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
  93. package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
  94. package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
  95. package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
  96. package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
  97. package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
  98. package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
  99. package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
  100. package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
  101. package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
  102. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
  103. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
  104. package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
  105. package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
  106. package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
  107. package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
  108. package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
  109. package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
  110. package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
  111. package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
  112. package/package.json +1 -1
  113. package/src/Constants.ts +9 -0
  114. package/src/index.ts +1 -0
  115. package/src/specs/MapTemplate.nitro.ts +3 -1
  116. package/src/templates/GridTemplate.ts +38 -3
  117. package/src/templates/InformationTemplate.ts +60 -29
  118. package/src/templates/ListTemplate.ts +27 -55
  119. package/src/templates/MapTemplate.ts +60 -2
  120. package/src/templates/MessageTemplate.ts +71 -24
  121. package/src/utils/NitroOptionsPanel.ts +218 -0
  122. package/src/utils/NitroSection.ts +160 -5
package/README.md CHANGED
@@ -22,6 +22,10 @@
22
22
  - **Headless Operation:** Runs in the background to keep the automotive experience alive even when the main app is not in the foreground.
23
23
  - **Powered by [NitroModules](https://nitro.margelo.com/)**
24
24
 
25
+ ## Requirements
26
+
27
+ - **iOS builds require Xcode 27+** (the iOS 27 SDK), even for apps that don't use any `mapConfig`/panel features — the library references `CPMapPanel`/`CPPanel` types internally behind `@available(iOS 27.0, *)` checks, but `@available` only defers *runtime* execution, not compile-time symbol resolution, so the SDK must be present to build at all.
28
+
25
29
  ## Installation
26
30
 
27
31
  1. **Install the package and its peer dependencies:**
@@ -137,37 +141,36 @@ Paste this into your Info.plist and adjust it to your needs. Check [Apple docs](
137
141
 
138
142
  #### MapTemplate
139
143
  if you want to make use of the MapTemplate and render react components you need to add this to your AppDelegate.swift
140
- This should cover old and new architecture, adjust to your needs!
144
+ This is an example that works for bare react-native (>= 0.82) and Expo SDK 57, check [this](https://github.com/Iternio-Planning-AB/react-native-auto-play/blob/dbd33ff32ee58338282ffe0f8a970e687e1e3520/packages/react-native-autoplay/README.md?plain=1#L139) for older versions.
141
145
 
142
146
  ```swift
143
- @objc func getRootViewForAutoplay(
147
+ @objc func getRootViewForAutoplay(
144
148
  moduleName: String,
145
149
  initialProperties: [String: Any]?
146
150
  ) -> UIView? {
147
- if RCTIsNewArchEnabled() {
148
- if let factory = reactNativeFactory?.rootViewFactory as? ExpoReactRootViewFactory {
149
- return factory.superView(
150
- withModuleName: moduleName,
151
- initialProperties: initialProperties,
152
- launchOptions: nil
153
- )
154
- }
155
-
156
- return reactNativeFactory?.rootViewFactory.view(
151
+ var autoPlayRootView: UIView?
152
+
153
+ if let factory = reactNativeFactory?.rootViewFactory
154
+ as? ExpoReactRootViewFactory
155
+ {
156
+ autoPlayRootView = factory.superView(
157
157
  withModuleName: moduleName,
158
- initialProperties: initialProperties
158
+ initialProperties: initialProperties,
159
+ bundleConfiguration: RCTBundleConfiguration(),
160
+ devMenuConfiguration: RCTDevMenuConfiguration(),
159
161
  )
160
162
  }
161
163
 
162
- if let rootView = window?.rootViewController?.view as? RCTRootView {
163
- return RCTRootView(
164
- bridge: rootView.bridge,
165
- moduleName: moduleName,
164
+ if autoPlayRootView == nil,
165
+ let factory = reactNativeFactory?.rootViewFactory
166
+ {
167
+ autoPlayRootView = factory.view(
168
+ withModuleName: moduleName,
166
169
  initialProperties: initialProperties
167
170
  )
168
171
  }
169
172
 
170
- return nil
173
+ return autoPlayRootView
171
174
  }
172
175
  ```
173
176
 
@@ -177,6 +180,10 @@ Reason for this is that CarPlay does not allow for color updates on maneuvers sh
177
180
  The color properties do not need to handle the mode change, best practice is to use ThemedColor whenever possible and set appropriate light and dark mode colors.
178
181
  This is mainly required on CarPlay for now since Android Auto lacks light mode.
179
182
 
183
+ #### CPListTemplate day/night header
184
+
185
+ **Known CarPlay platform bug, not fixable in this library:** on `ListTemplate` (`CPListTemplate`) only, the entire header — title text and buttons alike — doesn't track live light/dark mode switches; each toggle flips it to the *opposite* of the actual current theme instead, until the template is popped and pushed again. Other templates work fine, this seems to be an iOS 26 issue only.
186
+
180
187
  #### Dashboard buttons
181
188
  In case you wanna open up your CarPlay app from one of the CarPlay dashboard buttons set `launchHeadUnitScene` on the button and add this to your Info.plist. Make sure to apply your "Bundle Identifier" instead of the example one.
182
189
  ```xml
@@ -203,6 +210,49 @@ In case you have ProGuard enabled (`def enableProguardInReleaseBuilds = true` in
203
210
  -keep class com.margelo.nitro.swe.iternio.reactnativeautoplay.** { *; }
204
211
  ```
205
212
 
213
+ #### Native backdrop under the MapTemplate surface
214
+ On Android the React content of a `MapTemplate` is rendered onto the car screen through a virtual display. Some native views cannot be hosted there as React Native views — Fragment-based map SDK wrappers, for example, are bound to the phone `Activity`. For those the library can place a host-provided native `View` **under** the React surface of a display: the Android counterpart of the iOS `getRootViewForAutoplay` hook above. Your React tree then draws on top of it as an overlay. The factory is asked for the root display and for each cluster display, and may answer `null` for either.
215
+
216
+ ```kotlin
217
+ interface NativeBackdrop {
218
+ /** Added as the presentation root's FIRST child, match-parent. */
219
+ val view: View
220
+
221
+ /** The car's day/night changed (CarContext.isDarkMode) — redraw accordingly. */
222
+ fun onColorSchemeChanged(dark: Boolean)
223
+
224
+ /** Release everything; must be idempotent. */
225
+ fun destroy()
226
+ }
227
+
228
+ /** Which car display is asking for a backdrop. */
229
+ enum class NativeBackdropDisplay { ROOT, CLUSTER }
230
+
231
+ object NativeBackdropRegistry {
232
+ /** Return null to render that display without a backdrop. */
233
+ @Volatile
234
+ var factory: ((CarContext, NativeBackdropDisplay) -> NativeBackdrop?)? = null
235
+ }
236
+ ```
237
+
238
+ Register the factory in your `Application.onCreate`, before the `CarAppService` can start:
239
+
240
+ ```kotlin
241
+ NativeBackdropRegistry.factory = { carContext, display ->
242
+ when (display) {
243
+ NativeBackdropDisplay.ROOT -> MyMapBackdrop(carContext)
244
+ NativeBackdropDisplay.CLUSTER -> null // or a second map view for the cluster
245
+ }
246
+ }
247
+ ```
248
+
249
+ Lifecycle contract:
250
+
251
+ - The factory is consulted once per presentation of each display — i.e. again after every surface resize — and each backdrop is destroyed when its presentation is replaced or the renderer stops. A `destroy()` that throws is logged and does not interrupt teardown.
252
+ - A factory that throws is logged and ignored; the React surface still renders.
253
+ - The React surface view is transparent only while a backdrop is attached. Without a registered factory nothing changes: the surface stays opaque as before.
254
+ - `onColorSchemeChanged(dark)` is forwarded from each session's `onCarConfigurationChanged` to that display's backdrop, so it can follow the car's day/night setting (car app quality guideline MR-1). It fires regardless of which template is currently on screen.
255
+
206
256
  ### Android Auto Customization
207
257
  You can customize certain behaviors of the library on Android Auto by setting properties in your app's `android/gradle.properties` file.
208
258
 
@@ -542,6 +592,7 @@ Below is a concise overview of the most important props per template. Optional p
542
592
  | `headerActions` | `MapHeaderActions<MapTemplate>` | ❌ | Top action strip. See **Header Actions** below. |
543
593
  | `mapButtons` | `MapButtons<MapTemplate>` | ❌ | 1–4 map buttons shown on the map. To get working gestures on the MapTemplate running on Android Auto you have to add a `MapPanButton` |
544
594
  | `visibleTravelEstimate` | `'first'` `'last'` | ❌ | Which travel estimate to display. |
595
+ | `optionsPanel` | `OptionsPanelConfig<MapTemplate>` | ❌ | **iOS 27+ only, no-op on Android.** Panel shown when tapping the ellipsis button next to the travel estimates during active navigation. See **Options Panel** below. |
545
596
  | `onDidPan` / `onDidUpdateZoomGestureWithCenter` | callbacks | ❌ | Map gesture events. |
546
597
  | `onAppearanceDidChange` | `(colorScheme) => void` | ❌ | Listen for light/dark mode changes. |
547
598
  | `onAutoDriveEnabled` | `(template) => void` | ⚠️ | Android-only auto drive callback. Make sure to take action when receiving this and simulate a drive to the set destination. [Check Android docs for details](https://developer.android.com/reference/androidx/car/app/navigation/NavigationManagerCallback#onAutoDriveEnabled()) |
@@ -553,7 +604,7 @@ Below is a concise overview of the most important props per template. Optional p
553
604
  | `title` | `AutoText` | ✅ | Header title. |
554
605
  | `sections` | `Section<ListTemplate>` | ❌ | List sections/rows. Not providing anything here will result in a loading indicator on Android and an empty list on iOS. |
555
606
  | `headerActions` | `HeaderActions<ListTemplate>` | ❌ | Header actions. See **Header Actions** below. |
556
- | `mapConfig` | `BaseMapTemplateConfig<ListTemplate>` | ❌ | Android map-with-content layout. |
607
+ | `mapConfig` | `BaseMapTemplateConfig<ListTemplate>` | ❌ | Android map-with-content layout. **iOS 27+**: renders as a `CPMapPanel` on the current root map template instead. See **Map + Content** below. |
557
608
 
558
609
  #### GridTemplateConfig
559
610
 
@@ -562,7 +613,8 @@ Below is a concise overview of the most important props per template. Optional p
562
613
  | `title` | `AutoText` | ✅ | Header title. |
563
614
  | `buttons` | `GridButton<GridTemplate>[]` | ✅ | Grid items. Providing an empty array will result in a loading indicator on Android and an empty template on iOS. |
564
615
  | `headerActions` | `HeaderActions<GridTemplate>` | ❌ | Header actions. See **Header Actions** below. |
565
- | `mapConfig` | `BaseMapTemplateConfig<GridTemplate>` | ❌ | Android map-with-content layout. |
616
+ | `imageSize` | `'unset'` `'large'` `'medium'` `'small'` | ❌ | **Android only**, requires Android Car API 8. Controls grid item image size; defaults to `unset` (platform default layout). Ignored (with a `__DEV__` warning) when `mapConfig` is also set — `MapWithContentTemplate` doesn't support the sized grid content type. |
617
+ | `mapConfig` | `BaseMapTemplateConfig<GridTemplate>` | ❌ | Android map-with-content layout. **iOS 27+**: renders as a `CPMapPanel` on the current root map template instead. See **Map + Content** below. |
566
618
 
567
619
  #### SearchTemplateConfig
568
620
 
@@ -582,9 +634,9 @@ Below is a concise overview of the most important props per template. Optional p
582
634
  | --- | --- | --- | --- |
583
635
  | `title` | `AutoText` | ✅ | Header title. |
584
636
  | `items` | `InformationItems` | ❌ | 1–4 rows. |
585
- | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. |
637
+ | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. **iOS 27+ with `mapConfig` set**: at most 1 `TextButton` plus 1 icon-only `ImageButton`, enforced at the type level. |
586
638
  | `headerActions` | `HeaderActions<InformationTemplate>` | ❌ | Header actions. See **Header Actions** below. |
587
- | `mapConfig` | `BaseMapTemplateConfig<InformationTemplate>` | ❌ | Android map-with-content layout. |
639
+ | `mapConfig` | `BaseMapTemplateConfig<InformationTemplate>` | ❌ | Android map-with-content layout. **iOS 27+**: renders as a `CPMapPanel` on the current root map template instead. See **Map + Content** below. |
588
640
 
589
641
  #### MessageTemplateConfig
590
642
 
@@ -593,9 +645,9 @@ Below is a concise overview of the most important props per template. Optional p
593
645
  | `message` | `AutoText` | ✅ | Main message text. |
594
646
  | `title` | `AutoText` | ❌ | Android header title. |
595
647
  | `image` | `AutoImage` | ❌ | Android-only image above the message. |
596
- | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. |
597
- | `headerActions` | `HeaderActionsAndroid<MessageTemplate>` | ❌ | Android-only header actions. |
598
- | `mapConfig` | `BaseMapTemplateConfig<MessageTemplate>` | ❌ | Android map-with-content layout. |
648
+ | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. **iOS 27+ with `mapConfig` set**: at most 1 `TextButton` plus 1 icon-only `ImageButton`, enforced at the type level. |
649
+ | `headerActions` | `HeaderActions<MessageTemplate>` | ❌ | Header actions. See **Header Actions** below. **iOS**: `ios` only takes effect once this renders as a `CPMapPanel` (`mapConfig` set, iOS 27+) — without `mapConfig` (or below iOS 27) this is a full-screen `CPAlertTemplate` with no nav bar, so `ios` is silently unused. |
650
+ | `mapConfig` | `BaseMapTemplateConfig<MessageTemplate>` | ❌ | Android map-with-content layout. **iOS 27+**: renders as a `CPMapPanel` on the current root map template instead, trading the usual full-screen modal alert for panel content. See **Map + Content** below. |
599
651
 
600
652
  #### SignInTemplateConfig (Android-only)
601
653
 
@@ -818,11 +870,11 @@ useEffect(() => {
818
870
  | Template | Purpose | Notes |
819
871
  | --- | --- | --- |
820
872
  | `MapTemplate` | Navigation, map rendering | Use as root; supports map buttons & navigation APIs. |
821
- | `ListTemplate` | Lists/menus | Supports sections, radio/toggle rows. |
822
- | `GridTemplate` | Action grid | Use `GridButton` items. |
873
+ | `ListTemplate` | Lists/menus | Supports sections, radio/toggle rows. Can render as a CarPlay map panel, see **Map + Content**. |
874
+ | `GridTemplate` | Action grid | Use `GridButton` items. Can render as a CarPlay map panel, see **Map + Content**. |
823
875
  | `SearchTemplate` | Search UI | Android-only search bar callbacks. |
824
- | `InformationTemplate` | Info panels | Android uses PaneTemplate; iOS uses InformationTemplate. |
825
- | `MessageTemplate` | Modal messages | Always shown on top until popped. |
876
+ | `InformationTemplate` | Info panels | Android uses PaneTemplate; iOS uses InformationTemplate. Can render as a CarPlay map panel, see **Map + Content**. |
877
+ | `MessageTemplate` | Modal messages | Always shown on top until popped (a true full-screen modal alert on iOS). Can render as a CarPlay map panel instead, see **Map + Content**. |
826
878
 
827
879
  **Template quick examples:**
828
880
 
@@ -843,6 +895,137 @@ new ListTemplate({
843
895
  }).push();
844
896
  ```
845
897
 
898
+ ### Map + Content (`mapConfig`)
899
+
900
+ `ListTemplate`, `GridTemplate`, `InformationTemplate`, and `MessageTemplate` all accept an optional `mapConfig` prop. Setting it (an empty object is enough — no actions need to be specified) gives the template a map background instead of its normal full-screen presentation. The two platforms implement this completely differently, so behavior and limitations differ accordingly.
901
+
902
+ ```ts
903
+ new ListTemplate({
904
+ title: { text: 'Nearby' },
905
+ sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
906
+ mapConfig: {},
907
+ }).push();
908
+ ```
909
+
910
+ #### Android
911
+
912
+ `mapConfig` wraps the template in a `MapWithContentTemplate`, giving it a map background while the template's own content (list, grid, info, or message) is laid out on top.
913
+
914
+ #### iOS (27+)
915
+
916
+ `mapConfig` instead renders the template as a [`CPMapPanel`](https://developer.apple.com/documentation/carplay/cpmappanel) — an overlay shown **on the current root map template** (a `MapTemplate` set via `setRootTemplate()`). On iOS versions below 27, `mapConfig` is currently a no-op and the template renders normally (there is no map-background equivalent pre-27).
917
+
918
+ ```ts
919
+ // Root map template must already be set for the panel to have somewhere to attach to
920
+ new MapTemplate({ component: MapScreen, onStopNavigation: () => {} }).setRootTemplate();
921
+
922
+ // Pushing this on top now shows it as an overlay panel on the map, instead of a full-screen list.
923
+ // headerActions.ios.backButton is required here — as the first (and only) panel in the stack it
924
+ // gets no native close/back control (see "Things that behave differently in panel mode" below),
925
+ // so without it the driver has no way to leave the panel.
926
+ new ListTemplate({
927
+ title: { text: 'Nearby' },
928
+ sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
929
+ headerActions: { ios: { backButton: { type: 'back', onPress: () => HybridAutoPlay.popTemplate() } } },
930
+ mapConfig: {},
931
+ }).push();
932
+ ```
933
+
934
+ **Panels share the same push/pop stack as regular templates** — this is a library-level abstraction, not how Apple's API actually works. From your JS code's perspective, `.push()`, `HybridAutoPlay.popTemplate()`/`popToRootTemplate()`/`popToTemplate()`, and the lifecycle callbacks (`onWillAppear`, `onDidAppear`, etc.) behave the same whether the top of the stack is a panel or a regular pushed template — you can mix and pop through both without caring which is which. Natively, however, `CPMapPanel` is **not** part of `CPInterfaceController`'s template stack at all — Apple's API gives it its own, completely separate panel stack that lives on the `CPMapTemplate` that pushed it (`pushPanel`/`popPanel`/`CPMapPanelDelegate`, unrelated to `CPInterfaceController.pushTemplate`/`popTemplate`). This library tracks both stacks together internally and presents one unified stack to JS, so if you go looking at Apple's CarPlay documentation expecting to see panels integrated with `CPInterfaceController`, you won't find it there — that integration is something this library provides on top.
935
+
936
+ **Things that behave differently in panel mode:**
937
+
938
+ - **`headerActions`/`mapButtons` ownership**: while a panel is shown, it takes over the root map template's bar buttons and floating map buttons — using the panel template's **own** `headerActions`/`mapConfig.mapButtons`, not `mapConfig.headerActions` (which is Android-only; on iOS it's ignored, since there's no separate header for the map behind a panel). The map template's own buttons are restored automatically once the panel is popped.
939
+ - **The first panel must provide its own way to be closed**: CarPlay's native close button (✕) is always disabled on every panel — this library turns it off globally, and this is required for correct lifecycle tracking, not a style choice. Tapping ✕ on the topmost panel doesn't just pop that one panel — it discards the *entire* panel stack down to the map, covered panels included — but `CPMapPanelDelegate.panelDidHide` only ever fires once, for the topmost panel. This library would have no callback at all for the covered panels CarPlay silently destroyed underneath it: they'd stay tracked forever, `onPopped` would never fire for them, and their listeners/native templates would leak. The back chevron doesn't have this problem — it only ever pops one level, always the topmost panel — so it's left enabled: once a second panel is pushed, CarPlay shows it automatically to return to the first, and it isn't customizable. The first panel in the stack gets no such control, though, so it needs its own way out: `headerActions.ios.backButton` (supported by all four panel-capable templates, including `MessageTemplate` once `mapConfig` is set) or something inside the panel's own content (a list item, or `MessageTemplate`'s required `actions.ios[0]` `TextButton`) that calls `popTemplate()`/`popToRootTemplate()`. Without one, the driver has no way to leave that first panel short of `autoDismissMs`.
940
+ - **`InformationTemplate`/`MessageTemplate` `actions`**: a `CPMapPanel`'s button configuration only supports one `TextButton` (with a title) plus one optional icon-only `ImageButton` (any title on it is dropped natively) — far fewer than the up-to-3-`TextButton` shape available without `mapConfig`. The type system enforces this: `actions.ios` is restricted to `[TextButton]` or `[TextButton, ImageButton]` whenever `mapConfig` is set.
941
+ - **`MessageTemplate` stops being a true modal**: normally `MessageTemplate` is a full-screen, blocking alert (`CPAlertTemplate`) that covers everything regardless of OS version. With `mapConfig` set, it instead becomes dismissible panel content in the regular push/pop stack — a deliberate trade-off, not a partial implementation.
942
+
943
+ **Known iOS 27 beta limitations** (not something fixable in this library — re-test against newer betas):
944
+
945
+ - The optional icon-only `symbolButton` in a panel's button configuration does not appear to respond to taps at all on this beta — the button renders correctly, but its press handler is never invoked by CarPlay.
946
+ - `toggle` row accessory images render noticeably smaller inside a panel than in a regular (non-panel) `ListTemplate` — this is how Apple sizes `CPListItem.accessoryImage` on panels specifically, not something this library controls (see the `CPListItem.accessoryImage` known issue under **Options Panel** for the same underlying sizing bug's non-panel form).
947
+
948
+ ### Waypoint Rows (`type: 'waypoint'`)
949
+
950
+ Any list section (`ListTemplate.sections`, or an `OptionsPanel` list section — see below) can include a `waypoint` row alongside the usual `default`/`toggle`/`radio`/`text` rows:
951
+
952
+ ```ts
953
+ {
954
+ type: 'waypoint',
955
+ title: { text: 'Supercharger' },
956
+ address: 'Main St 1\n1234 Springfield',
957
+ coordinate: { latitude: 48.2, longitude: 16.37 },
958
+ travelEstimates: {
959
+ distance: { unit: 'kilometers', value: 12 },
960
+ duration: { timezone: 'Europe/Vienna', seconds: 600 },
961
+ visible: true,
962
+ },
963
+ image: { type: 'glyph', name: 'pin_drop' },
964
+ onPress: () => {},
965
+ }
966
+ ```
967
+
968
+ **iOS 27+ inside a `CPMapPanel`** (i.e. the enclosing `ListTemplate`/`GridTemplate` has `mapConfig` set, or this row is part of an `OptionsPanel` list section): renders as a real [`CPMapTemplateWaypoint`](https://developer.apple.com/documentation/carplay/cpmaptemplatewaypoint) item — `title` becomes the name, `address` the address, `image` the leading image (see the known-issue note below on image sizing). `travelEstimates.distance`/`.duration` are always sent to the native waypoint object (CarPlay requires them structurally), but they're **not shown by the waypoint item itself** — set `travelEstimates.visible: true` to additionally insert a sibling native [`CPTravelEstimates`](https://developer.apple.com/documentation/carplay/cptravelestimates) row right after it. This is a static snapshot, not live-updating — re-set `distance`/`duration` yourself (e.g. via `updateSections`/`updateOptionsPanel`) if it needs to track a changing location; there's no lighter-weight update path for just this value today.
969
+
970
+ **Everywhere else** (non-panel `ListTemplate`, Android, iOS < 27): falls back to a plain row, using `title` as the row title and `address` as the detail text — `travelEstimates.visible` has no effect here. Instead, reference `TextPlaceholders.Distance`/`TextPlaceholders.Duration` inside `title.text`/`address` yourself and this library fills them in automatically (the same substitution mechanism `AutoText.distance`/`.duration` already do everywhere):
971
+
972
+ ```ts
973
+ {
974
+ type: 'waypoint',
975
+ title: { text: `Supercharger (${TextPlaceholders.Distance})` },
976
+ address: `Main St 1 · ${TextPlaceholders.Duration} away`,
977
+ travelEstimates: { distance: { unit: 'kilometers', value: 12 }, duration: { timezone: 'Europe/Vienna', seconds: 600 } },
978
+ coordinate: { latitude: 48.2, longitude: 16.37 },
979
+ onPress: () => {},
980
+ }
981
+ ```
982
+
983
+ ### Options Panel (`optionsPanel`, iOS 27+)
984
+
985
+ `MapTemplate`'s `optionsPanel` prop configures the panel CarPlay shows when the user taps the ellipsis button next to the travel estimates during active navigation. It's a no-op on Android and on iOS below 27.
986
+
987
+ ```ts
988
+ mapTemplate.updateOptionsPanel({
989
+ title: { text: 'Trip options' },
990
+ sections: [
991
+ {
992
+ type: 'list',
993
+ title: 'Route',
994
+ items: [{ type: 'default', title: { text: 'Avoid tolls' }, onPress: () => {} }],
995
+ },
996
+ {
997
+ type: 'charger',
998
+ title: 'Charger',
999
+ location: {
1000
+ name: 'Fast Network Inc.',
1001
+ address: 'Main St 1',
1002
+ coordinate: { latitude: 48.2, longitude: 16.37 },
1003
+ travelEstimates: {
1004
+ distance: { unit: 'kilometers', value: 12 },
1005
+ duration: { timezone: 'Europe/Vienna', seconds: 600 },
1006
+ visible: true,
1007
+ },
1008
+ onPress: () => {},
1009
+ },
1010
+ outlets: [{ connector: 'ccs2', voltage: 400, powerKw: 300, onPress: () => {} }],
1011
+ },
1012
+ ],
1013
+ });
1014
+ ```
1015
+
1016
+ A section is one of:
1017
+
1018
+ | `type` | Renders as | Notes |
1019
+ | --- | --- | --- |
1020
+ | `'list'` | Rows (`default`/`toggle`/`radio`/`text`/`waypoint`) | Same row types and behavior as a regular `ListTemplate` section. |
1021
+ | `'grid'` | A row of `GridButton`s | Same shape as `GridTemplate.buttons`. |
1022
+ | `'charger'` | One `CPChargingStationConnection` item per outlet | `outlets[].connector` is one of `ccs1`/`ccs2`/`j1772`/`chaDeMo`/`mennekes`/`gbtDC`/`gbtAC`/`nacsDC`/`nacsAC`; `powerKw` above 1000 is shown in MW natively. `location` is optional and behaves exactly like a `waypoint` row's panel behavior above (own `CPMapTemplateWaypoint` item, `travelEstimates.visible` for the sibling estimate row) — except it uses `location.name` instead of a `title`, since the section's own `title` is already shown as the header (repeating it on the item would look redundant). |
1023
+
1024
+ **Known iOS 27 beta issues affecting waypoint/options-panel content** (not fixable in this library — re-test against newer betas; each was confirmed by direct testing, several already have Apple Feedback reports filed):
1025
+
1026
+ - **Custom (non-system) images are unreliable across several of these newer panel APIs.** A `waypoint` row's/`ChargerLocation`'s glyph `image` overflows at `CPNavigationAlert.maximumAvatarImageSize` on iOS 27 — worked around by dividing the requested size by `traitCollection.displayScale`, which fixes the overflow but introduces some blur (a real tradeoff, not a full fix). Non-glyph custom images have no known-good size at all — everything from explicit point sizes to real custom `UIImage.isSymbolImage` assets was tried without a reliable, correctly-sized result; only genuine **system** symbols (`UIImage(systemName:)`) size correctly there. Expect `image` on a waypoint/charger row to render, but not necessarily at a sensible or crisp size.
1027
+ - **`CPListItem.accessoryImage` (used for `toggle` rows) renders at some fixed, undersized footprint on iOS 27, regardless of the image's content, size, scale, or whether it's a real symbol image** — confirmed via extensive testing (content proportions, render scale, post-hoc scale metadata, genuine `UIImage.isSymbolImage` assets from both the app's own bundle and a library-owned resource bundle). Reproduces on a plain (non-panel) `ListTemplate` too, so it isn't specific to panels or to this library's usage of the API. No workaround found; filed as Apple Feedback.
1028
+
846
1029
  ### Voice Input
847
1030
 
848
1031
  The library provides a cross-platform in-app voice recording API built on top of the car microphone (when connected) or the device microphone (when no car is connected). The voice API lives in `HybridVoice`.
@@ -1095,6 +1278,8 @@ In case you are using Expo SDK >= 56 make sure to set `buildReactNativeFromSourc
1095
1278
  // Hide the splash screen for the CarPlay screen
1096
1279
  hideAsync(AutoPlayModules.AutoPlayRoot);
1097
1280
  ```
1281
+ - **CarPlay map panels (iOS 27 beta)**: a panel's optional icon-only `symbolButton` does not respond to taps. See **Map + Content** above for details. This is a beta platform limitation, not a bug in this library — re-test against newer iOS 27 betas.
1282
+ - **Waypoint/options-panel images and toggle-row sizing (iOS 27 beta)**: custom images on a `waypoint` row/`ChargerLocation` have no reliable size, and `CPListItem.accessoryImage` (`toggle` rows) renders at an undersized fixed footprint regardless of the image supplied. See **Waypoint Rows** above for details. Beta platform limitations, not bugs in this library — an Apple Feedback report has been filed for the `accessoryImage` issue.
1098
1283
  ### Android
1099
1284
  - **Broken exceptions with `react-native`** up to version 0.79
1100
1285
  When using react-native before 0.80.0 exceptions are broken and are reported as `Unknown runtime_error` or similar.
@@ -113,6 +113,11 @@ class AndroidAutoSession(sessionInfo: SessionInfo) :
113
113
  override fun onCarConfigurationChanged(configuration: Configuration) {
114
114
  val colorScheme = if (carContext.isDarkMode) ColorScheme.DARK else ColorScheme.LIGHT
115
115
 
116
+ // This display's native backdrop (root or cluster) must follow the car's day/night
117
+ // (car-app quality MR-1). Forwarded before the early returns below — the root template
118
+ // may not be a MapTemplate yet (e.g. a pre-trip MessageTemplate) and must still switch.
119
+ VirtualRenderer.onColorSchemeChanged(moduleName, carContext.isDarkMode)
120
+
116
121
  if (clusterId != null) {
117
122
  HybridCluster.emitColorScheme(clusterId, colorScheme)
118
123
  AndroidAutoScreen.getScreen(clusterId)?.applyConfigUpdate(invalidate = true)
@@ -147,8 +147,10 @@ class HybridMapTemplate : HybridMapTemplateSpec() {
147
147
 
148
148
  override fun startNavigation(
149
149
  templateId: String, trip: TripConfig
150
- ) {
151
- MapTemplate.startNavigation(trip)
150
+ ): Promise<Unit> {
151
+ return Promise.async {
152
+ MapTemplate.startNavigation(trip)
153
+ }
152
154
  }
153
155
 
154
156
  override fun stopNavigation(templateId: String) {
@@ -158,4 +160,9 @@ class HybridMapTemplate : HybridMapTemplateSpec() {
158
160
  override fun setManeuverState(templateId: String, state: ManeuverState) {
159
161
  // Android Auto does not have an equivalent to CPManeuverState
160
162
  }
163
+
164
+ override fun updateOptionsPanel(templateId: String, config: NitroOptionsPanelConfig?): Promise<Unit> {
165
+ // Android Auto has no equivalent to CarPlay's navigation session options panel
166
+ return Promise.async {}
167
+ }
161
168
  }
@@ -0,0 +1,40 @@
1
+ package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
+
3
+ import android.view.View
4
+ import androidx.car.app.CarContext
5
+
6
+ /**
7
+ * An optional host-app-provided native View rendered under the React surface of an
8
+ * Android Auto display (root or cluster) — the Android counterpart of the iOS
9
+ * `getRootViewForAutoplay` AppDelegate hook. Lets a host compose a native map beneath its React overlay, e.g. a
10
+ * native map view that cannot be hosted as a React Native view on the virtual display
11
+ * (Fragment-based map SDK wrappers are bound to the phone Activity).
12
+ *
13
+ * Contract: register `NativeBackdropRegistry.factory` before the CarAppService starts
14
+ * (Application.onCreate). The factory is consulted once per presentation (i.e. again after
15
+ * every surface resize); each backdrop is destroyed when its presentation is replaced or the
16
+ * renderer stops. A factory that throws is logged and ignored — the surface still renders.
17
+ */
18
+ interface NativeBackdrop {
19
+ /** Added as the presentation root's FIRST child, match-parent. */
20
+ val view: View
21
+
22
+ /** The car's day/night changed (CarContext.isDarkMode) — redraw accordingly. */
23
+ fun onColorSchemeChanged(dark: Boolean)
24
+
25
+ /** Release everything; must be idempotent. */
26
+ fun destroy()
27
+ }
28
+
29
+ /** Which car display is asking for a backdrop. */
30
+ enum class NativeBackdropDisplay { ROOT, CLUSTER }
31
+
32
+ object NativeBackdropRegistry {
33
+ /**
34
+ * Consulted once per presentation for the root display AND for each cluster display.
35
+ * Return null to render that display without a backdrop (e.g. a host with a single
36
+ * native map view may serve the root only). A throwing factory is logged and ignored.
37
+ */
38
+ @Volatile
39
+ var factory: ((CarContext, NativeBackdropDisplay) -> NativeBackdrop?)? = null
40
+ }
@@ -7,6 +7,7 @@ import android.graphics.Rect
7
7
  import android.hardware.display.DisplayManager
8
8
  import android.hardware.display.VirtualDisplay
9
9
  import android.os.Bundle
10
+ import android.util.Log
10
11
  import android.view.ContextThemeWrapper
11
12
  import android.view.Display
12
13
  import android.view.LayoutInflater
@@ -42,6 +43,10 @@ class VirtualRenderer(
42
43
  ) {
43
44
  private var virtualDisplay: VirtualDisplay? = null
44
45
  private val pendingDisplays = mutableListOf<VirtualDisplay>()
46
+ // Optional host view under the React surface (see NativeBackdrop), one per presentation;
47
+ // parked ones are destroyed in the same sweep as pendingDisplays
48
+ private var currentBackdrop: NativeBackdrop? = null
49
+ private val pendingBackdrops = mutableListOf<NativeBackdrop>()
45
50
 
46
51
  private var reactSurfaceImpl: ReactSurfaceImpl? = null
47
52
  private var reactSurfaceView: ReactSurfaceView? = null
@@ -408,14 +413,39 @@ class VirtualRenderer(
408
413
  }
409
414
 
410
415
 
416
+ // Ask the host for a native backdrop for this display; the host is told whether it
417
+ // is the root or a cluster and may answer null for either. `this@VirtualRenderer.context`
418
+ // is the CarContext — the presentation's own `context` parameter is the ReactContext
419
+ // and shadows it. A throwing factory is logged and ignored so the React surface still
420
+ // renders.
421
+ val display = if (isCluster) NativeBackdropDisplay.CLUSTER else NativeBackdropDisplay.ROOT
422
+ val backdrop: NativeBackdrop? = NativeBackdropRegistry.factory?.let { factory ->
423
+ runCatching { factory(this@VirtualRenderer.context, display) }
424
+ .onFailure { Log.w(TAG, "native backdrop factory failed ($display); rendering without it", it) }
425
+ .getOrNull()
426
+ }
427
+ currentBackdrop?.let { pendingBackdrops.add(it) }
428
+ currentBackdrop = backdrop
429
+
411
430
  val rootContainer = FrameLayout(themedContext).apply {
412
431
  layoutParams = FrameLayout.LayoutParams(
413
432
  FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
414
433
  )
415
434
  clipChildren = false
416
435
 
436
+ backdrop?.let {
437
+ // A view the host hands out more than once must not crash Presentation.onCreate
438
+ (it.view.parent as? ViewGroup)?.removeView(it.view)
439
+ addView(it.view, FrameLayout.LayoutParams(
440
+ FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
441
+ ))
442
+ }
417
443
  addView(reactSurfaceView)
418
444
  }
445
+ // The surface view is painted opaque DKGRAY at construction and survives resizes by
446
+ // re-parenting, so (re)apply per presentation: see-through only while a backdrop is
447
+ // actually attached.
448
+ reactSurfaceView?.setBackgroundColor(if (backdrop != null) Color.TRANSPARENT else Color.DKGRAY)
419
449
 
420
450
  splashScreenView?.let {
421
451
  rootContainer.addView(it)
@@ -434,6 +464,9 @@ class VirtualRenderer(
434
464
  it.release()
435
465
  }
436
466
  pendingDisplays.clear()
467
+ // Backdrops share the lifetime of the displays they drew on
468
+ pendingBackdrops.forEach { destroyBackdropQuietly(it, "parked") }
469
+ pendingBackdrops.clear()
437
470
  }
438
471
  }
439
472
  })
@@ -509,8 +542,22 @@ class VirtualRenderer(
509
542
  }
510
543
  }
511
544
 
545
+ /** Host code, same trust boundary as the factory: log and continue on failure. */
546
+ private fun destroyBackdropQuietly(backdrop: NativeBackdrop, which: String) {
547
+ runCatching { backdrop.destroy() }
548
+ .onFailure { Log.w(TAG, "native backdrop ($which) destroy failed; continuing teardown", it) }
549
+ }
550
+
512
551
  @MainThread
513
552
  private fun stop() {
553
+ // Current and parked — a stop during a resize must not leak the backdrop whose
554
+ // replacement never drew. Host destroy() is guarded like the factory call: a
555
+ // throwing host must not skip the virtual-display release and surface teardown below.
556
+ currentBackdrop?.let { destroyBackdropQuietly(it, "current") }
557
+ currentBackdrop = null
558
+ pendingBackdrops.forEach { destroyBackdropQuietly(it, "parked") }
559
+ pendingBackdrops.clear()
560
+
514
561
  virtualDisplay?.release()
515
562
  virtualDisplay = null
516
563
 
@@ -554,5 +601,10 @@ class VirtualRenderer(
554
601
  virtualRenderer[moduleId]?.stop()
555
602
  virtualRenderer.remove(moduleId)
556
603
  }
604
+
605
+ // Forwarded by AndroidAutoSession.onCarConfigurationChanged
606
+ fun onColorSchemeChanged(moduleId: String, dark: Boolean) {
607
+ virtualRenderer[moduleId]?.currentBackdrop?.onColorSchemeChanged(dark)
608
+ }
557
609
  }
558
610
  }
@@ -1,14 +1,19 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay.template
2
2
 
3
3
  import androidx.car.app.CarContext
4
+ import androidx.car.app.annotations.ExperimentalCarApi
4
5
  import androidx.car.app.model.GridItem
6
+ import androidx.car.app.model.GridSection
5
7
  import androidx.car.app.model.GridTemplate
6
8
  import androidx.car.app.model.ItemList
9
+ import androidx.car.app.model.SectionedItemTemplate
7
10
  import androidx.car.app.model.Template
8
11
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.GridTemplateConfig
12
+ import com.margelo.nitro.swe.iternio.reactnativeautoplay.GridImageSize
9
13
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroAction
10
14
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroGridButton
11
15
 
16
+ @OptIn(ExperimentalCarApi::class)
12
17
  class GridTemplate(context: CarContext, config: GridTemplateConfig) :
13
18
  AndroidAutoTemplate<GridTemplateConfig>(context, config) {
14
19
 
@@ -18,7 +23,33 @@ class GridTemplate(context: CarContext, config: GridTemplateConfig) :
18
23
  override val autoDismissMs = config.autoDismissMs
19
24
 
20
25
  override fun parse(): Template {
21
- val template = GridTemplate.Builder().apply {
26
+ val imageSize = config.imageSize
27
+ val template = when {
28
+ // SectionedItemTemplate (needed for per-item imageSize) is not a valid content
29
+ // template for MapWithContentTemplate, so imageSize is ignored while mapConfig is set.
30
+ config.mapConfig != null -> createGridTemplate()
31
+ imageSize == null || imageSize == GridImageSize.UNSET -> createGridTemplate()
32
+ else -> createSizedGridTemplate(imageSize)
33
+ }
34
+
35
+ return Parser.parseMapWithContentConfig(context, config.mapConfig, template)
36
+ }
37
+
38
+ private fun createGridTemplate(): Template = GridTemplate.Builder().apply {
39
+ setHeader(Parser.parseHeader(context, config.title, config.headerActions))
40
+
41
+ if (config.buttons.isEmpty()) {
42
+ setLoading(true)
43
+ return@apply
44
+ }
45
+
46
+ setSingleList(ItemList.Builder().apply {
47
+ config.buttons.forEach { button -> addItem(createGridItem(button)) }
48
+ }.build())
49
+ }.build()
50
+
51
+ private fun createSizedGridTemplate(imageSize: GridImageSize): Template =
52
+ SectionedItemTemplate.Builder().apply {
22
53
  setHeader(Parser.parseHeader(context, config.title, config.headerActions))
23
54
 
24
55
  if (config.buttons.isEmpty()) {
@@ -26,20 +57,23 @@ class GridTemplate(context: CarContext, config: GridTemplateConfig) :
26
57
  return@apply
27
58
  }
28
59
 
29
- setSingleList(ItemList.Builder().apply {
30
- config.buttons.forEachIndexed { index, button ->
31
- addItem(GridItem.Builder().apply {
32
- setTitle(Parser.parseText(button.title))
33
- setOnClickListener(button.onPress)
34
- button.image.let { image ->
35
- setImage(Parser.parseImage(context, image))
36
- }
37
- }.build())
38
- }
60
+ addSection(GridSection.Builder().apply {
61
+ setItemSize(imageSize.toItemSize())
62
+ config.buttons.forEach { button -> addItem(createGridItem(button)) }
39
63
  }.build())
40
64
  }.build()
41
65
 
42
- return Parser.parseMapWithContentConfig(context, config.mapConfig, template)
66
+ private fun createGridItem(button: NitroGridButton): GridItem = GridItem.Builder().apply {
67
+ setTitle(Parser.parseText(button.title))
68
+ setOnClickListener(button.onPress)
69
+ setImage(Parser.parseImage(context, button.image))
70
+ }.build()
71
+
72
+ private fun GridImageSize.toItemSize(): Int = when (this) {
73
+ GridImageSize.LARGE -> GridSection.ITEM_SIZE_LARGE
74
+ GridImageSize.MEDIUM -> GridSection.ITEM_SIZE_MEDIUM
75
+ GridImageSize.SMALL -> GridSection.ITEM_SIZE_SMALL
76
+ GridImageSize.UNSET -> error("An unset grid image size has no item size")
43
77
  }
44
78
 
45
79
  override fun setTemplateHeaderActions(headerActions: Array<NitroAction>?) {
@@ -72,4 +106,4 @@ class GridTemplate(context: CarContext, config: GridTemplateConfig) :
72
106
  config = config.copy(buttons = buttons)
73
107
  super.applyConfigUpdate()
74
108
  }
75
- }
109
+ }