@iternio/react-native-auto-play 0.5.8 → 0.5.10

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 (44) hide show
  1. package/README.md +8 -0
  2. package/android/build.gradle +14 -2
  3. package/android/gradle.properties +2 -0
  4. package/android/src/automotive/AndroidManifest-nonnav.xml +78 -0
  5. package/android/src/main/AndroidManifest-nonnav.xml +35 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoService.kt +6 -1
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAndroidWindowInformation.kt +49 -0
  8. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +1 -0
  9. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +116 -29
  10. package/lib/components/WindowInformationWrapper.d.ts +15 -0
  11. package/lib/components/WindowInformationWrapper.js +17 -0
  12. package/lib/hybrid/HybridAndroidWindowInformation.android.d.ts +2 -0
  13. package/lib/hybrid/HybridAndroidWindowInformation.android.js +2 -0
  14. package/lib/hybrid/HybridAndroidWindowInformation.d.ts +2 -0
  15. package/lib/hybrid/HybridAndroidWindowInformation.js +1 -0
  16. package/lib/index.d.ts +2 -1
  17. package/lib/index.js +2 -1
  18. package/lib/scenes/AutoPlayCluster.js +6 -1
  19. package/lib/specs/AndroidWindowInformation.nitro.d.ts +22 -0
  20. package/lib/specs/AndroidWindowInformation.nitro.js +1 -0
  21. package/lib/templates/MapTemplate.js +6 -1
  22. package/lib/types/RootComponent.d.ts +4 -0
  23. package/nitro.json +6 -0
  24. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +2 -0
  25. package/nitrogen/generated/android/ReactNativeAutoPlayOnLoad.cpp +18 -0
  26. package/nitrogen/generated/android/c++/JFunc_void_WindowInformation.hpp +77 -0
  27. package/nitrogen/generated/android/c++/JHybridAndroidWindowInformationSpec.cpp +68 -0
  28. package/nitrogen/generated/android/c++/JHybridAndroidWindowInformationSpec.hpp +63 -0
  29. package/nitrogen/generated/android/c++/JWindowInformation.hpp +65 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_WindowInformation.kt +80 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAndroidWindowInformationSpec.kt +59 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WindowInformation.kt +61 -0
  33. package/nitrogen/generated/shared/c++/HybridAndroidWindowInformationSpec.cpp +21 -0
  34. package/nitrogen/generated/shared/c++/HybridAndroidWindowInformationSpec.hpp +65 -0
  35. package/nitrogen/generated/shared/c++/WindowInformation.hpp +91 -0
  36. package/package.json +1 -1
  37. package/src/components/WindowInformationWrapper.tsx +30 -0
  38. package/src/hybrid/HybridAndroidWindowInformation.android.ts +5 -0
  39. package/src/hybrid/HybridAndroidWindowInformation.ts +3 -0
  40. package/src/index.ts +2 -1
  41. package/src/scenes/AutoPlayCluster.ts +6 -1
  42. package/src/specs/AndroidWindowInformation.nitro.ts +24 -0
  43. package/src/templates/MapTemplate.ts +6 -1
  44. package/src/types/RootComponent.ts +4 -0
package/README.md CHANGED
@@ -206,6 +206,14 @@ In case you have ProGuard enabled (`def enableProguardInReleaseBuilds = true` in
206
206
  ### Android Auto Customization
207
207
  You can customize certain behaviors of the library on Android Auto by setting properties in your app's `android/gradle.properties` file.
208
208
 
209
+ - **App Category**: Declare which Android Auto / Automotive app type the `CarAppService` advertises — the Android counterpart to choosing your CarPlay scene type in `Info.plist`.
210
+ ```properties
211
+ ReactNativeAutoPlay_androidAutoAppCategory=navigation
212
+ ```
213
+ Supported values are `navigation`, `poi`, `parking`, `charging`, `iot`, `messaging`, `calling`, and `weather` (see the [supported app categories](https://developer.android.com/training/cars)); each maps to `androidx.car.app.category.<UPPERCASE>`. An unrecognized value fails the build. The default `navigation` keeps the existing behavior. Any other value selects a lean manifest that declares the chosen category and omits the navigation-only permissions (`NAVIGATION_TEMPLATES`, `MAP_TEMPLATES`, `ACCESS_SURFACE`), the `FEATURE_CLUSTER` category, and the `NAVIGATE`/`geo` intent filters — a non-navigation, no-map template app needs none of these, and declaring them gets the app reviewed against the navigation-app bar on the Play Store.
214
+
215
+ On Android Automotive, the app-focus helpers on `HybridAndroidAutomotive` (`requestAppFocus`, `registerAppFocusListener`, `getAppFocusState`) request/observe **navigation** focus specifically; they are meant for navigation apps and should not be used with a non-navigation category.
216
+
209
217
  - **Telemetry Update Interval**: Control how often telemetry data is updated.
210
218
  ```properties
211
219
  ReactNativeAutoPlay_androidTelemetryUpdateInterval=4000
@@ -28,6 +28,11 @@ def getExtOrIntegerDefault(name) {
28
28
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeAutoPlay_" + name]).toInteger()
29
29
  }
30
30
 
31
+ // Non-navigation categories select the lean manifest (AndroidManifest-nonnav.xml).
32
+ def androidAutoAppCategory = getExtOrDefault("androidAutoAppCategory").toString().trim().toLowerCase(java.util.Locale.ROOT)
33
+ def validAndroidAutoCategories = ['navigation', 'poi', 'parking', 'charging', 'iot', 'messaging', 'calling', 'weather']
34
+ def isNavigationApp = androidAutoAppCategory == "navigation"
35
+
31
36
  android {
32
37
  namespace "com.margelo.nitro.swe.iternio.reactnativeautoplay"
33
38
 
@@ -35,6 +40,10 @@ android {
35
40
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
36
41
 
37
42
  defaultConfig {
43
+ if (!validAndroidAutoCategories.contains(androidAutoAppCategory)) {
44
+ throw new GradleException("Invalid ReactNativeAutoPlay_androidAutoAppCategory '${androidAutoAppCategory}'. Must be one of: ${validAndroidAutoCategories.join(', ')}")
45
+ }
46
+
38
47
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
39
48
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
40
49
 
@@ -55,6 +64,9 @@ android {
55
64
  }
56
65
  }
57
66
 
67
+ manifestPlaceholders.autoPlayAppCategory = "androidx.car.app.category." + androidAutoAppCategory.toUpperCase(java.util.Locale.ROOT)
68
+
69
+ buildConfigField "boolean", "IS_NAVIGATION_APP", isNavigationApp.toString()
58
70
  buildConfigField "float", "SCALE_FACTOR", getExtOrDefault("androidAutoScaleFactor")
59
71
  buildConfigField "long", "TELEMETRY_UPDATE_INTERVAL", getExtOrDefault("androidTelemetryUpdateInterval")
60
72
  buildConfigField "long", "CLUSTER_SPLASH_DELAY_MS", getExtOrDefault("clusterSplashDelayMs")
@@ -117,10 +129,10 @@ android {
117
129
 
118
130
  if (getExtOrDefault("isAutomotiveApp") == "true") {
119
131
  java.srcDirs += 'src/automotive/java'
120
- manifest.srcFile 'src/automotive/AndroidManifest.xml'
132
+ manifest.srcFile isNavigationApp ? 'src/automotive/AndroidManifest.xml' : 'src/automotive/AndroidManifest-nonnav.xml'
121
133
  } else {
122
134
  java.srcDirs += 'src/auto/java'
123
- manifest.srcFile 'src/main/AndroidManifest.xml'
135
+ manifest.srcFile isNavigationApp ? 'src/main/AndroidManifest.xml' : 'src/main/AndroidManifest-nonnav.xml'
124
136
  }
125
137
  }
126
138
  }
@@ -5,6 +5,8 @@ ReactNativeAutoPlay_compileSdkVersion=36
5
5
  ReactNativeAutoPlay_ndkVersion=27.1.12297006
6
6
  # specifies if the Android Auto (androidx.car.app:app-projected) or Automotive (androidx.car.app:app-automotive) library is used
7
7
  ReactNativeAutoPlay_isAutomotiveApp=false
8
+ # Android Auto/Automotive app category -> androidx.car.app.category.<UPPERCASE>. Non-navigation values use a lean manifest.
9
+ ReactNativeAutoPlay_androidAutoAppCategory=navigation
8
10
  # apply a scale factor on the root view that affects everything rendered by react-native, does not affect templates
9
11
  ReactNativeAutoPlay_androidAutoScaleFactor=1.5f
10
12
  ReactNativeAutoPlay_androidTelemetryUpdateInterval=4000
@@ -0,0 +1,78 @@
1
+ <!-- Non-navigation Android Automotive manifest; see AndroidManifest.xml for the navigation variant. -->
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+ <uses-feature
5
+ android:name="android.hardware.type.automotive"
6
+ android:required="true" />
7
+
8
+ <uses-feature
9
+ android:name="android.software.car.templates_host"
10
+ android:required="true" />
11
+
12
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
13
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
14
+
15
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
16
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
17
+
18
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
19
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
20
+
21
+
22
+ <!-- Android Automotive specific permissions -->
23
+ <uses-permission android:name="android.car.permission.CAR_ENERGY" />
24
+ <uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
25
+ <uses-permission android:name="android.car.permission.CAR_EXTERIOR_ENVIRONMENT" />
26
+ <uses-permission android:name="android.car.permission.CAR_IDENTIFICATION" />
27
+ <uses-permission android:name="android.car.permission.CAR_INFO" />
28
+ <uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />
29
+ <uses-permission android:name="android.car.permission.CAR_SPEED" />
30
+
31
+ <application>
32
+ <activity
33
+ android:name="androidx.car.app.activity.CarAppActivity"
34
+ android:exported="true"
35
+ android:launchMode="singleTask"
36
+ android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
37
+
38
+ <intent-filter>
39
+ <action android:name="android.intent.action.MAIN" />
40
+
41
+ <category android:name="android.intent.category.LAUNCHER" />
42
+ </intent-filter>
43
+
44
+ <meta-data
45
+ android:name="distractionOptimized"
46
+ android:value="true" />
47
+
48
+ </activity>
49
+
50
+ <service
51
+ android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.AndroidAutoService"
52
+ android:exported="true"
53
+ android:foregroundServiceType="location">
54
+ <intent-filter>
55
+ <action android:name="androidx.car.app.CarAppService" />
56
+
57
+ <category android:name="${autoPlayAppCategory}" />
58
+ </intent-filter>
59
+ </service>
60
+
61
+ <service android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.HeadlessTaskService" />
62
+
63
+ <activity android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.SignInWithGoogleActivity" />
64
+
65
+ <provider
66
+ android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.ActivityRenderStateProvider"
67
+ android:authorities="${applicationId}.ActivityRenderStateProvider"
68
+ android:exported="false" />
69
+
70
+ <meta-data
71
+ android:name="com.android.automotive"
72
+ android:resource="@xml/automotive_app_desc" />
73
+ <meta-data
74
+ android:name="androidx.car.app.minCarApiLevel"
75
+ android:value="1" />
76
+ </application>
77
+
78
+ </manifest>
@@ -0,0 +1,35 @@
1
+ <!-- Non-navigation Android Auto manifest; see AndroidManifest.xml for the navigation variant. -->
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:tools="http://schemas.android.com/tools">
4
+
5
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
6
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
7
+
8
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
9
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
10
+
11
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
12
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
13
+
14
+ <!-- Android Auto specific permissions -->
15
+ <uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
16
+ <uses-permission android:name="com.google.android.gms.permission.CAR_MILEAGE" />
17
+ <uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
18
+
19
+ <application>
20
+ <service android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.AndroidAutoService" android:exported="true" android:foregroundServiceType="location">
21
+ <intent-filter>
22
+ <action android:name="androidx.car.app.CarAppService" />
23
+
24
+ <category android:name="${autoPlayAppCategory}" />
25
+ </intent-filter>
26
+ </service>
27
+
28
+ <meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc" tools:ignore="MetadataTagInsideApplicationTag" />
29
+ <meta-data android:name="androidx.car.app.minCarApiLevel" android:value="1" />
30
+
31
+ <service android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.HeadlessTaskService" />
32
+
33
+ <provider android:name="com.margelo.nitro.swe.iternio.reactnativeautoplay.ActivityRenderStateProvider" android:authorities="${applicationId}.ActivityRenderStateProvider" android:exported="false" />
34
+ </application>
35
+ </manifest>
@@ -136,9 +136,14 @@ class AndroidAutoService : CarAppService() {
136
136
  private fun createNotification(
137
137
  title: String?, text: String?, largeIcon: Bitmap?
138
138
  ): Notification {
139
+ val notificationCategory = if (BuildConfig.IS_NAVIGATION_APP) {
140
+ NotificationCompat.CATEGORY_NAVIGATION
141
+ } else {
142
+ NotificationCompat.CATEGORY_SERVICE
143
+ }
139
144
  return NotificationCompat.Builder(this, CHANNEL_ID)
140
145
  .setSmallIcon(R.drawable.ic_notification).setOngoing(true)
141
- .setCategory(NotificationCompat.CATEGORY_NAVIGATION).setOnlyAlertOnce(true)
146
+ .setCategory(notificationCategory).setOnlyAlertOnce(true)
142
147
  .setWhen(System.currentTimeMillis()).setPriority(NotificationManager.IMPORTANCE_LOW)
143
148
  .extend(
144
149
  CarAppExtender.Builder().setImportance(NotificationManagerCompat.IMPORTANCE_LOW)
@@ -0,0 +1,49 @@
1
+ package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
+
3
+ import java.util.concurrent.ConcurrentHashMap
4
+ import java.util.concurrent.CopyOnWriteArrayList
5
+
6
+ class HybridAndroidWindowInformation : HybridAndroidWindowInformationSpec() {
7
+ init {
8
+ windowInformationListeners.clear()
9
+ }
10
+
11
+ override fun addWindowInformationListener(
12
+ moduleName: String, callback: (WindowInformation) -> Unit
13
+ ): () -> Unit {
14
+ val callbacks = windowInformationListeners.getOrPut(moduleName) {
15
+ CopyOnWriteArrayList()
16
+ }
17
+ callbacks.add(callback)
18
+
19
+ VirtualRenderer.getWindowInformation(moduleName)?.let {
20
+ callback(it)
21
+ }
22
+
23
+ return {
24
+ windowInformationListeners[moduleName]?.let {
25
+ it.remove(callback)
26
+ if (it.isEmpty()) {
27
+ windowInformationListeners.remove(moduleName)
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ companion object {
34
+ const val TAG = "HybridAndroidWindowInformation"
35
+
36
+ private val windowInformationListeners =
37
+ ConcurrentHashMap<String, CopyOnWriteArrayList<(WindowInformation) -> Unit>>()
38
+
39
+ fun removeListeners(templateId: String) {
40
+ windowInformationListeners.remove(templateId)
41
+ }
42
+
43
+ fun emitWindowInformation(moduleName: String, window: WindowInformation) {
44
+ windowInformationListeners[moduleName]?.forEach {
45
+ it(window)
46
+ }
47
+ }
48
+ }
49
+ }
@@ -264,6 +264,7 @@ class HybridAutoPlay : HybridAutoPlaySpec() {
264
264
  fun removeListeners(templateId: String) {
265
265
  renderStateListeners.remove(templateId)
266
266
  safeAreaInsetsListeners.remove(templateId)
267
+ HybridAndroidWindowInformation.removeListeners(templateId)
267
268
  }
268
269
 
269
270
  fun emit(event: EventName) {
@@ -42,20 +42,24 @@ class VirtualRenderer(
42
42
  ) {
43
43
  private var virtualDisplay: VirtualDisplay? = null
44
44
 
45
- private lateinit var reactSurfaceImpl: ReactSurfaceImpl
45
+ private var reactSurfaceImpl: ReactSurfaceImpl? = null
46
46
  private var reactSurfaceView: ReactSurfaceView? = null
47
47
  private var reactSurfaceId: Int? = null
48
48
 
49
49
  private var height: Int = 0
50
50
  private var width: Int = 0
51
+ private var dpi: Int = 0
51
52
 
52
53
  private var splashWillDisappear = false
53
54
 
54
55
  /**
55
- * scale is the actual scale factor required to calculate proper insets and is passed in initialProperties to js side
56
+ * scale is the actual scale factor required to calculate proper insets and is passed in initialProperties to js side.
57
+ * The density is derived from the surface container's dpi so it always matches the VirtualDisplay,
58
+ * and is recomputed on every onSurfaceAvailable since the host may recreate the surface with new metrics.
56
59
  */
57
- private val virtualScreenDensity = context.resources.displayMetrics.density
58
- val scale = BuildConfig.SCALE_FACTOR * virtualScreenDensity
60
+ private var virtualScreenDensity = context.resources.displayMetrics.density
61
+ var scale = BuildConfig.SCALE_FACTOR * virtualScreenDensity
62
+ private set
59
63
 
60
64
  private fun isSurfaceReady(surfaceContainer: SurfaceContainer): Boolean {
61
65
  return surfaceContainer.surface != null && surfaceContainer.dpi != 0 && surfaceContainer.height != 0 && surfaceContainer.width != 0
@@ -68,7 +72,7 @@ class VirtualRenderer(
68
72
  val areaDebouncer = Debouncer(200)
69
73
 
70
74
  // 12dp seems to be the default margin on AA for the ETA widget and the maneuver so use it as fallback
71
- val defaultMargin = (12.0 * context.resources.displayMetrics.density).toInt()
75
+ val defaultMargin get() = (12.0 * virtualScreenDensity).toInt()
72
76
  var minMargin = Int.MAX_VALUE
73
77
  var stableArea = Rect(0, 0, 0, 0)
74
78
  var visibleArea = Rect(0, 0, 0, 0)
@@ -78,25 +82,16 @@ class VirtualRenderer(
78
82
  return
79
83
  }
80
84
 
81
- val manager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
82
- virtualDisplay?.release()
83
- virtualDisplay = manager.createVirtualDisplay(
84
- moduleName,
85
- surfaceContainer.width,
86
- surfaceContainer.height,
87
- surfaceContainer.dpi,
88
- surfaceContainer.surface,
89
- DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION,
90
- )
91
-
92
- val display = virtualDisplay?.display ?: return
93
- height = surfaceContainer.height
94
- width = surfaceContainer.width
95
-
96
- initRenderer(display)
85
+ applySurface(surfaceContainer)
97
86
  }
98
87
 
99
88
  override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
89
+ // the host re-emits visible/stable areas for the next surface, so drop state derived
90
+ // from the old surface instead of combining it with the new dimensions
91
+ minMargin = Int.MAX_VALUE
92
+ stableArea = Rect(0, 0, 0, 0)
93
+ visibleArea = Rect(0, 0, 0, 0)
94
+
100
95
  virtualDisplay?.release()
101
96
  virtualDisplay = null
102
97
  }
@@ -230,6 +225,46 @@ class VirtualRenderer(
230
225
  })
231
226
  }
232
227
 
228
+ @MainThread
229
+ private fun applySurface(surfaceContainer: SurfaceContainer) {
230
+ val surfaceWidth = surfaceContainer.width
231
+ val surfaceHeight = surfaceContainer.height
232
+
233
+ // The host signals surface size changes only via onSurfaceDestroyed -> onSurfaceAvailable
234
+ // with new dimensions, so all size-derived state (layout params, scale, Fabric measure
235
+ // specs) must be recomputed when the metrics differ from the current ones. The react
236
+ // surface itself is kept alive so JS state survives the resize; the JS side is informed
237
+ // via the windowInformation listener since the window initial prop stays stale.
238
+ val isResize = reactSurfaceView != null && (width != surfaceWidth
239
+ || height != surfaceHeight || dpi != surfaceContainer.dpi)
240
+
241
+ val manager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
242
+ virtualDisplay?.release()
243
+ virtualDisplay = manager.createVirtualDisplay(
244
+ moduleName,
245
+ surfaceWidth,
246
+ surfaceHeight,
247
+ surfaceContainer.dpi,
248
+ surfaceContainer.surface,
249
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION,
250
+ )
251
+
252
+ val display = virtualDisplay?.display ?: return
253
+ height = surfaceHeight
254
+ width = surfaceWidth
255
+ dpi = surfaceContainer.dpi
256
+ virtualScreenDensity = surfaceContainer.dpi / 160f
257
+ scale = BuildConfig.SCALE_FACTOR * virtualScreenDensity
258
+
259
+ initRenderer(display)
260
+
261
+ if (isResize) {
262
+ getWindowInformation(moduleName)?.let {
263
+ HybridAndroidWindowInformation.emitWindowInformation(moduleName, it)
264
+ }
265
+ }
266
+ }
267
+
233
268
  private fun getMapTemplateConfig(): MapTemplateConfig? {
234
269
  val screenManager = AndroidAutoScreen.getScreen(moduleName)?.screenManager ?: return null
235
270
  val marker = screenManager.top.marker ?: return null
@@ -290,19 +325,48 @@ class VirtualRenderer(
290
325
  val appTheme = context.applicationContext.applicationInfo.theme
291
326
  val themedContext = ContextThemeWrapper(context.applicationContext, appTheme)
292
327
 
293
- if (!this@VirtualRenderer::reactSurfaceImpl.isInitialized) {
294
- reactSurfaceImpl = ReactSurfaceImpl(themedContext, moduleName, initialProperties)
328
+ val surfaceImpl = reactSurfaceImpl ?: ReactSurfaceImpl(
329
+ themedContext, moduleName, initialProperties
330
+ ).also {
331
+ reactSurfaceImpl = it
295
332
  }
296
333
 
297
334
  var splashScreenView: View? = null
298
335
 
299
336
  reactSurfaceView?.let {
300
337
  (it.parent as ViewGroup).removeView(it)
338
+
339
+ // the surface may have been recreated with new dimensions, so reapply all
340
+ // size-derived values and push the new layout specs into the running Fabric surface
341
+ it.layoutParams = FrameLayout.LayoutParams(
342
+ (width / reactNativeScale).toInt(), (height / reactNativeScale).toInt()
343
+ )
344
+ it.scaleX = reactNativeScale
345
+ it.scaleY = reactNativeScale
346
+
347
+ reactSurfaceId?.let { surfaceId ->
348
+ fabricUiManager.updateRootLayoutSpecs(
349
+ surfaceId,
350
+ View.MeasureSpec.makeMeasureSpec(
351
+ (width / reactNativeScale).toInt(), View.MeasureSpec.EXACTLY
352
+ ),
353
+ View.MeasureSpec.makeMeasureSpec(
354
+ (height / reactNativeScale).toInt(), View.MeasureSpec.EXACTLY
355
+ ),
356
+ 0,
357
+ 0
358
+ )
359
+ }
301
360
  } ?: run {
361
+ // skip the splash when the surface is rebuilt after a resize, content is already loaded
302
362
  splashScreenView =
303
- if (isCluster) getClusterSplashScreen(themedContext, height, width) else null
363
+ if (isCluster && !splashWillDisappear) {
364
+ getClusterSplashScreen(themedContext, height, width)
365
+ } else {
366
+ null
367
+ }
304
368
 
305
- val surfaceView = ReactSurfaceView(themedContext, reactSurfaceImpl).apply {
369
+ val surfaceView = ReactSurfaceView(themedContext, surfaceImpl).apply {
306
370
  layoutParams = FrameLayout.LayoutParams(
307
371
  (width / reactNativeScale).toInt(), (height / reactNativeScale).toInt()
308
372
  )
@@ -402,10 +466,7 @@ class VirtualRenderer(
402
466
  }
403
467
 
404
468
  @MainThread
405
- private fun stop() {
406
- virtualDisplay?.release()
407
- virtualDisplay = null
408
-
469
+ private fun stopReactSurface() {
409
470
  val uiManager = NitroModules.applicationContext?.let {
410
471
  UIManagerHelper.getUIManager(
411
472
  it, UIManagerType.FABRIC
@@ -420,6 +481,19 @@ class VirtualRenderer(
420
481
  // Fabric already invalidated
421
482
  } finally {
422
483
  reactSurfaceId = null
484
+ reactSurfaceView = null
485
+ reactSurfaceImpl = null
486
+ }
487
+ }
488
+
489
+ @MainThread
490
+ private fun stop() {
491
+ virtualDisplay?.release()
492
+ virtualDisplay = null
493
+
494
+ try {
495
+ stopReactSurface()
496
+ } finally {
423
497
  virtualRenderer.remove(moduleName)
424
498
  }
425
499
  }
@@ -433,6 +507,19 @@ class VirtualRenderer(
433
507
  return virtualRenderer.contains(moduleId)
434
508
  }
435
509
 
510
+ fun getWindowInformation(moduleId: String): WindowInformation? {
511
+ val renderer = virtualRenderer[moduleId] ?: return null
512
+ if (renderer.width == 0 || renderer.height == 0) {
513
+ return null
514
+ }
515
+
516
+ return WindowInformation(
517
+ width = (renderer.width / renderer.scale).toInt().toDouble(),
518
+ height = (renderer.height / renderer.scale).toInt().toDouble(),
519
+ scale = renderer.scale.toDouble()
520
+ )
521
+ }
522
+
436
523
  fun removeRenderer(moduleId: String) {
437
524
  virtualRenderer[moduleId]?.stop()
438
525
  virtualRenderer.remove(moduleId)
@@ -0,0 +1,15 @@
1
+ import type React from 'react';
2
+ import type { RootComponentInitialProps } from '../types/RootComponent';
3
+ /**
4
+ * Renders a user-provided root component and keeps its `window` prop up to date when the host
5
+ * recreates the render surface with new dimensions (some head units run Android Auto windowed
6
+ * and resize it at runtime), so consumers can simply read `props.window` without registering a
7
+ * listener themselves.
8
+ * On iOS this only passes the initial props through — CarPlay windows keep their size for the
9
+ * lifetime of a scene, so there is nothing to subscribe to.
10
+ */
11
+ export declare function WindowInformationWrapper({ moduleName, component, componentProps, }: {
12
+ moduleName: string;
13
+ component: React.ComponentType<RootComponentInitialProps>;
14
+ componentProps: RootComponentInitialProps;
15
+ }): React.ReactElement<RootComponentInitialProps, string | React.JSXElementConstructor<any>>;
@@ -0,0 +1,17 @@
1
+ import { createElement, useEffect, useState } from 'react';
2
+ import { HybridAndroidWindowInformation } from '../hybrid/HybridAndroidWindowInformation';
3
+ /**
4
+ * Renders a user-provided root component and keeps its `window` prop up to date when the host
5
+ * recreates the render surface with new dimensions (some head units run Android Auto windowed
6
+ * and resize it at runtime), so consumers can simply read `props.window` without registering a
7
+ * listener themselves.
8
+ * On iOS this only passes the initial props through — CarPlay windows keep their size for the
9
+ * lifetime of a scene, so there is nothing to subscribe to.
10
+ */
11
+ export function WindowInformationWrapper({ moduleName, component, componentProps, }) {
12
+ const [window, setWindow] = useState(componentProps.window);
13
+ useEffect(() => {
14
+ return HybridAndroidWindowInformation?.addWindowInformationListener(moduleName, setWindow);
15
+ }, [moduleName]);
16
+ return createElement(component, { ...componentProps, window });
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { AndroidWindowInformation } from '../specs/AndroidWindowInformation.nitro';
2
+ export declare const HybridAndroidWindowInformation: AndroidWindowInformation | null;
@@ -0,0 +1,2 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+ export const HybridAndroidWindowInformation = NitroModules.createHybridObject('AndroidWindowInformation');
@@ -0,0 +1,2 @@
1
+ import type { AndroidWindowInformation } from '../specs/AndroidWindowInformation.nitro';
2
+ export declare const HybridAndroidWindowInformation: AndroidWindowInformation | null;
@@ -0,0 +1 @@
1
+ export const HybridAndroidWindowInformation = null;
package/lib/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { HybridAndroidAutoTelemetry } from './hybrid/HybridAndroidAutoTelemetry';
2
+ import { HybridAndroidWindowInformation } from './hybrid/HybridAndroidWindowInformation';
2
3
  import { HybridAutoPlay } from './hybrid/HybridAutoPlay';
3
4
  import { HybridVoice } from './hybrid/HybridVoice';
4
5
  import type { AndroidAutomotive } from './specs/AndroidAutomotive.nitro';
5
- export { HybridAndroidAutoTelemetry, HybridAutoPlay, HybridVoice };
6
+ export { HybridAndroidAutoTelemetry, HybridAndroidWindowInformation, HybridAutoPlay, HybridVoice };
6
7
  export declare const HybridAndroidAutomotive: AndroidAutomotive | null;
7
8
  /**
8
9
  * These are the static module names for the app running on the mobile device, head unit screen and the CarPlay dashboard.
package/lib/index.js CHANGED
@@ -2,10 +2,11 @@ import { Platform } from 'react-native';
2
2
  import { NitroModules } from 'react-native-nitro-modules';
3
3
  import AutoPlayHeadlessJsTask from './AutoPlayHeadlessJsTask';
4
4
  import { HybridAndroidAutoTelemetry } from './hybrid/HybridAndroidAutoTelemetry';
5
+ import { HybridAndroidWindowInformation } from './hybrid/HybridAndroidWindowInformation';
5
6
  import { HybridAutoPlay } from './hybrid/HybridAutoPlay';
6
7
  import { HybridVoice } from './hybrid/HybridVoice';
7
8
  AutoPlayHeadlessJsTask.registerHeadlessTask(HybridAutoPlay);
8
- export { HybridAndroidAutoTelemetry, HybridAutoPlay, HybridVoice };
9
+ export { HybridAndroidAutoTelemetry, HybridAndroidWindowInformation, HybridAutoPlay, HybridVoice };
9
10
  export const HybridAndroidAutomotive = Platform.OS === 'android'
10
11
  ? NitroModules.createHybridObject('AndroidAutomotive')
11
12
  : null;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { AppRegistry, Platform } from 'react-native';
3
3
  import { NitroModules } from 'react-native-nitro-modules';
4
4
  import { SafeAreaInsetsProvider } from '../components/SafeAreaInsetsContext';
5
+ import { WindowInformationWrapper } from '../components/WindowInformationWrapper';
5
6
  import { NitroImageUtil } from '../utils/NitroImage';
6
7
  const HybridCluster = NitroModules.createHybridObject('Cluster');
7
8
  class Cluster {
@@ -49,7 +50,11 @@ class Cluster {
49
50
  AppRegistry.registerComponent(clusterId, () => (props) => React.createElement(SafeAreaInsetsProvider, {
50
51
  moduleName: clusterId,
51
52
  // biome-ignore lint/correctness/noChildrenProp: there is no other way in a ts file
52
- children: React.createElement(component, props),
53
+ children: React.createElement(WindowInformationWrapper, {
54
+ moduleName: clusterId,
55
+ component,
56
+ componentProps: props,
57
+ }),
53
58
  }));
54
59
  this.clusters[clusterId] = true;
55
60
  await HybridCluster.initRootView(clusterId);
@@ -0,0 +1,22 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules';
2
+ import type { CleanupCallback } from '../types/Event';
3
+ import type { WindowInformation } from '../types/RootComponent';
4
+ /**
5
+ * Android-only access to render-surface window dimensions. On iOS this module does not exist —
6
+ * use the platform-split `HybridAndroidWindowInformation` wrapper which resolves to `null` there.
7
+ */
8
+ export interface AndroidWindowInformation extends HybridObject<{
9
+ android: 'kotlin';
10
+ }> {
11
+ /**
12
+ * callback for window dimension changes, fired when the host recreates the render surface
13
+ * with a different size (some head units run Android Auto windowed and resize it at runtime).
14
+ * Root components rendered via a template's `component` prop do NOT need this — their
15
+ * `window` prop is kept up to date automatically. Use this listener only for
16
+ * size-dependent logic living outside a root component.
17
+ * Fires with the last known window information immediately if the surface is already initialized.
18
+ * @param moduleName one of @AutoPlayModules or a cluster scene uuid
19
+ * @namespace Android
20
+ */
21
+ addWindowInformationListener(moduleName: string, callback: (window: WindowInformation) => void): CleanupCallback;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,7 @@ import { AppRegistry, Platform } from 'react-native';
3
3
  import { NitroModules } from 'react-native-nitro-modules';
4
4
  import { MapTemplateProvider } from '../components/MapTemplateContext';
5
5
  import { SafeAreaInsetsProvider } from '../components/SafeAreaInsetsContext';
6
+ import { WindowInformationWrapper } from '../components/WindowInformationWrapper';
6
7
  import { HybridAutoPlay } from '../hybrid/HybridAutoPlay';
7
8
  import { NitroActionUtil } from '../utils/NitroAction';
8
9
  import { NitroAlertUtil } from '../utils/NitroAlert';
@@ -23,7 +24,11 @@ export class MapTemplate extends Template {
23
24
  children: React.createElement(SafeAreaInsetsProvider, {
24
25
  moduleName: this.id,
25
26
  // biome-ignore lint/correctness/noChildrenProp: there is no other way in a ts file
26
- children: React.createElement(component, props),
27
+ children: React.createElement(WindowInformationWrapper, {
28
+ moduleName: this.id,
29
+ component,
30
+ componentProps: props,
31
+ }),
27
32
  }),
28
33
  }));
29
34
  const nitroConfig = {
@@ -19,6 +19,10 @@ export type RootComponentInitialProps = {
19
19
  * this is the initial color scheme, add onAppearanceDidChange on MapTemplate to get updates
20
20
  */
21
21
  colorScheme: ColorScheme;
22
+ /**
23
+ * the dimensions of the render surface, kept up to date when the host resizes the surface
24
+ * (e.g. head units running Android Auto windowed)
25
+ */
22
26
  window: WindowInformation;
23
27
  };
24
28
  export type AutoPlayClusterInitialProps = RootComponentInitialProps & {