@hot-updater/react-native 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +492 -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,492 @@
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
+ HotUpdaterDecorRoots.Root(rootView.rootViewTag, runCatching { runtime?.sourceURL }.getOrNull(), runtime)
64
+ }
65
+ }
66
+ }
67
+
68
+ companion object {
69
+ val shared = HotUpdaterRecoveryAttribution()
70
+
71
+ // Instance names have no "@", so this names no launch.
72
+ private const val EARLIER_LAUNCH_SUFFIX = "@earlier-launch"
73
+
74
+ // A JavaScript stack frame's source: an absolute path or a URL, such
75
+ // as Hermes' "at f (address at /data/.../index.android.bundle:1:2345)",
76
+ // "f@/data/.../index.android.bundle:1:2345", or "assets://widget.android.bundle:1:9".
77
+ private val FRAME_SOURCE = Regex("""((?:[A-Za-z][A-Za-z0-9+.-]*://|/)[^\s()]*?)(?::\d+){1,2}(?=[)\s,]|$)""")
78
+
79
+ /** The bundle locations the frames of [stack] run from. */
80
+ fun frameSources(stack: String): List<String> =
81
+ FRAME_SOURCE
82
+ .findAll(stack)
83
+ .map { it.groupValues[1] }
84
+ .distinct()
85
+ .toList()
86
+
87
+ /**
88
+ * The named instance whose bundle every located frame of [stacks]
89
+ * runs from, or null when no frame is located or they name main, an
90
+ * unknown bundle, or more than one instance. A runtime runs one
91
+ * bundle, so its error's frames name its instance even before its
92
+ * HotUpdater module exists.
93
+ */
94
+ fun namedInstanceOfStack(
95
+ stacks: List<String>,
96
+ resolve: (String) -> String,
97
+ ): String? = instanceOfStack(stacks, resolve)?.takeIf { it != HotUpdaterInstanceEnvironment.MAIN_NAME }
98
+
99
+ /**
100
+ * The instance, main included, whose bundle every located frame of
101
+ * [stacks] runs from; null when no frame is located or they name more
102
+ * than one instance. Main's frames are main's own bundle or any the
103
+ * named instances don't know, as [HotUpdaterInstanceRegistry.resolveName] has it.
104
+ */
105
+ fun instanceOfStack(
106
+ stacks: List<String>,
107
+ resolve: (String) -> String,
108
+ ): String? {
109
+ val sources = stacks.flatMap(::frameSources).distinct()
110
+ if (sources.isEmpty()) return null
111
+ return sources.map(resolve).toSet().singleOrNull()
112
+ }
113
+
114
+ /**
115
+ * Whether a view of [viewInstance] counts for [instanceName]. Main
116
+ * takes every view no named instance claims, so a view whose runtime
117
+ * isn't known yet counts for main, as every view always has.
118
+ */
119
+ fun owns(
120
+ instanceName: String,
121
+ viewInstance: String?,
122
+ ): Boolean =
123
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) {
124
+ viewInstance == null || viewInstance == HotUpdaterInstanceEnvironment.MAIN_NAME
125
+ } else {
126
+ viewInstance == instanceName
127
+ }
128
+ }
129
+
130
+ @Volatile
131
+ private var context: Context? = null
132
+ private var trackedApplication: Application? = null
133
+
134
+ // Activities that may hold a React Native view whose first content appears.
135
+ private val activities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
136
+ private val startedActivities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
137
+ private val runtimes = mutableListOf<WeakReference<ReactContext>>()
138
+
139
+ private val activityCallbacks =
140
+ object : Application.ActivityLifecycleCallbacks {
141
+ override fun onActivityCreated(
142
+ activity: Activity,
143
+ savedInstanceState: Bundle?,
144
+ ) {
145
+ synchronized(activities) { activities += activity }
146
+ }
147
+
148
+ override fun onActivityStarted(activity: Activity) {
149
+ synchronized(activities) {
150
+ activities += activity
151
+ startedActivities += activity
152
+ }
153
+ }
154
+
155
+ override fun onActivityDestroyed(activity: Activity) {
156
+ synchronized(activities) {
157
+ activities -= activity
158
+ startedActivities -= activity
159
+ }
160
+ }
161
+
162
+ override fun onActivityResumed(activity: Activity) {}
163
+
164
+ override fun onActivityPaused(activity: Activity) {}
165
+
166
+ override fun onActivityStopped(activity: Activity) {
167
+ synchronized(activities) { startedActivities -= activity }
168
+ }
169
+
170
+ override fun onActivitySaveInstanceState(
171
+ activity: Activity,
172
+ outState: Bundle,
173
+ ) {}
174
+ }
175
+
176
+ /** Starts following the app's activities, once per process. */
177
+ fun track(context: Context) {
178
+ this.context = context.applicationContext
179
+ val application = context.applicationContext as? Application ?: return
180
+ synchronized(this) {
181
+ if (trackedApplication != null) return
182
+ trackedApplication = application
183
+ }
184
+ application.registerActivityLifecycleCallbacks(activityCallbacks)
185
+ }
186
+
187
+ /**
188
+ * Remembers [activity], which the app handed over as a context: it may
189
+ * start, or have started, before [track] registered for lifecycle
190
+ * callbacks. It counts as started only while it is.
191
+ */
192
+ fun noteActivity(activity: Activity) {
193
+ synchronized(activities) { activities += activity }
194
+ }
195
+
196
+ /**
197
+ * Remembers a runtime whose HotUpdater module was created. Its current
198
+ * activity is searched for its views even when that activity started
199
+ * before [track], as when a brownfield app adds React Native to a screen
200
+ * already shown.
201
+ */
202
+ fun registerRuntime(reactContext: ReactContext) {
203
+ if (context == null) context = reactContext.applicationContext
204
+ synchronized(runtimes) {
205
+ runtimes.removeAll { it.get() == null }
206
+ if (runtimes.none { it.get() === reactContext }) runtimes += WeakReference(reactContext)
207
+ }
208
+ }
209
+
210
+ /** Forgets [reactContext], whose runtime was torn down. */
211
+ fun unregisterRuntime(reactContext: ReactContext) {
212
+ synchronized(runtimes) { runtimes.removeAll { it.get().let { runtime -> runtime == null || runtime === reactContext } } }
213
+ }
214
+
215
+ /**
216
+ * The activities started now, as far as this process has seen: the ones
217
+ * seen starting since [track], and any other known activity, such as a
218
+ * registered runtime's current one, that reports it is started now. A
219
+ * runtime keeps its activity after it stops. A gate created after they
220
+ * started reads them here.
221
+ */
222
+ fun startedActivities(): List<Activity> {
223
+ val (seenStarted, known) = synchronized(activities) { startedActivities.toList() to activities.toList() }
224
+ val runtimeActivities = synchronized(runtimes) { runtimes.mapNotNull { it.get()?.currentActivity } }
225
+ return (seenStarted + (known + runtimeActivities).filter(isStarted)).distinct()
226
+ }
227
+
228
+ /** [namedInstanceOfStack] with this process's instances. */
229
+ fun namedInstanceOfStack(stacks: List<String>): String? {
230
+ val context = context ?: return null
231
+ return namedInstanceOfStack(stacks) { resolveName(context, it) }
232
+ }
233
+
234
+ /** [instanceOfStack] with this process's instances. */
235
+ fun instanceOfStack(stacks: List<String>): String? {
236
+ val context = context ?: return null
237
+ return instanceOfStack(stacks) { resolveName(context, it) }
238
+ }
239
+
240
+ /** The instance whose bundle runs from [sourceURL], or null when it isn't known. */
241
+ fun instanceOf(sourceURL: String?): String? {
242
+ val url = sourceURL?.takeIf { it.isNotEmpty() } ?: return null
243
+ val context = context ?: return HotUpdaterInstanceEnvironment.MAIN_NAME
244
+ return resolveName(context, url)
245
+ }
246
+
247
+ /**
248
+ * The instance whose current launch a view of a runtime that loaded
249
+ * [sourceURL] belongs to. A named host may still show a view of its
250
+ * previous runtime while its next launch starts: that view belongs to an
251
+ * earlier launch, which neither arms nor verifies the current one. Main
252
+ * takes any view, as it always has. Every view that arms or verifies a
253
+ * launch is attributed here, null before the view joins a runtime.
254
+ */
255
+ fun launchInstanceOf(
256
+ sourceURL: String?,
257
+ runtime: Any? = null,
258
+ ): String? {
259
+ val instanceName = instanceOf(sourceURL) ?: return null
260
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) return instanceName
261
+ // The launch [runtime] registered for, when it did; else its bundle's URL.
262
+ val current = runtimeRunsCurrentLaunch(instanceName, runtime) ?: runsCurrentLaunch(instanceName, sourceURL)
263
+ return if (current) instanceName else "$instanceName$EARLIER_LAUNCH_SUFFIX"
264
+ }
265
+
266
+ /** Whether [activity] shows a React Native view that counts for [instanceName]. */
267
+ fun showsViewOf(
268
+ activity: Activity,
269
+ instanceName: String,
270
+ ): Boolean {
271
+ if (decorRoots.rootsIn(activity).any { owns(instanceName, launchInstanceOf(it.sourceURL, it.runtime)) }) return true
272
+ // Main takes the views no named instance claims, which a decor view shows.
273
+ if (instanceName == HotUpdaterInstanceEnvironment.MAIN_NAME) return false
274
+ return runtimeRoots.runtimesShownIn(activity).any { launchInstanceOf(it.sourceURL, it.runtime) == instanceName }
275
+ }
276
+
277
+ /**
278
+ * The instance of the root view whose first content appeared, which React
279
+ * Native names by its root tag; null when no known activity shows it or
280
+ * its runtime isn't known yet.
281
+ */
282
+ fun instanceOfRootTag(rootTag: Int): String? = rootOf(rootTag)?.instanceName
283
+
284
+ /** A root view's instance, as [instanceOfRootTag] has it, and the runtime that holds it. */
285
+ data class RootOwner(
286
+ val instanceName: String,
287
+ val runtime: Any?,
288
+ )
289
+
290
+ /** [instanceOfRootTag], with the runtime holding the root view, by which content is bound to that runtime's launch. */
291
+ fun rootOf(rootTag: Int): RootOwner? {
292
+ runtimeRoots.runtimeOfRootTag(rootTag)?.let { root ->
293
+ return launchInstanceOf(root.sourceURL, root.runtime)?.let { RootOwner(it, root.runtime) }
294
+ }
295
+ val candidates =
296
+ synchronized(activities) { activities.toList() } +
297
+ synchronized(runtimes) { runtimes.mapNotNull { it.get()?.currentActivity } }
298
+ candidates.distinct().forEach { activity ->
299
+ // A view whose runtime isn't known yet tells nothing either.
300
+ decorRoots.rootsIn(activity).firstOrNull { it.rootTag == rootTag }?.let { root ->
301
+ return launchInstanceOf(root.sourceURL, root.runtime)?.let { RootOwner(it, root.runtime) }
302
+ }
303
+ }
304
+ return null
305
+ }
306
+
307
+ private fun registeredRuntimes(): List<ReactContext> = synchronized(runtimes) { runtimes.mapNotNull { it.get() } }
308
+
309
+ /** Root views through the registered runtimes' UI managers. Main thread, as UI managers require. */
310
+ private inner class UIManagerRuntimeRoots : HotUpdaterRuntimeRoots {
311
+ private val liveRootTags = HotUpdaterLiveRootTags()
312
+
313
+ // The runtimes the root tags were looked at with.
314
+ private val scannedRuntimes = WeakIdentitySet<ReactContext>()
315
+
316
+ override fun runtimeOfRootTag(rootTag: Int): HotUpdaterRuntimeRoots.Runtime? {
317
+ registeredRuntimes().forEach { runtime ->
318
+ if (RootViews.rootViewOf(RootViews.uiManagersOf(runtime), rootTag) != null) {
319
+ return HotUpdaterRuntimeRoots.Runtime(runCatching { runtime.sourceURL }.getOrNull(), runtime)
320
+ }
321
+ }
322
+ return null
323
+ }
324
+
325
+ override fun runtimesShownIn(activity: Activity): List<HotUpdaterRuntimeRoots.Runtime> {
326
+ val runtimes = registeredRuntimes()
327
+ if (runtimes.isEmpty()) return emptyList()
328
+ val nextRootViewTag = RootViews.nextRootViewTag() ?: return emptyList()
329
+ val uiManagers = runtimes.associateWith { RootViews.uiManagersOf(it) }
330
+ // A runtime registered since: its roots may be older than the recent tags.
331
+ if (runtimes.map(scannedRuntimes::add).any { it }) liveRootTags.rescanAll()
332
+ val rootTags =
333
+ liveRootTags.liveTags(nextRootViewTag) { rootTag ->
334
+ uiManagers.values.any { RootViews.rootViewOf(it, rootTag) != null }
335
+ }
336
+ return runtimes
337
+ .filter { runtime ->
338
+ rootTags.any { rootTag ->
339
+ RootViews.rootViewOf(uiManagers.getValue(runtime), rootTag)?.let { isShownIn(it, activity) } ==
340
+ true
341
+ }
342
+ }.map { HotUpdaterRuntimeRoots.Runtime(runCatching { it.sourceURL }.getOrNull(), it) }
343
+ }
344
+
345
+ private fun isShownIn(
346
+ view: View,
347
+ activity: Activity,
348
+ ): Boolean =
349
+ view.isAttachedToWindow &&
350
+ view.isShown &&
351
+ view.windowVisibility == View.VISIBLE &&
352
+ RootViews.ownerActivity(view.context) === activity &&
353
+ isStarted(activity)
354
+ }
355
+
356
+ internal object RootViews {
357
+ // UIManagerType.LEGACY, named DEFAULT before React Native 0.80.
358
+ private const val LEGACY_UI_MANAGER = 1
359
+
360
+ /** [runtime]'s UI managers: Fabric's, and the legacy one of a runtime on the bridge. */
361
+ fun uiManagersOf(runtime: ReactContext): List<UIManager> =
362
+ listOf(UIManagerType.FABRIC, LEGACY_UI_MANAGER)
363
+ .mapNotNull { type -> runCatching { UIManagerHelper.getUIManager(runtime, type) }.getOrNull() }
364
+ .distinct()
365
+
366
+ /**
367
+ * The view of [rootTag] one of [uiManagers] holds: Fabric's surfaces
368
+ * hold their root view under the surface id, which is the root tag,
369
+ * and the legacy UI manager its root views under theirs.
370
+ */
371
+ fun rootViewOf(
372
+ uiManagers: List<UIManager>,
373
+ rootTag: Int,
374
+ ): View? = uiManagers.firstNotNullOfOrNull { runCatching { it.resolveView(rootTag) }.getOrNull() }
375
+
376
+ /**
377
+ * Every root tag React Native handed out so far: one counter serves
378
+ * every runtime and both renderers, from 1 in steps of 10. Null when
379
+ * the counter can't be read.
380
+ */
381
+ fun nextRootViewTag(): Int? =
382
+ runCatching {
383
+ val generator = Class.forName("com.facebook.react.uimanager.ReactRootViewTagGenerator")
384
+ // A Kotlin object's field from React Native 0.80, a Java static before.
385
+ listOf("nextRootViewTag", "sNextRootViewTag")
386
+ .firstNotNullOfOrNull { name -> runCatching { generator.getDeclaredField(name) }.getOrNull() }
387
+ ?.apply { isAccessible = true }
388
+ ?.getInt(null)
389
+ }.getOrNull()
390
+
391
+ /** The activity a view's context belongs to, through wrappers such as a dialog's theme. */
392
+ fun ownerActivity(context: Context?): Activity? {
393
+ var current = context
394
+ repeat(MAX_CONTEXT_DEPTH) {
395
+ when (current) {
396
+ is Activity -> return current as Activity
397
+ is ReactContext -> return (current as ReactContext).currentActivity
398
+ is ContextWrapper -> current = (current as ContextWrapper).baseContext
399
+ else -> return null
400
+ }
401
+ }
402
+ return null
403
+ }
404
+
405
+ private const val MAX_CONTEXT_DEPTH = 16
406
+ }
407
+ }
408
+
409
+ /**
410
+ * The root tags whose root view a registered runtime holds, however many
411
+ * React Native handed out before them. React Native hands out root tags from
412
+ * one counter for every runtime and both renderers, from 1 in steps of 10,
413
+ * and lists no live roots: each tag handed out is looked at once it is, and
414
+ * the ones a runtime holds are kept until their root goes, which no age
415
+ * bounds. Tags no runtime held yet, such as main's (whose runtime isn't
416
+ * registered), are looked at again only while they are among the latest
417
+ * [maxUnheld]: a dead tag is never live again. A runtime that registers may
418
+ * hold an older one, shown before its HotUpdater module was created, so
419
+ * [rescanAll] has every tag handed out looked at once more.
420
+ */
421
+ internal class HotUpdaterLiveRootTags(
422
+ private val maxUnheld: Int = 64,
423
+ ) {
424
+ private var nextUnseen = ROOT_TAG_FIRST
425
+ private val live = linkedSetOf<Int>()
426
+ private val unheld = ArrayDeque<Int>()
427
+
428
+ /** Looks at every tag handed out again on the next [liveTags], as after a runtime registered. Rare. */
429
+ @Synchronized
430
+ fun rescanAll() {
431
+ nextUnseen = ROOT_TAG_FIRST
432
+ unheld.clear()
433
+ }
434
+
435
+ /** The live root tags, with [nextRootViewTag] React Native's counter; [isHeld] whether a runtime holds a tag's root. */
436
+ @Synchronized
437
+ fun liveTags(
438
+ nextRootViewTag: Int,
439
+ isHeld: (Int) -> Boolean,
440
+ ): List<Int> {
441
+ while (nextUnseen < nextRootViewTag) {
442
+ if (nextUnseen !in live) unheld.addLast(nextUnseen)
443
+ nextUnseen += ROOT_TAG_STEP
444
+ }
445
+ live.retainAll(isHeld)
446
+ val held = unheld.filter(isHeld)
447
+ unheld.removeAll(held.toSet())
448
+ live += held
449
+ while (unheld.size > maxUnheld) unheld.removeFirst()
450
+ return live.toList()
451
+ }
452
+
453
+ private companion object {
454
+ const val ROOT_TAG_FIRST = 1
455
+ const val ROOT_TAG_STEP = 10
456
+ }
457
+ }
458
+
459
+ /** Root views React Native's UI managers know, in any window. Main thread. */
460
+ internal interface HotUpdaterRuntimeRoots {
461
+ /** A registered runtime, by the URL it loaded its bundle from. */
462
+ data class Runtime(
463
+ val sourceURL: String?,
464
+ /** The runtime itself, which content is bound to the launch of. */
465
+ val runtime: Any? = null,
466
+ )
467
+
468
+ /** The registered runtime holding root view [rootTag], or null. */
469
+ fun runtimeOfRootTag(rootTag: Int): Runtime?
470
+
471
+ /** The registered runtimes whose root view a started [activity] shows in any of its windows. */
472
+ fun runtimesShownIn(activity: Activity): List<Runtime>
473
+ }
474
+
475
+ /** React Native root views an activity's decor view contains. Main thread. */
476
+ internal fun interface HotUpdaterDecorRoots {
477
+ /** A root view, by its root tag and the URL its runtime loaded, null before it joins one. */
478
+ data class Root(
479
+ val rootTag: Int,
480
+ val sourceURL: String?,
481
+ /** The runtime holding the view, which content is bound to the launch of. */
482
+ val runtime: Any? = null,
483
+ )
484
+
485
+ fun rootsIn(activity: Activity): List<Root>
486
+ }
487
+
488
+ internal fun rootViewsIn(view: View): List<ReactRootView> {
489
+ if (view is ReactRootView) return listOf(view)
490
+ if (view !is ViewGroup) return emptyList()
491
+ return (0 until view.childCount).flatMap { rootViewsIn(view.getChildAt(it)) }
492
+ }