@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.
- package/README.md +214 -29
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +21 -2
- package/ios/Types.swift +3 -0
- package/ios/extensions/UIImage+Toggle.swift +5 -2
- package/ios/hybrid/HybridAutoPlay.swift +28 -16
- package/ios/hybrid/HybridMapTemplate.swift +22 -5
- package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
- package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
- package/ios/templates/AutoPlayTemplate.swift +24 -2
- package/ios/templates/GridTemplate.swift +57 -57
- package/ios/templates/InformationTemplate.swift +84 -9
- package/ios/templates/ListTemplate.swift +66 -11
- package/ios/templates/MapTemplate.swift +83 -68
- package/ios/templates/MessageTemplate.swift +72 -6
- package/ios/templates/Parser.swift +564 -69
- package/ios/templates/SearchTemplate.swift +1 -1
- package/ios/templates/TemplateStore.swift +5 -15
- package/ios/utils/SymbolFont.swift +13 -28
- package/lib/Constants.d.ts +4 -0
- package/lib/Constants.js +6 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/specs/MapTemplate.nitro.d.ts +3 -1
- package/lib/templates/GridTemplate.d.ts +23 -3
- package/lib/templates/GridTemplate.js +8 -0
- package/lib/templates/InformationTemplate.d.ts +38 -12
- package/lib/templates/ListTemplate.d.ts +16 -47
- package/lib/templates/MapTemplate.d.ts +26 -2
- package/lib/templates/MapTemplate.js +16 -2
- package/lib/templates/MessageTemplate.d.ts +52 -11
- package/lib/templates/MessageTemplate.js +1 -1
- package/lib/utils/NitroOptionsPanel.d.ts +103 -0
- package/lib/utils/NitroOptionsPanel.js +63 -0
- package/lib/utils/NitroSection.d.ts +89 -2
- package/lib/utils/NitroSection.js +49 -2
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
- package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
- package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
- package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
- package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
- package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
- package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
- package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
- package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
- package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
- package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
- package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
- package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
- package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
- package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
- package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
- package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
- package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
- package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
- package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
- package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
- package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
- package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
- package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
- package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
- package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
- package/package.json +1 -1
- package/src/Constants.ts +9 -0
- package/src/index.ts +1 -0
- package/src/specs/MapTemplate.nitro.ts +3 -1
- package/src/templates/GridTemplate.ts +38 -3
- package/src/templates/InformationTemplate.ts +60 -29
- package/src/templates/ListTemplate.ts +27 -55
- package/src/templates/MapTemplate.ts +60 -2
- package/src/templates/MessageTemplate.ts +71 -24
- package/src/utils/NitroOptionsPanel.ts +218 -0
- 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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
| `
|
|
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` | `
|
|
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)
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt
CHANGED
|
@@ -147,8 +147,10 @@ class HybridMapTemplate : HybridMapTemplateSpec() {
|
|
|
147
147
|
|
|
148
148
|
override fun startNavigation(
|
|
149
149
|
templateId: String, trip: TripConfig
|
|
150
|
-
) {
|
|
151
|
-
|
|
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
|
}
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt
CHANGED
|
@@ -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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
+
}
|