@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.
- package/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -185
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
17
|
-
//
|
|
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
|
|
25
|
+
return getManifestString(context, metaDataKey)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
|
|
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
|
+
}
|