@hot-updater/react-native 1.0.7 → 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 (144) 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/ReactIntegrationManagerBase.kt +0 -34
  143. package/android/src/newarch/ReactIntegrationManager.kt +0 -185
  144. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,87 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+
5
+ /**
6
+ * The native counterpart of the instance JavaScript's `HotUpdater.init`
7
+ * returns, for apps that check for and stage updates before React Native
8
+ * starts: [checkForUpdate], the returned update's `updateBundle`,
9
+ * [resetChannel], and the getters, with this instance's server and request
10
+ * defaults. [getJSBundleFile] is the bundle React Native starts with.
11
+ *
12
+ * An update staged before [getJSBundleFile] is read applies at this launch;
13
+ * one staged after it applies at the next launch. Create one with
14
+ * `HotUpdater.init`. Instances with the same [HotUpdaterOptions.name] share
15
+ * one native state; `main` is the one React Native's `HotUpdater` uses.
16
+ */
17
+ class HotUpdaterInstance internal constructor(
18
+ private val context: Context,
19
+ val options: HotUpdaterOptions,
20
+ private val environment: HotUpdaterInstanceEnvironment,
21
+ ) {
22
+ internal companion object {
23
+ /**
24
+ * @throws IllegalArgumentException when the name is invalid, or was
25
+ * already created with another configuration
26
+ */
27
+ fun create(
28
+ context: Context,
29
+ options: HotUpdaterOptions,
30
+ ): HotUpdaterInstance {
31
+ val environment = HotUpdaterInstanceEnvironment.of(options)
32
+ if (environment.isMain) {
33
+ MainInstanceConfiguration.shared.claim(options)
34
+ } else {
35
+ HotUpdaterInstanceRegistry.shared.register(context, environment)
36
+ }
37
+ return HotUpdaterInstance(context, options, environment)
38
+ }
39
+ }
40
+
41
+ private val impl: HotUpdaterImpl
42
+ get() =
43
+ if (environment.isMain) {
44
+ HotUpdater.getInstance(context)
45
+ } else {
46
+ HotUpdaterInstanceRegistry.shared.impl(context, environment)
47
+ }
48
+
49
+ /**
50
+ * The update the app should apply, or null when there is none. Unlike
51
+ * JavaScript, which returns null and calls `onError`, a failed check
52
+ * throws; it is still reported to plugins as `onUpdateError`. Waits for
53
+ * other native update work; cancelling the caller ends the wait.
54
+ */
55
+ suspend fun checkForUpdate(options: HotUpdaterCheckOptions): HotUpdaterUpdate? =
56
+ NativeUpdateCheck.checkForUpdate(context, environment.name, { impl }, NativeCheckRequest(this.options, options))
57
+
58
+ /**
59
+ * The bundle React Native starts with: the staged update, or the bundle
60
+ * embedded in the app. Reading it fixes this launch's bundle.
61
+ */
62
+ fun getJSBundleFile(): String =
63
+ if (environment.isMain) {
64
+ impl.getJSBundleFile()
65
+ } else {
66
+ HotUpdaterInstanceRegistry.shared.launchURL(context, environment)
67
+ }
68
+
69
+ /**
70
+ * Clears the runtime channel switch and returns to the default channel's
71
+ * bundle at the next launch, once other native update work finishes.
72
+ */
73
+ suspend fun resetChannel(): Boolean = NativeUpdateCheck.resetChannel(context, environment.name) { impl }
74
+
75
+ fun getAppVersion(): String? = HotUpdaterImpl.getAppVersion(context)
76
+
77
+ /** The bundle this launch runs, or null for the built-in bundle or before [getJSBundleFile] is read. */
78
+ fun getBundleId(): String? = impl.getRunningBundleId()
79
+
80
+ fun getMinBundleId(): String = HotUpdaterImpl.getMinBundleId()
81
+
82
+ fun getChannel(): String = impl.getChannel()
83
+
84
+ fun getDefaultChannel(): String = impl.getDefaultChannel()
85
+
86
+ fun getFingerprintHash(): String? = impl.getFingerprintHash()
87
+ }
@@ -0,0 +1,323 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+ import java.io.File
5
+
6
+ /**
7
+ * Where an instance keeps its native state and which configuration it reads,
8
+ * chosen by [HotUpdaterOptions.name].
9
+ *
10
+ * `main` is the state React Native's `HotUpdater`, the TurboModule, and the
11
+ * static entry points use: every path Hot Updater has always used, and the
12
+ * configuration its [HotUpdaterOptions] and the deprecated
13
+ * `HotUpdater.configure` record in [HotUpdaterConfig] before the manifest. Any
14
+ * other name keeps its state beside main's, never inside `bundle-store`, whose
15
+ * sweeps delete entries they don't know, and reads its configuration from its
16
+ * [HotUpdaterOptions] and then the manifest, never from [HotUpdaterConfig].
17
+ * The install id and the cohort belong to the device and stay shared.
18
+ */
19
+ internal class HotUpdaterInstanceEnvironment private constructor(
20
+ val name: String,
21
+ private val channel: String?,
22
+ private val fingerprintHash: String?,
23
+ private val publicKey: String?,
24
+ private val bundleAssetName: String,
25
+ // Main's impl: the values it was created with, which it reads instead of
26
+ // HotUpdaterConfig. Null for main's static entry points, which read
27
+ // HotUpdaterConfig, and for named instances.
28
+ private val mainValues: MainImplValues? = null,
29
+ ) {
30
+ companion object {
31
+ const val MAIN_NAME = "main"
32
+ const val DEFAULT_BUNDLE_ASSET_NAME = "index.android.bundle"
33
+
34
+ /** The family every main isolation key is in, whatever its version and channel. */
35
+ const val MAIN_PREFERENCES_PREFIX = "HotUpdaterPrefs_"
36
+
37
+ // Main's sweep deletes every other HotUpdaterPrefs_ file, so named
38
+ // families must not start with it. Names have no underscore, so one
39
+ // name's family is never a prefix of another's.
40
+ private const val INSTANCE_PREFERENCES_PREFIX = "HotUpdaterInstancePrefs_"
41
+
42
+ /** Beside `bundle-store` in the app's files directory: one directory per named instance. */
43
+ const val INSTANCES_DIRECTORY = "hot-updater-instances"
44
+ private const val INSTANCES_NO_BACKUP_DIRECTORY = "instances"
45
+ private const val RELEASE_CATALOG_CACHE_DIRECTORY = "release-catalog-cache"
46
+ private val NAME_PATTERN = Regex("^[a-z0-9-]{1,32}$")
47
+
48
+ val main = HotUpdaterInstanceEnvironment(MAIN_NAME, null, null, null, DEFAULT_BUNDLE_ASSET_NAME)
49
+
50
+ /** Main, fixed to [values]: what main's impl reads, so a later write to HotUpdaterConfig never reaches it. */
51
+ fun main(values: MainImplValues) = HotUpdaterInstanceEnvironment(MAIN_NAME, null, null, null, DEFAULT_BUNDLE_ASSET_NAME, values)
52
+
53
+ /**
54
+ * The environment of [options]. `main`'s configuration lives in
55
+ * [HotUpdaterConfig], where `HotUpdater.init` records its options.
56
+ * @throws IllegalArgumentException when the name is not 1 to 32
57
+ * lowercase letters, digits, or hyphens, or when `main` names a
58
+ * bundle asset other than its own
59
+ */
60
+ fun of(options: HotUpdaterOptions): HotUpdaterInstanceEnvironment {
61
+ require(NAME_PATTERN.matches(options.name)) {
62
+ "HotUpdater instance name must be 1 to 32 lowercase letters, digits, or hyphens: \"${options.name}\""
63
+ }
64
+ if (options.name == MAIN_NAME) {
65
+ // React Native's own HotUpdater starts main from this asset.
66
+ require(options.bundleAssetName == DEFAULT_BUNDLE_ASSET_NAME) {
67
+ "HotUpdater instance \"main\" launches $DEFAULT_BUNDLE_ASSET_NAME, not \"${options.bundleAssetName}\""
68
+ }
69
+ return main
70
+ }
71
+ // Main's runtime is told apart by the bundle it runs, so a named
72
+ // instance embeds a bundle of its own.
73
+ require(options.bundleAssetName != DEFAULT_BUNDLE_ASSET_NAME) {
74
+ "A named HotUpdater instance needs its own bundleAssetName, not $DEFAULT_BUNDLE_ASSET_NAME: \"${options.name}\""
75
+ }
76
+ return HotUpdaterInstanceEnvironment(
77
+ name = options.name,
78
+ channel = options.channel?.takeIf { it.isNotEmpty() },
79
+ fingerprintHash = options.fingerprintHash?.takeIf { it.isNotEmpty() },
80
+ publicKey = options.publicKey?.takeIf { it.isNotEmpty() },
81
+ bundleAssetName = options.bundleAssetName,
82
+ )
83
+ }
84
+ }
85
+
86
+ val isMain: Boolean
87
+ get() = name == MAIN_NAME
88
+
89
+ /** The directory that holds `bundle-store` and `bundle-manifest-temp`. */
90
+ fun filesRoot(context: Context): File? =
91
+ if (isMain) {
92
+ context.filesDir
93
+ } else {
94
+ context.filesDir?.let { File(File(it, INSTANCES_DIRECTORY), name) }
95
+ }
96
+
97
+ /** The client plugins' key-value store, which also holds the native update event queue. */
98
+ fun keyValueStorageFile(context: Context): File =
99
+ if (isMain) {
100
+ NoBackupStorage.file(context, KeyValueStorageService.FILENAME)
101
+ } else {
102
+ File(noBackupRoot(context), KeyValueStorageService.FILENAME)
103
+ }
104
+
105
+ /** Each directory trims to its own entries, so one instance never evicts another's. */
106
+ fun releaseCatalogCacheDirectory(context: Context): File =
107
+ if (isMain) {
108
+ File(context.noBackupFilesDir, "hot-updater-release-catalog-cache")
109
+ } else {
110
+ File(noBackupRoot(context), RELEASE_CATALOG_CACHE_DIRECTORY)
111
+ }
112
+
113
+ /** The prefix of every isolation key of this instance, which its preferences sweep owns. */
114
+ val preferencesPrefix: String
115
+ get() = if (isMain) MAIN_PREFERENCES_PREFIX else "$INSTANCE_PREFERENCES_PREFIX${name}_"
116
+
117
+ /**
118
+ * The key that names this instance's preferences and stamps its bundle
119
+ * metadata; a new key discards the stored bundles.
120
+ *
121
+ * With a fingerprint, the key is the fingerprint, the build's
122
+ * [minBundleId], and the channel: a host release that ships the same
123
+ * artifact keeps its downloads, while a new build, whose embedded bundle
124
+ * is newer, starts from it. Without one, the app version takes the
125
+ * fingerprint's and the build's place, as the appVersion strategy's
126
+ * releases are keyed by it.
127
+ */
128
+ fun isolationKey(
129
+ context: Context,
130
+ minBundleId: String = HotUpdaterImpl.getMinBundleId(),
131
+ ): String {
132
+ if (isMain) {
133
+ // The deprecated HotUpdater.configure(isolationKey:) override is
134
+ // used verbatim. It is main's own key, so a named instance never
135
+ // uses it.
136
+ val override = if (mainValues != null) mainValues.isolationKey else HotUpdaterConfig.isolationKey
137
+ override?.takeIf { it.isNotEmpty() }?.let { return it }
138
+ }
139
+
140
+ val fingerprintHash = fingerprintHash(context)
141
+ val appChannel = defaultChannel(context)
142
+ val baseKey =
143
+ if (!fingerprintHash.isNullOrEmpty()) {
144
+ "${fingerprintHash}_$minBundleId"
145
+ } else {
146
+ HotUpdaterImpl.getAppVersion(context) ?: "unknown"
147
+ }
148
+
149
+ return "$preferencesPrefix${baseKey}_$appChannel"
150
+ }
151
+
152
+ /** The channel the instance's build ships with, before any runtime switch. */
153
+ fun defaultChannel(context: Context): String =
154
+ if (isMain) {
155
+ mainValues?.channel ?: HotUpdaterImpl.getChannel(context)
156
+ } else {
157
+ channel
158
+ ?: NativeConfigUtils.getManifestString(context, NativeConfigUtils.CHANNEL_META_DATA_KEY)
159
+ ?: HotUpdaterImpl.DEFAULT_CHANNEL
160
+ }
161
+
162
+ fun fingerprintHash(context: Context): String? =
163
+ if (isMain) {
164
+ if (mainValues != null) mainValues.fingerprintHash else HotUpdaterImpl.getFingerprintHash(context)
165
+ } else {
166
+ // Every instance's bundle comes from the project the build
167
+ // fingerprinted.
168
+ fingerprintHash ?: HotUpdaterConfig.buildFingerprintHash
169
+ }
170
+
171
+ /** The PEM key bundles must be signed with, or null when signing is off. */
172
+ fun publicKey(context: Context): String? =
173
+ if (isMain) {
174
+ if (mainValues != null) mainValues.publicKey else SignatureVerifier.getPublicKeyFromConfig(context)
175
+ } else {
176
+ publicKey ?: NativeConfigUtils.getManifestString(context, NativeConfigUtils.PUBLIC_KEY_META_DATA_KEY)
177
+ }
178
+
179
+ /** The JavaScript bundle embedded in the app that the instance launches without an update. */
180
+ val fallbackBundleURL: String
181
+ get() = "assets://$bundleAssetName"
182
+
183
+ /**
184
+ * Whether [other] resolves to the same configuration, so both can share
185
+ * this name's state: an option left out and one that names the value the
186
+ * manifest or the default gives are the same.
187
+ */
188
+ fun hasSameConfiguration(
189
+ other: HotUpdaterInstanceEnvironment,
190
+ context: Context,
191
+ ): Boolean =
192
+ name == other.name &&
193
+ defaultChannel(context) == other.defaultChannel(context) &&
194
+ fingerprintHash(context) == other.fingerprintHash(context) &&
195
+ publicKey(context) == other.publicKey(context) &&
196
+ bundleAssetName == other.bundleAssetName
197
+
198
+ private fun noBackupRoot(context: Context): File = File(NoBackupStorage.file(context, INSTANCES_NO_BACKUP_DIRECTORY), name)
199
+ }
200
+
201
+ /**
202
+ * The one [HotUpdaterImpl] of each named instance, so instances with the same
203
+ * name share state, and the launch URLs they handed out, which tell each
204
+ * React Native runtime's TurboModule which instance it serves. Main's impl is
205
+ * `HotUpdater.getInstance`, which every static entry point uses.
206
+ */
207
+ internal class HotUpdaterInstanceRegistry(
208
+ private val create: (Context, HotUpdaterInstanceEnvironment) -> HotUpdaterImpl,
209
+ ) {
210
+ companion object {
211
+ val shared = HotUpdaterInstanceRegistry { context, environment -> HotUpdaterImpl(context, environment) }
212
+ }
213
+
214
+ private val environments = HashMap<String, HotUpdaterInstanceEnvironment>()
215
+ private val impls = HashMap<String, HotUpdaterImpl>()
216
+
217
+ // The last launch URL each name handed out, which the runtime that loads
218
+ // it reports back as its source URL.
219
+
220
+ /** Every launch URL each name handed out: a runtime may still run an earlier one. */
221
+ private val launchURLs = HashMap<String, MutableSet<String>>()
222
+
223
+ /**
224
+ * Records the configuration of [environment]'s name. A name reads one
225
+ * configuration, since its stored bundles were verified and keyed with it.
226
+ * @throws IllegalArgumentException when the name was registered with another
227
+ */
228
+ @Synchronized
229
+ fun register(
230
+ context: Context,
231
+ environment: HotUpdaterInstanceEnvironment,
232
+ ) {
233
+ require(!environment.isMain) { "main's state belongs to HotUpdater.getInstance" }
234
+ val registered = environments.getOrPut(environment.name) { environment }
235
+ require(registered.hasSameConfiguration(environment, context)) {
236
+ "HotUpdater instance \"${environment.name}\" was already created with another channel, " +
237
+ "fingerprintHash, publicKey, or bundleAssetName"
238
+ }
239
+ }
240
+
241
+ @Synchronized
242
+ fun impl(
243
+ context: Context,
244
+ environment: HotUpdaterInstanceEnvironment,
245
+ ): HotUpdaterImpl {
246
+ register(context, environment)
247
+ return impls.getOrPut(environment.name) { create(context, environment) }
248
+ }
249
+
250
+ /** The impl of [name], or null when no named instance of it has one yet. */
251
+ @Synchronized
252
+ fun impl(name: String): HotUpdaterImpl? = impls[name]
253
+
254
+ /**
255
+ * The bundle a React Native host of [environment] starts with, recorded
256
+ * so the TurboModule of the runtime that loads it finds this instance.
257
+ */
258
+ fun launchURL(
259
+ context: Context,
260
+ environment: HotUpdaterInstanceEnvironment,
261
+ ): String {
262
+ // Reading it prepares the launch, which reads storage, so it runs
263
+ // outside the registry's lock.
264
+ val url = impl(context, environment).getJSBundleFile()
265
+ synchronized(this) { launchURLs.getOrPut(environment.name) { mutableSetOf() }.add(url) }
266
+ return url
267
+ }
268
+
269
+ /**
270
+ * The name of the instance whose bundle a React Native runtime runs, from
271
+ * the URL the runtime loaded it from: the launch URL a named instance
272
+ * handed out, then a file inside a named instance's storage (a bundle it
273
+ * staged, loaded after a reload), then main, which covers main's own
274
+ * bundles, the development server, and URLs no instance knows. Only
275
+ * names with an impl are candidates.
276
+ */
277
+ @Synchronized
278
+ fun resolveName(
279
+ context: Context,
280
+ sourceURL: String?,
281
+ ): String {
282
+ // Crash recovery asks for every view it checks; without a named
283
+ // instance, main's answer needs no path work.
284
+ if (sourceURL.isNullOrEmpty() || impls.isEmpty()) return HotUpdaterInstanceEnvironment.MAIN_NAME
285
+
286
+ // Two names that hand out the same URL, such as the same embedded
287
+ // bundle, can't be told apart by it.
288
+ launchURLs
289
+ .filter { (name, urls) -> sourceURL in urls && name in impls }
290
+ .keys
291
+ .singleOrNull()
292
+ ?.let { return it }
293
+
294
+ val sourceSegments = pathSegments(canonicalPath(sourceURL.removePrefix("file://")))
295
+ impls.keys
296
+ .firstOrNull { name ->
297
+ val root = environments[name]?.filesRoot(context) ?: return@firstOrNull false
298
+ val rootSegments = pathSegments(canonicalPath(root.path))
299
+ // Whole segments, so "app" never claims "app-2"'s files.
300
+ sourceSegments.size > rootSegments.size && sourceSegments.subList(0, rootSegments.size) == rootSegments
301
+ }?.let { return it }
302
+
303
+ return HotUpdaterInstanceEnvironment.MAIN_NAME
304
+ }
305
+
306
+ // React Native may hold `/data/data/...` where `filesDir` is
307
+ // `/data/user/0/...`, a symlink to the same directory.
308
+ private fun canonicalPath(path: String): String =
309
+ if (path.startsWith("/")) runCatching { File(path).canonicalPath }.getOrDefault(path) else path
310
+
311
+ private fun pathSegments(path: String): List<String> {
312
+ if (!path.startsWith("/")) return emptyList()
313
+ val segments = ArrayList<String>()
314
+ path.split('/').forEach { segment ->
315
+ when (segment) {
316
+ "", "." -> Unit
317
+ ".." -> if (segments.isNotEmpty()) segments.removeAt(segments.size - 1)
318
+ else -> segments += segment
319
+ }
320
+ }
321
+ return segments
322
+ }
323
+ }
@@ -0,0 +1,46 @@
1
+ package com.hotupdater
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+
6
+ /**
7
+ * The main thread, where views, lifecycle callbacks, and crash recovery's
8
+ * delayed work run. Tests stand in for it to run posted work by hand.
9
+ */
10
+ internal interface HotUpdaterMainThread {
11
+ fun isCurrent(): Boolean
12
+
13
+ fun post(task: Runnable)
14
+
15
+ fun postDelayed(
16
+ task: Runnable,
17
+ delayMs: Long,
18
+ )
19
+
20
+ /** Removes [task] while it waits; one already running is not stopped. */
21
+ fun remove(task: Runnable)
22
+
23
+ companion object {
24
+ val android: HotUpdaterMainThread =
25
+ object : HotUpdaterMainThread {
26
+ private val handler by lazy { Handler(Looper.getMainLooper()) }
27
+
28
+ override fun isCurrent(): Boolean = Looper.myLooper() == Looper.getMainLooper()
29
+
30
+ override fun post(task: Runnable) {
31
+ handler.post(task)
32
+ }
33
+
34
+ override fun postDelayed(
35
+ task: Runnable,
36
+ delayMs: Long,
37
+ ) {
38
+ handler.postDelayed(task, delayMs)
39
+ }
40
+
41
+ override fun remove(task: Runnable) {
42
+ handler.removeCallbacks(task)
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,30 @@
1
+ package com.hotupdater
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import android.os.Build
6
+ import java.io.File
7
+
8
+ /**
9
+ * The process [HotUpdaterRestartActivity] runs in (its manifest's
10
+ * `android:process`). Android runs the app's `Application.onCreate` there too,
11
+ * so an app that starts React Native from it, as a brownfield app does, reaches
12
+ * Hot Updater in this process as well.
13
+ */
14
+ internal object HotUpdaterProcess {
15
+ private const val RESTART_PROCESS_SUFFIX = ":hotupdater_restart"
16
+
17
+ fun isRestartProcess(context: Context): Boolean = isRestartProcessName(context.packageName, currentProcessName())
18
+
19
+ fun isRestartProcessName(
20
+ packageName: String,
21
+ processName: String?,
22
+ ): Boolean = processName == packageName + RESTART_PROCESS_SUFFIX
23
+
24
+ private fun currentProcessName(): String? =
25
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
26
+ Application.getProcessName()
27
+ } else {
28
+ runCatching { File("/proc/self/cmdline").readText().trimEnd('\u0000') }.getOrNull()
29
+ }
30
+ }