@hot-updater/react-native 1.0.8 → 1.1.1

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 (145) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,493 @@
1
+ package com.hotupdater
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.content.Context
6
+ import android.content.ContextWrapper
7
+ import android.os.Bundle
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import com.facebook.react.ReactRootView
11
+ import com.facebook.react.bridge.ReactContext
12
+ import com.facebook.react.bridge.UIManager
13
+ import com.facebook.react.uimanager.UIManagerHelper
14
+ import com.facebook.react.uimanager.common.UIManagerType
15
+ import java.lang.ref.WeakReference
16
+ import java.util.Collections
17
+ import java.util.WeakHashMap
18
+
19
+ /**
20
+ * Which instance a React Native view, runtime, or JavaScript error belongs to:
21
+ * the instance whose bundle the runtime runs, by the URL React Native loaded
22
+ * it from ([HotUpdaterInstanceRegistry.resolveName]). With only main, all of
23
+ * it is main's.
24
+ */
25
+ internal class HotUpdaterRecoveryAttribution(
26
+ private val resolveName: (Context, String) -> String = HotUpdaterInstanceRegistry.shared::resolveName,
27
+ private val isStarted: (Activity) -> Boolean = { isActivityStarted(it) },
28
+ runtimeRoots: HotUpdaterRuntimeRoots? = null,
29
+ decorRoots: HotUpdaterDecorRoots? = null,
30
+ /**
31
+ * Whether a runtime that loaded the URL runs the instance's current
32
+ * launch. A lambda, not a reference: the manager's companion reads
33
+ * [shared], so creating [shared] must not initialize the manager.
34
+ */
35
+ private val runsCurrentLaunch: (instanceName: String, sourceURL: String?) -> Boolean = { name, url ->
36
+ HotUpdaterRecoveryManager.runsCurrentLaunch(name, url)
37
+ },
38
+ /**
39
+ * Whether a runtime registered for the instance's current launch, by the
40
+ * launch it registered for: a replacement launch of the same bundle has
41
+ * the same URL. Null for a runtime that registered for no launch.
42
+ */
43
+ private val runtimeRunsCurrentLaunch: (instanceName: String, runtime: Any?) -> Boolean? = { name, runtime ->
44
+ HotUpdaterRecoveryManager.runtimeRunsCurrentLaunch(name, runtime)
45
+ },
46
+ ) {
47
+ /**
48
+ * Root views of the registered runtimes, found through their UI
49
+ * managers in whichever window shows them: a dialog or a presentation
50
+ * has its own window, which no activity's decor view contains.
51
+ */
52
+ private val runtimeRoots: HotUpdaterRuntimeRoots = runtimeRoots ?: UIManagerRuntimeRoots()
53
+
54
+ /** Root views in activities' decor views, for a runtime not registered yet. */
55
+ private val decorRoots: HotUpdaterDecorRoots =
56
+ decorRoots ?: HotUpdaterDecorRoots { activity ->
57
+ val decorView = activity.window?.peekDecorView()
58
+ if (decorView == null) {
59
+ emptyList()
60
+ } else {
61
+ rootViewsIn(decorView).map { rootView ->
62
+ val runtime = runCatching { rootView.currentReactContext }.getOrNull()
63
+ // A call, not `rootViewTag`: Kotlin 2.2 has no property for a getter the Kotlin `ReactRoot` declares.
64
+ HotUpdaterDecorRoots.Root(rootView.getRootViewTag(), runCatching { runtime?.sourceURL }.getOrNull(), runtime)
65
+ }
66
+ }
67
+ }
68
+
69
+ companion object {
70
+ val shared = HotUpdaterRecoveryAttribution()
71
+
72
+ // Instance names have no "@", so this names no launch.
73
+ private const val EARLIER_LAUNCH_SUFFIX = "@earlier-launch"
74
+
75
+ // A JavaScript stack frame's source: an absolute path or a URL, such
76
+ // as Hermes' "at f (address at /data/.../index.android.bundle:1:2345)",
77
+ // "f@/data/.../index.android.bundle:1:2345", or "assets://widget.android.bundle:1:9".
78
+ private val FRAME_SOURCE = Regex("""((?:[A-Za-z][A-Za-z0-9+.-]*://|/)[^\s()]*?)(?::\d+){1,2}(?=[)\s,]|$)""")
79
+
80
+ /** The bundle locations the frames of [stack] run from. */
81
+ fun frameSources(stack: String): List<String> =
82
+ FRAME_SOURCE
83
+ .findAll(stack)
84
+ .map { it.groupValues[1] }
85
+ .distinct()
86
+ .toList()
87
+
88
+ /**
89
+ * The named instance whose bundle every located frame of [stacks]
90
+ * runs from, or null when no frame is located or they name main, an
91
+ * unknown bundle, or more than one instance. A runtime runs one
92
+ * bundle, so its error's frames name its instance even before its
93
+ * HotUpdater module exists.
94
+ */
95
+ fun namedInstanceOfStack(
96
+ stacks: List<String>,
97
+ resolve: (String) -> String,
98
+ ): String? = instanceOfStack(stacks, resolve)?.takeIf { it != HotUpdaterInstanceEnvironment.MAIN_NAME }
99
+
100
+ /**
101
+ * The instance, main included, whose bundle every located frame of
102
+ * [stacks] runs from; null when no frame is located or they name more
103
+ * than one instance. Main's frames are main's own bundle or any the
104
+ * named instances don't know, as [HotUpdaterInstanceRegistry.resolveName] has it.
105
+ */
106
+ fun instanceOfStack(
107
+ stacks: List<String>,
108
+ resolve: (String) -> String,
109
+ ): String? {
110
+ val sources = stacks.flatMap(::frameSources).distinct()
111
+ if (sources.isEmpty()) return null
112
+ return sources.map(resolve).toSet().singleOrNull()
113
+ }
114
+
115
+ /**
116
+ * Whether a view of [viewInstance] counts for [instanceName]. Main
117
+ * takes every view no named instance claims, so a view whose runtime
118
+ * isn't known yet counts for main, as every view always has.
119
+ */
120
+ fun owns(
121
+ instanceName: String,
122
+ viewInstance: String?,
123
+ ): Boolean =
124
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) {
125
+ viewInstance == null || viewInstance == HotUpdaterInstanceEnvironment.MAIN_NAME
126
+ } else {
127
+ viewInstance == instanceName
128
+ }
129
+ }
130
+
131
+ @Volatile
132
+ private var context: Context? = null
133
+ private var trackedApplication: Application? = null
134
+
135
+ // Activities that may hold a React Native view whose first content appears.
136
+ private val activities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
137
+ private val startedActivities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
138
+ private val runtimes = mutableListOf<WeakReference<ReactContext>>()
139
+
140
+ private val activityCallbacks =
141
+ object : Application.ActivityLifecycleCallbacks {
142
+ override fun onActivityCreated(
143
+ activity: Activity,
144
+ savedInstanceState: Bundle?,
145
+ ) {
146
+ synchronized(activities) { activities += activity }
147
+ }
148
+
149
+ override fun onActivityStarted(activity: Activity) {
150
+ synchronized(activities) {
151
+ activities += activity
152
+ startedActivities += activity
153
+ }
154
+ }
155
+
156
+ override fun onActivityDestroyed(activity: Activity) {
157
+ synchronized(activities) {
158
+ activities -= activity
159
+ startedActivities -= activity
160
+ }
161
+ }
162
+
163
+ override fun onActivityResumed(activity: Activity) {}
164
+
165
+ override fun onActivityPaused(activity: Activity) {}
166
+
167
+ override fun onActivityStopped(activity: Activity) {
168
+ synchronized(activities) { startedActivities -= activity }
169
+ }
170
+
171
+ override fun onActivitySaveInstanceState(
172
+ activity: Activity,
173
+ outState: Bundle,
174
+ ) {}
175
+ }
176
+
177
+ /** Starts following the app's activities, once per process. */
178
+ fun track(context: Context) {
179
+ this.context = context.applicationContext
180
+ val application = context.applicationContext as? Application ?: return
181
+ synchronized(this) {
182
+ if (trackedApplication != null) return
183
+ trackedApplication = application
184
+ }
185
+ application.registerActivityLifecycleCallbacks(activityCallbacks)
186
+ }
187
+
188
+ /**
189
+ * Remembers [activity], which the app handed over as a context: it may
190
+ * start, or have started, before [track] registered for lifecycle
191
+ * callbacks. It counts as started only while it is.
192
+ */
193
+ fun noteActivity(activity: Activity) {
194
+ synchronized(activities) { activities += activity }
195
+ }
196
+
197
+ /**
198
+ * Remembers a runtime whose HotUpdater module was created. Its current
199
+ * activity is searched for its views even when that activity started
200
+ * before [track], as when a brownfield app adds React Native to a screen
201
+ * already shown.
202
+ */
203
+ fun registerRuntime(reactContext: ReactContext) {
204
+ if (context == null) context = reactContext.applicationContext
205
+ synchronized(runtimes) {
206
+ runtimes.removeAll { it.get() == null }
207
+ if (runtimes.none { it.get() === reactContext }) runtimes += WeakReference(reactContext)
208
+ }
209
+ }
210
+
211
+ /** Forgets [reactContext], whose runtime was torn down. */
212
+ fun unregisterRuntime(reactContext: ReactContext) {
213
+ synchronized(runtimes) { runtimes.removeAll { it.get().let { runtime -> runtime == null || runtime === reactContext } } }
214
+ }
215
+
216
+ /**
217
+ * The activities started now, as far as this process has seen: the ones
218
+ * seen starting since [track], and any other known activity, such as a
219
+ * registered runtime's current one, that reports it is started now. A
220
+ * runtime keeps its activity after it stops. A gate created after they
221
+ * started reads them here.
222
+ */
223
+ fun startedActivities(): List<Activity> {
224
+ val (seenStarted, known) = synchronized(activities) { startedActivities.toList() to activities.toList() }
225
+ val runtimeActivities = synchronized(runtimes) { runtimes.mapNotNull { it.get()?.currentActivity } }
226
+ return (seenStarted + (known + runtimeActivities).filter(isStarted)).distinct()
227
+ }
228
+
229
+ /** [namedInstanceOfStack] with this process's instances. */
230
+ fun namedInstanceOfStack(stacks: List<String>): String? {
231
+ val context = context ?: return null
232
+ return namedInstanceOfStack(stacks) { resolveName(context, it) }
233
+ }
234
+
235
+ /** [instanceOfStack] with this process's instances. */
236
+ fun instanceOfStack(stacks: List<String>): String? {
237
+ val context = context ?: return null
238
+ return instanceOfStack(stacks) { resolveName(context, it) }
239
+ }
240
+
241
+ /** The instance whose bundle runs from [sourceURL], or null when it isn't known. */
242
+ fun instanceOf(sourceURL: String?): String? {
243
+ val url = sourceURL?.takeIf { it.isNotEmpty() } ?: return null
244
+ val context = context ?: return HotUpdaterInstanceEnvironment.MAIN_NAME
245
+ return resolveName(context, url)
246
+ }
247
+
248
+ /**
249
+ * The instance whose current launch a view of a runtime that loaded
250
+ * [sourceURL] belongs to. A named host may still show a view of its
251
+ * previous runtime while its next launch starts: that view belongs to an
252
+ * earlier launch, which neither arms nor verifies the current one. Main
253
+ * takes any view, as it always has. Every view that arms or verifies a
254
+ * launch is attributed here, null before the view joins a runtime.
255
+ */
256
+ fun launchInstanceOf(
257
+ sourceURL: String?,
258
+ runtime: Any? = null,
259
+ ): String? {
260
+ val instanceName = instanceOf(sourceURL) ?: return null
261
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) return instanceName
262
+ // The launch [runtime] registered for, when it did; else its bundle's URL.
263
+ val current = runtimeRunsCurrentLaunch(instanceName, runtime) ?: runsCurrentLaunch(instanceName, sourceURL)
264
+ return if (current) instanceName else "$instanceName$EARLIER_LAUNCH_SUFFIX"
265
+ }
266
+
267
+ /** Whether [activity] shows a React Native view that counts for [instanceName]. */
268
+ fun showsViewOf(
269
+ activity: Activity,
270
+ instanceName: String,
271
+ ): Boolean {
272
+ if (decorRoots.rootsIn(activity).any { owns(instanceName, launchInstanceOf(it.sourceURL, it.runtime)) }) return true
273
+ // Main takes the views no named instance claims, which a decor view shows.
274
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) return false
275
+ return runtimeRoots.runtimesShownIn(activity).any { launchInstanceOf(it.sourceURL, it.runtime) == instanceName }
276
+ }
277
+
278
+ /**
279
+ * The instance of the root view whose first content appeared, which React
280
+ * Native names by its root tag; null when no known activity shows it or
281
+ * its runtime isn't known yet.
282
+ */
283
+ fun instanceOfRootTag(rootTag: Int): String? = rootOf(rootTag)?.instanceName
284
+
285
+ /** A root view's instance, as [instanceOfRootTag] has it, and the runtime that holds it. */
286
+ data class RootOwner(
287
+ val instanceName: String,
288
+ val runtime: Any?,
289
+ )
290
+
291
+ /** [instanceOfRootTag], with the runtime holding the root view, by which content is bound to that runtime's launch. */
292
+ fun rootOf(rootTag: Int): RootOwner? {
293
+ runtimeRoots.runtimeOfRootTag(rootTag)?.let { root ->
294
+ return launchInstanceOf(root.sourceURL, root.runtime)?.let { RootOwner(it, root.runtime) }
295
+ }
296
+ val candidates =
297
+ synchronized(activities) { activities.toList() } +
298
+ synchronized(runtimes) { runtimes.mapNotNull { it.get()?.currentActivity } }
299
+ candidates.distinct().forEach { activity ->
300
+ // A view whose runtime isn't known yet tells nothing either.
301
+ decorRoots.rootsIn(activity).firstOrNull { it.rootTag == rootTag }?.let { root ->
302
+ return launchInstanceOf(root.sourceURL, root.runtime)?.let { RootOwner(it, root.runtime) }
303
+ }
304
+ }
305
+ return null
306
+ }
307
+
308
+ private fun registeredRuntimes(): List<ReactContext> = synchronized(runtimes) { runtimes.mapNotNull { it.get() } }
309
+
310
+ /** Root views through the registered runtimes' UI managers. Main thread, as UI managers require. */
311
+ private inner class UIManagerRuntimeRoots : HotUpdaterRuntimeRoots {
312
+ private val liveRootTags = HotUpdaterLiveRootTags()
313
+
314
+ // The runtimes the root tags were looked at with.
315
+ private val scannedRuntimes = WeakIdentitySet<ReactContext>()
316
+
317
+ override fun runtimeOfRootTag(rootTag: Int): HotUpdaterRuntimeRoots.Runtime? {
318
+ registeredRuntimes().forEach { runtime ->
319
+ if (RootViews.rootViewOf(RootViews.uiManagersOf(runtime), rootTag) != null) {
320
+ return HotUpdaterRuntimeRoots.Runtime(runCatching { runtime.sourceURL }.getOrNull(), runtime)
321
+ }
322
+ }
323
+ return null
324
+ }
325
+
326
+ override fun runtimesShownIn(activity: Activity): List<HotUpdaterRuntimeRoots.Runtime> {
327
+ val runtimes = registeredRuntimes()
328
+ if (runtimes.isEmpty()) return emptyList()
329
+ val nextRootViewTag = RootViews.nextRootViewTag() ?: return emptyList()
330
+ val uiManagers = runtimes.associateWith { RootViews.uiManagersOf(it) }
331
+ // A runtime registered since: its roots may be older than the recent tags.
332
+ if (runtimes.map(scannedRuntimes::add).any { it }) liveRootTags.rescanAll()
333
+ val rootTags =
334
+ liveRootTags.liveTags(nextRootViewTag) { rootTag ->
335
+ uiManagers.values.any { RootViews.rootViewOf(it, rootTag) != null }
336
+ }
337
+ return runtimes
338
+ .filter { runtime ->
339
+ rootTags.any { rootTag ->
340
+ RootViews.rootViewOf(uiManagers.getValue(runtime), rootTag)?.let { isShownIn(it, activity) } ==
341
+ true
342
+ }
343
+ }.map { HotUpdaterRuntimeRoots.Runtime(runCatching { it.sourceURL }.getOrNull(), it) }
344
+ }
345
+
346
+ private fun isShownIn(
347
+ view: View,
348
+ activity: Activity,
349
+ ): Boolean =
350
+ view.isAttachedToWindow &&
351
+ view.isShown &&
352
+ view.windowVisibility == View.VISIBLE &&
353
+ RootViews.ownerActivity(view.context) === activity &&
354
+ isStarted(activity)
355
+ }
356
+
357
+ internal object RootViews {
358
+ // UIManagerType.LEGACY, named DEFAULT before React Native 0.80.
359
+ private const val LEGACY_UI_MANAGER = 1
360
+
361
+ /** [runtime]'s UI managers: Fabric's, and the legacy one of a runtime on the bridge. */
362
+ fun uiManagersOf(runtime: ReactContext): List<UIManager> =
363
+ listOf(UIManagerType.FABRIC, LEGACY_UI_MANAGER)
364
+ .mapNotNull { type -> runCatching { UIManagerHelper.getUIManager(runtime, type) }.getOrNull() }
365
+ .distinct()
366
+
367
+ /**
368
+ * The view of [rootTag] one of [uiManagers] holds: Fabric's surfaces
369
+ * hold their root view under the surface id, which is the root tag,
370
+ * and the legacy UI manager its root views under theirs.
371
+ */
372
+ fun rootViewOf(
373
+ uiManagers: List<UIManager>,
374
+ rootTag: Int,
375
+ ): View? = uiManagers.firstNotNullOfOrNull { runCatching { it.resolveView(rootTag) }.getOrNull() }
376
+
377
+ /**
378
+ * Every root tag React Native handed out so far: one counter serves
379
+ * every runtime and both renderers, from 1 in steps of 10. Null when
380
+ * the counter can't be read.
381
+ */
382
+ fun nextRootViewTag(): Int? =
383
+ runCatching {
384
+ val generator = Class.forName("com.facebook.react.uimanager.ReactRootViewTagGenerator")
385
+ // A Kotlin object's field from React Native 0.80, a Java static before.
386
+ listOf("nextRootViewTag", "sNextRootViewTag")
387
+ .firstNotNullOfOrNull { name -> runCatching { generator.getDeclaredField(name) }.getOrNull() }
388
+ ?.apply { isAccessible = true }
389
+ ?.getInt(null)
390
+ }.getOrNull()
391
+
392
+ /** The activity a view's context belongs to, through wrappers such as a dialog's theme. */
393
+ fun ownerActivity(context: Context?): Activity? {
394
+ var current = context
395
+ repeat(MAX_CONTEXT_DEPTH) {
396
+ when (current) {
397
+ is Activity -> return current as Activity
398
+ is ReactContext -> return (current as ReactContext).currentActivity
399
+ is ContextWrapper -> current = (current as ContextWrapper).baseContext
400
+ else -> return null
401
+ }
402
+ }
403
+ return null
404
+ }
405
+
406
+ private const val MAX_CONTEXT_DEPTH = 16
407
+ }
408
+ }
409
+
410
+ /**
411
+ * The root tags whose root view a registered runtime holds, however many
412
+ * React Native handed out before them. React Native hands out root tags from
413
+ * one counter for every runtime and both renderers, from 1 in steps of 10,
414
+ * and lists no live roots: each tag handed out is looked at once it is, and
415
+ * the ones a runtime holds are kept until their root goes, which no age
416
+ * bounds. Tags no runtime held yet, such as main's (whose runtime isn't
417
+ * registered), are looked at again only while they are among the latest
418
+ * [maxUnheld]: a dead tag is never live again. A runtime that registers may
419
+ * hold an older one, shown before its HotUpdater module was created, so
420
+ * [rescanAll] has every tag handed out looked at once more.
421
+ */
422
+ internal class HotUpdaterLiveRootTags(
423
+ private val maxUnheld: Int = 64,
424
+ ) {
425
+ private var nextUnseen = ROOT_TAG_FIRST
426
+ private val live = linkedSetOf<Int>()
427
+ private val unheld = ArrayDeque<Int>()
428
+
429
+ /** Looks at every tag handed out again on the next [liveTags], as after a runtime registered. Rare. */
430
+ @Synchronized
431
+ fun rescanAll() {
432
+ nextUnseen = ROOT_TAG_FIRST
433
+ unheld.clear()
434
+ }
435
+
436
+ /** The live root tags, with [nextRootViewTag] React Native's counter; [isHeld] whether a runtime holds a tag's root. */
437
+ @Synchronized
438
+ fun liveTags(
439
+ nextRootViewTag: Int,
440
+ isHeld: (Int) -> Boolean,
441
+ ): List<Int> {
442
+ while (nextUnseen < nextRootViewTag) {
443
+ if (nextUnseen !in live) unheld.addLast(nextUnseen)
444
+ nextUnseen += ROOT_TAG_STEP
445
+ }
446
+ live.retainAll(isHeld)
447
+ val held = unheld.filter(isHeld)
448
+ unheld.removeAll(held.toSet())
449
+ live += held
450
+ while (unheld.size > maxUnheld) unheld.removeFirst()
451
+ return live.toList()
452
+ }
453
+
454
+ private companion object {
455
+ const val ROOT_TAG_FIRST = 1
456
+ const val ROOT_TAG_STEP = 10
457
+ }
458
+ }
459
+
460
+ /** Root views React Native's UI managers know, in any window. Main thread. */
461
+ internal interface HotUpdaterRuntimeRoots {
462
+ /** A registered runtime, by the URL it loaded its bundle from. */
463
+ data class Runtime(
464
+ val sourceURL: String?,
465
+ /** The runtime itself, which content is bound to the launch of. */
466
+ val runtime: Any? = null,
467
+ )
468
+
469
+ /** The registered runtime holding root view [rootTag], or null. */
470
+ fun runtimeOfRootTag(rootTag: Int): Runtime?
471
+
472
+ /** The registered runtimes whose root view a started [activity] shows in any of its windows. */
473
+ fun runtimesShownIn(activity: Activity): List<Runtime>
474
+ }
475
+
476
+ /** React Native root views an activity's decor view contains. Main thread. */
477
+ internal fun interface HotUpdaterDecorRoots {
478
+ /** A root view, by its root tag and the URL its runtime loaded, null before it joins one. */
479
+ data class Root(
480
+ val rootTag: Int,
481
+ val sourceURL: String?,
482
+ /** The runtime holding the view, which content is bound to the launch of. */
483
+ val runtime: Any? = null,
484
+ )
485
+
486
+ fun rootsIn(activity: Activity): List<Root>
487
+ }
488
+
489
+ internal fun rootViewsIn(view: View): List<ReactRootView> {
490
+ if (view is ReactRootView) return listOf(view)
491
+ if (view !is ViewGroup) return emptyList()
492
+ return (0 until view.childCount).flatMap { rootViewsIn(view.getChildAt(it)) }
493
+ }