@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,137 @@
1
+ package com.hotupdater
2
+
3
+ /**
4
+ * Which instances a crash charges. Each instance's launch is watched on its
5
+ * own, but a crash ends the whole process, so the process-wide crash paths
6
+ * (the uncaught exception handler, the native signal handler, and JavaScript
7
+ * fatal errors) ask this table which instances' crash markers to write.
8
+ *
9
+ * Each instance that monitors a launch takes one of [capacity] slots, which
10
+ * the native signal handler mirrors (HotUpdaterRecovery.cpp): main always
11
+ * takes slot 0. The signal handler can't allocate, so the slots are fixed; an
12
+ * instance that finds them full gets no marker from a native crash, and its
13
+ * unfinished launch still rolls back through `launchInProgress`.
14
+ */
15
+ internal class HotUpdaterRecoverySlots(
16
+ private val capacity: Int = SLOT_COUNT,
17
+ ) {
18
+ /** What one instance's crash monitoring knows about its current launch. */
19
+ data class Launch(
20
+ val name: String,
21
+ val bundleId: String?,
22
+ /** The launch waits for its first content, so a crash rolls it back. */
23
+ val shouldRollback: Boolean,
24
+ /** One of its views was shown. Main counts from the start, as it always has. */
25
+ val armed: Boolean,
26
+ /** Its first content appeared, so its runtime finished loading its bundle. */
27
+ val contentAppeared: Boolean = false,
28
+ ) {
29
+ val isMain: Boolean
30
+ get() = name == HotUpdaterInstanceEnvironment.MAIN_NAME
31
+
32
+ /** A crash nobody claims charges this launch. */
33
+ val unverified: Boolean
34
+ get() = shouldRollback && armed
35
+ }
36
+
37
+ private val slots = HashMap<String, Int>()
38
+ private var attributedName: String? = null
39
+
40
+ /** The slot of [name], taken on first use, or null when every slot is taken. */
41
+ @Synchronized
42
+ fun slotOf(name: String): Int? {
43
+ if (name == HotUpdaterInstanceEnvironment.MAIN_NAME) return MAIN_SLOT
44
+ slots[name]?.let { return it }
45
+ // Names stay in their slot for the process: there are only a few, and a
46
+ // slot reused for another name could write a marker into the wrong store.
47
+ val slot = slots.size + 1
48
+ if (slot >= capacity) return null
49
+ slots[name] = slot
50
+ return slot
51
+ }
52
+
53
+ /** The slot [name] already has, without taking one. */
54
+ @Synchronized
55
+ fun assignedSlot(name: String): Int? = if (name == HotUpdaterInstanceEnvironment.MAIN_NAME) MAIN_SLOT else slots[name]
56
+
57
+ /**
58
+ * Records that the crash about to happen is [name]'s JavaScript fatal
59
+ * error, so the crash paths it ends in charge no other instance.
60
+ */
61
+ @Synchronized
62
+ fun attribute(name: String) {
63
+ attributedName = name
64
+ }
65
+
66
+ /** Ends the claim: the claimed error did not end the process. */
67
+ @Synchronized
68
+ fun clearAttribution() {
69
+ attributedName = null
70
+ }
71
+
72
+ /**
73
+ * The instance the current crash belongs to, or null when nobody claimed
74
+ * it. A claim holds until its release ([clearAttribution]): a fatal error
75
+ * that didn't end the process, such as one a development build shows on
76
+ * screen, must not claim a later crash, and the launches it left alone
77
+ * count again only with that release.
78
+ */
79
+ @Synchronized
80
+ fun attributedName(): String? = attributedName
81
+
82
+ companion object {
83
+ /** Kept equal to kSlotCount in HotUpdaterRecovery.cpp. */
84
+ const val SLOT_COUNT = 8
85
+ const val MAIN_SLOT = 0
86
+
87
+ /**
88
+ * A claim ends when its error's handler returns. This only releases a
89
+ * claim whose handler never returned nor ended the process. Kept
90
+ * equal to kNativeClaimFallbackMilliseconds in HotUpdaterRecovery.cpp.
91
+ */
92
+ const val CLAIM_FAIL_SAFE_MS = 60_000L
93
+
94
+ /**
95
+ * The instance a JavaScript error no runtime claimed most likely came
96
+ * from, or null to leave it unclaimed. A runtime whose bundle throws
97
+ * while it loads, before JavaScript reaches HotUpdater, never has its
98
+ * module, so nothing hooks its errors. Any launch of this process
99
+ * whose runtime hasn't shown it finished loading may have thrown: one
100
+ * without first content and, for a named instance, without its module
101
+ * (main's runtime never registers one). Whether it may roll back says
102
+ * nothing about that: a built-in bundle throws as well. Only when the
103
+ * one such launch is a named launch waiting for its first content is
104
+ * the error its: charging every armed launch instead would miss it
105
+ * until one of its views is shown, and the next start would select
106
+ * the same failing bundle.
107
+ */
108
+ fun inferJavaScriptErrorOwner(
109
+ launches: Collection<Launch>,
110
+ registeredRuntimes: Set<String>,
111
+ ): String? {
112
+ val possibleOwner =
113
+ launches
114
+ .filter { !it.contentAppeared && (it.isMain || it.name !in registeredRuntimes) }
115
+ .singleOrNull() ?: return null
116
+ return possibleOwner.name.takeIf { !possibleOwner.isMain && possibleOwner.shouldRollback }
117
+ }
118
+
119
+ /**
120
+ * The launches whose crash markers a crash writes. A crash claimed by
121
+ * an instance writes only that instance's marker, for any launch still
122
+ * waiting for its first content: its runtime threw, shown or not.
123
+ * Otherwise every launch a crash can be blamed on gets one: an armed
124
+ * launch still waiting for its first content. Main also records a
125
+ * verified launch, which never rolls back, as it always has.
126
+ */
127
+ fun markerTargets(
128
+ launches: Collection<Launch>,
129
+ attributedName: String?,
130
+ ): List<Launch> =
131
+ if (attributedName != null) {
132
+ launches.filter { it.name == attributedName && (it.isMain || it.shouldRollback) }
133
+ } else {
134
+ launches.filter { it.isMain || it.unverified }
135
+ }
136
+ }
137
+ }
@@ -30,6 +30,10 @@ internal class HotUpdaterRecoveryState {
30
30
  @Synchronized
31
31
  fun snapshot(): Launch? = launch
32
32
 
33
+ /** Whether the current launch's first content appeared. */
34
+ @Synchronized
35
+ fun hasContentAppeared(): Boolean = launch != null && contentAppeared
36
+
33
37
  @Synchronized
34
38
  fun stop() {
35
39
  launch = null
@@ -0,0 +1,53 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+
5
+ /**
6
+ * The native instance a TurboModule serves: the one whose bundle its React
7
+ * Native runtime runs. Each runtime has its own module, so the runtimes of
8
+ * two hosts each reach their own instance, and JavaScript needs no name.
9
+ */
10
+ internal class HotUpdaterRuntimeInstance(
11
+ private val context: Context,
12
+ private val sourceURL: () -> String?,
13
+ private val registry: HotUpdaterInstanceRegistry = HotUpdaterInstanceRegistry.shared,
14
+ private val main: () -> HotUpdaterImpl = { HotUpdater.getInstance(context) },
15
+ ) {
16
+ @Volatile
17
+ private var resolvedName: String? = null
18
+
19
+ /**
20
+ * Resolved on first use and kept, since a runtime runs one bundle until
21
+ * it reloads, which creates a new module. JavaScript calls the module only
22
+ * after its bundle loads; before that the runtime has no source URL, and
23
+ * main serves without being kept.
24
+ */
25
+ val name: String
26
+ get() {
27
+ resolvedName?.let { return it }
28
+ val url = sourceURL()?.takeIf { it.isNotEmpty() } ?: return HotUpdaterInstanceEnvironment.MAIN_NAME
29
+ return registry.resolveName(context, url).also { resolvedName = it }
30
+ }
31
+
32
+ val impl: HotUpdaterImpl
33
+ get() = impl(name)
34
+
35
+ fun impl(name: String): HotUpdaterImpl = if (name == HotUpdaterInstanceEnvironment.MAIN_NAME) main() else registry.impl(name) ?: main()
36
+
37
+ /**
38
+ * Why [call] can't run, or null when it can. Reloading reaches main's
39
+ * React host or restarts the app, so only main's runtime may until each
40
+ * host reloads itself.
41
+ */
42
+ fun reloadUnsupportedReason(): String? {
43
+ val name = name
44
+ if (name == HotUpdaterInstanceEnvironment.MAIN_NAME) return null
45
+ return RELOAD_UNSUPPORTED_MESSAGE
46
+ }
47
+ }
48
+
49
+ /** The rejection code and message iOS uses too. */
50
+ internal const val RELOAD_UNSUPPORTED_CODE = "RELOAD_UNSUPPORTED"
51
+ internal const val RELOAD_UNSUPPORTED_MESSAGE =
52
+ "HotUpdater.reload() and HotUpdater.reloadProcess() are not supported in a React Native runtime of a named " +
53
+ "HotUpdaterInstance yet. Reload its React Native host from native code."
@@ -0,0 +1,149 @@
1
+ package com.hotupdater
2
+
3
+ /** Which Release catalog an update check reads: the app version's or the fingerprint's. */
4
+ enum class HotUpdaterUpdateStrategy(
5
+ internal val value: String,
6
+ internal val releaseStrategy: ReleaseStrategy,
7
+ ) {
8
+ APP_VERSION("appVersion", ReleaseStrategy.APP_VERSION),
9
+ FINGERPRINT("fingerprint", ReleaseStrategy.FINGERPRINT),
10
+ }
11
+
12
+ /** Whether an update moves the app forward or rolls it back. */
13
+ enum class HotUpdaterUpdateStatus {
14
+ UPDATE,
15
+ ROLLBACK,
16
+ }
17
+
18
+ /**
19
+ * The values JavaScript passes to `HotUpdater.init`: the server and the
20
+ * request defaults of a [HotUpdaterInstance], plus the native state it uses.
21
+ * `main` records [channel], [fingerprintHash], and [publicKey] before its
22
+ * native state is first read; a value left null keeps one the deprecated
23
+ * `HotUpdater.configure` set.
24
+ */
25
+ class HotUpdaterOptions
26
+ @JvmOverloads
27
+ constructor(
28
+ /** The update server's absolute HTTP(S) URL. */
29
+ val baseURL: String,
30
+ /** Sent with every request; a check's own headers override them. */
31
+ val requestHeaders: Map<String, String> = emptyMap(),
32
+ /** Each request's timeout, unless a check sets its own. */
33
+ val requestTimeoutMillis: Long = 5_000,
34
+ /**
35
+ * Which native state the instance uses, like a Firebase app name: `main`,
36
+ * the default, is the state React Native's own `HotUpdater` uses. Any other
37
+ * name keeps its bundles, settings, and stored values apart from every
38
+ * other name's; instances with the same name share them. 1 to 32
39
+ * lowercase letters, digits, or hyphens.
40
+ */
41
+ val name: String = HotUpdaterInstanceEnvironment.MAIN_NAME,
42
+ /**
43
+ * The default channel the instance's build ships with, before any
44
+ * runtime switch; null reads the manifest's `com.hotupdater.CHANNEL`,
45
+ * then `production`. It is part of the storage key, so it must not
46
+ * change after the first launch: switch channels at runtime with
47
+ * [HotUpdaterCheckOptions.channel] and [HotUpdaterInstance.resetChannel].
48
+ * An instance of a name refuses a channel other than the one the name
49
+ * already uses.
50
+ */
51
+ val channel: String? = null,
52
+ /**
53
+ * The fingerprint hash of the fingerprint strategy; null reads the
54
+ * fingerprint the build embedded, and without one the instance uses
55
+ * the appVersion strategy's storage key.
56
+ */
57
+ val fingerprintHash: String? = null,
58
+ /**
59
+ * The PEM public key bundles must be signed with; null reads the
60
+ * manifest's `com.hotupdater.PUBLIC_KEY`, and without one signing is off.
61
+ */
62
+ val publicKey: String? = null,
63
+ /**
64
+ * The JavaScript bundle embedded in the app's assets that a named instance
65
+ * launches when it has no update. Assets of every library merge into the
66
+ * app's, so each named instance embeds its bundle under its own name.
67
+ * `main` launches `index.android.bundle` and refuses any other name.
68
+ */
69
+ val bundleAssetName: String = HotUpdaterInstanceEnvironment.DEFAULT_BUNDLE_ASSET_NAME,
70
+ )
71
+
72
+ /** The values JavaScript passes to `checkForUpdate`. */
73
+ class HotUpdaterCheckOptions(
74
+ val updateStrategy: HotUpdaterUpdateStrategy,
75
+ /**
76
+ * Checks this channel instead of the current one, as JavaScript's
77
+ * `checkForUpdate({ channel })` does: the switch is saved only once the
78
+ * update is staged, and checking another channel while the runtime channel
79
+ * is already switched is an error until [HotUpdaterInstance.resetChannel].
80
+ */
81
+ val channel: String? = null,
82
+ /** Merged over the instance's headers; these win. */
83
+ val requestHeaders: Map<String, String> = emptyMap(),
84
+ /** Each request's timeout; null uses the instance's. */
85
+ val requestTimeoutMillis: Long? = null,
86
+ )
87
+
88
+ /**
89
+ * One check's settings: the instance's, with the check's own over them, as
90
+ * JavaScript's `checkForUpdate` merges them.
91
+ */
92
+ internal class NativeCheckRequest(
93
+ val baseURL: String,
94
+ val updateStrategy: HotUpdaterUpdateStrategy,
95
+ val requestHeaders: Map<String, String> = emptyMap(),
96
+ val channel: String? = null,
97
+ val requestTimeoutMillis: Long = 5_000,
98
+ ) {
99
+ constructor(instance: HotUpdaterOptions, check: HotUpdaterCheckOptions) : this(
100
+ baseURL = instance.baseURL,
101
+ updateStrategy = check.updateStrategy,
102
+ requestHeaders =
103
+ LinkedHashMap(instance.requestHeaders).apply {
104
+ // Header names are case-insensitive: the check's spelling replaces the instance's.
105
+ check.requestHeaders.forEach { (name, value) ->
106
+ keys.removeAll { it.equals(name, ignoreCase = true) }
107
+ put(name, value)
108
+ }
109
+ },
110
+ channel = check.channel,
111
+ requestTimeoutMillis = check.requestTimeoutMillis ?: instance.requestTimeoutMillis,
112
+ )
113
+ }
114
+
115
+ /**
116
+ * The update a check found, with the fields of JavaScript's
117
+ * `CheckForUpdateResult`.
118
+ */
119
+ class HotUpdaterUpdate internal constructor(
120
+ /** [releaseId], or the bundle ID when the update names no Release. */
121
+ val id: String,
122
+ val releaseId: String?,
123
+ val status: HotUpdaterUpdateStatus,
124
+ /** A required update: the device should not keep running its current bundle. */
125
+ val shouldForceUpdate: Boolean,
126
+ val message: String?,
127
+ /** How many of the 1000 rollout cohorts receive the Release. */
128
+ val rolloutCohortCount: Int,
129
+ /** Cohorts that receive the Release whatever [rolloutCohortCount] says. */
130
+ val targetCohorts: List<String>,
131
+ /** `INSTALL`, `ADOPT_RELEASE`, or `USE_BUILTIN`. */
132
+ val transitionKind: String,
133
+ /** The update selects the bundle the app already launches, so staging it only records the selection. */
134
+ internal val runsCurrentBundle: Boolean,
135
+ /** What staging this update produces; a staging already running for it is joined. */
136
+ internal val targetKey: String,
137
+ private val install: suspend (((Double) -> Unit)?) -> Boolean,
138
+ ) {
139
+ /**
140
+ * Downloads and stages the update for the next launch, or commits a
141
+ * selection that needs no download, and joins a staging already running
142
+ * for it. Waits without a deadline; cancelling the caller ends the wait,
143
+ * not the download. Returns false when the selection was superseded.
144
+ *
145
+ * @param onProgress receives the progress as a fraction from 0 to 1,
146
+ * throttled, on any thread.
147
+ */
148
+ suspend fun updateBundle(onProgress: ((Double) -> Unit)? = null): Boolean = install(onProgress)
149
+ }
@@ -14,9 +14,19 @@ class InstallIdentityService(
14
14
  private val identityFile: File,
15
15
  ) {
16
16
  companion object {
17
- /** The install id kept in `noBackupFilesDir/hot-updater/install-identity.json`. */
17
+ private val services = mutableMapOf<String, InstallIdentityService>()
18
+
19
+ /**
20
+ * The install id kept in `noBackupFilesDir/hot-updater/install-identity.json`.
21
+ * One service per file for the whole process: every instance reads the
22
+ * same file, so instances that start at once never create different ids.
23
+ */
18
24
  fun create(context: Context): InstallIdentityService =
19
- InstallIdentityService(NoBackupStorage.file(context, InstallationIdentity.IDENTITY_FILENAME))
25
+ forFile(NoBackupStorage.file(context, InstallationIdentity.IDENTITY_FILENAME))
26
+
27
+ @Synchronized
28
+ internal fun forFile(identityFile: File): InstallIdentityService =
29
+ services.getOrPut(identityFile.absolutePath) { InstallIdentityService(identityFile) }
20
30
  }
21
31
 
22
32
  private var identity: InstallationIdentity? = null
@@ -6,28 +6,27 @@ import android.os.Build
6
6
 
7
7
  object NativeConfigUtils {
8
8
  const val CHANNEL_META_DATA_KEY = "com.hotupdater.CHANNEL"
9
- const val FINGERPRINT_HASH_META_DATA_KEY = "com.hotupdater.FINGERPRINT_HASH"
10
9
  const val PUBLIC_KEY_META_DATA_KEY = "com.hotupdater.PUBLIC_KEY"
11
10
 
12
11
  fun getString(
13
12
  context: Context,
14
13
  metaDataKey: String,
15
14
  ): String? {
16
- // Programmatic overrides take priority over manifest metadata
17
- // (brownfield/AAR setups, see HotUpdaterConfig).
15
+ // Main's options and the deprecated HotUpdater.configure take
16
+ // priority over manifest metadata (see HotUpdaterConfig).
18
17
  val override =
19
18
  when (metaDataKey) {
20
- FINGERPRINT_HASH_META_DATA_KEY -> HotUpdaterConfig.fingerprintHash
21
19
  PUBLIC_KEY_META_DATA_KEY -> HotUpdaterConfig.publicKey
22
20
  CHANNEL_META_DATA_KEY -> HotUpdaterConfig.channel
23
21
  else -> null
24
22
  }
25
23
  override?.takeIf { it.isNotEmpty() }?.let { return it }
26
24
 
27
- return getManifestMetaDataString(context, metaDataKey)
25
+ return getManifestString(context, metaDataKey)
28
26
  }
29
27
 
30
- private fun getManifestMetaDataString(
28
+ /** The merged manifest's value alone, which named instances read. */
29
+ internal fun getManifestString(
31
30
  context: Context,
32
31
  key: String,
33
32
  ): String? =
@@ -0,0 +1,172 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+
5
+ /** The native state behind the TurboModule, read and written through [HotUpdaterImpl]. */
6
+ internal class HotUpdaterImplUpdateCheckState(
7
+ private val context: Context,
8
+ private val impl: HotUpdaterImpl,
9
+ ) : UpdateCheckState {
10
+ private val cohortService by lazy { CohortService(context) }
11
+
12
+ override fun getAppVersion(): String? = HotUpdaterImpl.getAppVersion(context)
13
+
14
+ override fun getBundleId(): String? = impl.getBundleIdForUpdateCheck()
15
+
16
+ override fun getMinBundleId(): String = HotUpdaterImpl.getMinBundleId()
17
+
18
+ override fun getDefaultChannel(): String = impl.getDefaultChannel()
19
+
20
+ override fun getChannel(): String = impl.getChannel()
21
+
22
+ override fun getCohort(): String = cohortService.getCohort()
23
+
24
+ override fun getFingerprintHash(): String? = impl.getFingerprintHash()
25
+
26
+ override fun getCrashHistory(): List<String> = impl.getCrashHistory()
27
+
28
+ override fun getActiveUpdateState(): ActiveSelections {
29
+ val state = impl.getActiveUpdateState()
30
+ return ActiveSelections(
31
+ activeSelection = selectionOf(state["activeSelection"]),
32
+ stableSelection = selectionOf(state["stableSelection"]),
33
+ )
34
+ }
35
+
36
+ override fun acceptReleaseCatalog(guard: ReleaseSelectionGuard): Boolean =
37
+ impl.acceptReleaseCatalog(
38
+ catalogId = guard.catalogId,
39
+ scopeKey = guard.scopeKey,
40
+ generation = guard.generation,
41
+ catalogHash = guard.catalogHash,
42
+ channel = guard.channel,
43
+ selectionContextHash = guard.selectionContextHash,
44
+ )
45
+
46
+ override fun isReleaseSelectionCurrent(guard: ReleaseSelectionGuard): Boolean =
47
+ impl.isReleaseSelectionCurrent(
48
+ catalogId = guard.catalogId,
49
+ scopeKey = guard.scopeKey,
50
+ generation = guard.generation,
51
+ catalogHash = guard.catalogHash,
52
+ channel = guard.channel,
53
+ selectionContextHash = guard.selectionContextHash,
54
+ )
55
+
56
+ override suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = impl.commitReleaseSelection(selection)
57
+
58
+ override suspend fun updateBundle(
59
+ request: StageBundleRequest,
60
+ onProgress: (Double) -> Unit,
61
+ ): UpdateBundleResult =
62
+ impl.updateBundle(
63
+ bundleId = request.bundleId,
64
+ manifestUrl = request.manifestUrl,
65
+ manifestFileHash = request.manifestFileHash,
66
+ assets = request.assets,
67
+ channel = request.channel,
68
+ selection = request.selection,
69
+ archiveUrl = request.archiveUrl,
70
+ fileResolver =
71
+ request.filesUrl?.let {
72
+ HttpArtifactFileResolver(it, request.requestHeaders, request.requestTimeoutMillis, request.onFilesResponse)
73
+ },
74
+ progressCallback = { onProgress(it.progress) },
75
+ )
76
+
77
+ override fun getReleaseCatalogCache(partition: String): String? = impl.getReleaseCatalogCache(partition)
78
+
79
+ override fun setReleaseCatalogCache(
80
+ partition: String,
81
+ payload: String,
82
+ ): Boolean = impl.setReleaseCatalogCache(partition, payload)
83
+
84
+ override fun removeReleaseCatalogCache(partition: String): Boolean = impl.removeReleaseCatalogCache(partition)
85
+
86
+ override fun getStorageItem(key: String): String? = impl.getStorageItem(key)
87
+
88
+ override fun setStorageItem(
89
+ key: String,
90
+ value: String?,
91
+ ) {
92
+ impl.setStorageItem(key, value)
93
+ }
94
+
95
+ override fun isOffline(): Boolean = NetworkState.hasNoActiveNetwork(context)
96
+
97
+ /** A selection of `getActiveUpdateState`, whose generation is a double for JavaScript. */
98
+ private fun selectionOf(value: Any?): PersistedSelection? {
99
+ val map = value as? Map<*, *> ?: return null
100
+ return PersistedSelection(
101
+ kind = map["kind"] as? String ?: return null,
102
+ releaseId = map["releaseId"] as? String,
103
+ bundleId = map["bundleId"] as? String ?: return null,
104
+ catalogId = map["catalogId"] as? String,
105
+ scopeKey = map["scopeKey"] as? String,
106
+ generation = (map["generation"] as? Number)?.toLong(),
107
+ catalogHash = map["catalogHash"] as? String,
108
+ channel = map["channel"] as? String ?: "",
109
+ selectionContextHash = map["selectionContextHash"] as? String,
110
+ )
111
+ }
112
+ }
113
+
114
+ /**
115
+ * One native update checker per instance name, behind [HotUpdaterInstance]
116
+ * and the TurboModule's `resetChannel`, which uses the checker of its
117
+ * runtime's instance. Each checker serializes the work of its own name's state.
118
+ */
119
+ internal object NativeUpdateCheck {
120
+ private val checkers = HashMap<String, NativeUpdateChecker>()
121
+
122
+ internal fun checker(
123
+ context: Context,
124
+ name: String,
125
+ impl: () -> HotUpdaterImpl,
126
+ ): NativeUpdateChecker =
127
+ synchronized(checkers) {
128
+ checkers.getOrPut(name) {
129
+ NativeUpdateChecker(
130
+ HotUpdaterImplUpdateCheckState(context.applicationContext, impl()),
131
+ OkHttpUpdateCheckTransport(),
132
+ )
133
+ }
134
+ }
135
+
136
+ /**
137
+ * The app process checks; Hot Updater's restart process only relaunches
138
+ * it. A check there would settle the previous launch and consume the crash
139
+ * marker before the relaunched app reads them.
140
+ */
141
+ private fun requireAppProcess(
142
+ context: Context,
143
+ call: String,
144
+ ) {
145
+ check(!HotUpdaterProcess.isRestartProcess(context)) {
146
+ "$call runs in the app's process, not Hot Updater's restart process"
147
+ }
148
+ }
149
+
150
+ suspend fun checkForUpdate(
151
+ context: Context,
152
+ name: String,
153
+ impl: () -> HotUpdaterImpl,
154
+ request: NativeCheckRequest,
155
+ ): HotUpdaterUpdate? {
156
+ requireAppProcess(context, "HotUpdaterInstance.checkForUpdate")
157
+ return checker(context, name, impl).checkForUpdate(request)
158
+ }
159
+
160
+ /** Clears main's runtime channel once a check or staging running in this process finishes. */
161
+ suspend fun resetChannel(
162
+ context: Context,
163
+ impl: () -> HotUpdaterImpl,
164
+ ): Boolean = resetChannel(context, HotUpdaterInstanceEnvironment.MAIN_NAME, impl)
165
+
166
+ /** Clears [name]'s runtime channel once a check or staging of that name finishes. */
167
+ suspend fun resetChannel(
168
+ context: Context,
169
+ name: String,
170
+ impl: () -> HotUpdaterImpl,
171
+ ): Boolean = checker(context, name, impl).resetChannel { impl().resetChannel() }
172
+ }