@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
|
@@ -109,14 +109,34 @@ interface BundleStorageService {
|
|
|
109
109
|
*/
|
|
110
110
|
fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Settles what the previous process left once per process, before the
|
|
114
|
+
* first read of the selection: interrupted promotions, a crash rollback,
|
|
115
|
+
* and a launch that never finished. [prepareLaunch] runs it first; a native
|
|
116
|
+
* update check that runs before React Native starts runs it earlier.
|
|
117
|
+
* [pendingRecovery] is called only when this run settles.
|
|
118
|
+
*/
|
|
119
|
+
fun settlePreviousLaunch(pendingRecovery: () -> PendingCrashRecovery?) {}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The bundle [prepareLaunch] would load, read from the saved metadata
|
|
123
|
+
* without preparing the launch. Null is the built-in bundle. Call
|
|
124
|
+
* [settlePreviousLaunch] first, or a bundle the next launch rolls back
|
|
125
|
+
* is returned.
|
|
126
|
+
*/
|
|
127
|
+
fun peekLaunchBundleId(): String? = null
|
|
128
|
+
|
|
112
129
|
/**
|
|
113
130
|
* Updates the bundle from the specified URL
|
|
114
131
|
* @param bundleId ID of the bundle to update
|
|
115
132
|
* @param manifestUrl URL of the target bundle manifest
|
|
116
133
|
* @param manifestFileHash Manifest hash or signature
|
|
134
|
+
* @param selection the Release selection this staging commits, checked
|
|
135
|
+
* before the download and again when the staging commits
|
|
117
136
|
* @param progressCallback Callback for download progress updates
|
|
118
137
|
* @return how the staged bundle arrived
|
|
119
138
|
* @throws HotUpdaterException if the update fails
|
|
139
|
+
* @throws StaleReleaseSelectionException if [selection] is no longer current
|
|
120
140
|
*/
|
|
121
141
|
suspend fun updateBundle(
|
|
122
142
|
bundleId: String,
|
|
@@ -125,11 +145,10 @@ interface BundleStorageService {
|
|
|
125
145
|
assets: Map<String, ChangedAssetDescriptor>,
|
|
126
146
|
archiveUrl: String? = null,
|
|
127
147
|
fileResolver: ArtifactFileResolver? = null,
|
|
148
|
+
selection: PersistedSelection? = null,
|
|
128
149
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
129
150
|
): UpdateBundleResult
|
|
130
151
|
|
|
131
|
-
fun stageReleaseSelection(selection: PersistedSelection): Boolean = false
|
|
132
|
-
|
|
133
152
|
fun acceptReleaseCatalog(
|
|
134
153
|
catalogId: String,
|
|
135
154
|
scopeKey: String,
|
|
@@ -158,6 +177,13 @@ interface BundleStorageService {
|
|
|
158
177
|
*/
|
|
159
178
|
fun markLaunchStarted(launchedBundleId: String?)
|
|
160
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Undoes [markLaunchStarted] when another instance's crash ends the
|
|
182
|
+
* process: the launch did not fail, so the next process launches its
|
|
183
|
+
* bundle again instead of counting the launch as unfinished.
|
|
184
|
+
*/
|
|
185
|
+
fun clearLaunchStarted(launchedBundleId: String?) {}
|
|
186
|
+
|
|
161
187
|
/**
|
|
162
188
|
* Marks the current launch as successful after the first content appeared.
|
|
163
189
|
*/
|
|
@@ -227,6 +253,10 @@ class BundleFileStorageService(
|
|
|
227
253
|
private val preferences: PreferencesService,
|
|
228
254
|
private val isolationKey: String,
|
|
229
255
|
private val defaultChannelProvider: () -> String = { HotUpdaterImpl.getChannel(context) },
|
|
256
|
+
private val fingerprintHashProvider: () -> String? = { HotUpdaterImpl.getFingerprintHash(context) },
|
|
257
|
+
// The key bundles must be signed with, or null when signing is off.
|
|
258
|
+
private val publicKeyProvider: () -> String? = { SignatureVerifier.getPublicKeyFromConfig(context) },
|
|
259
|
+
private val fallbackBundleURL: String = "assets://index.android.bundle",
|
|
230
260
|
builtInAssetResolver: BuiltInAssetResolver? = null,
|
|
231
261
|
private val metadataWriter: (BundleMetadata, File) -> Boolean = { metadata, file ->
|
|
232
262
|
metadata.saveToFile(file)
|
|
@@ -256,7 +286,6 @@ class BundleFileStorageService(
|
|
|
256
286
|
}
|
|
257
287
|
|
|
258
288
|
private val releaseStateLock = Any()
|
|
259
|
-
private val pendingInstallSelections = java.util.concurrent.ConcurrentHashMap<String, PersistedSelection>()
|
|
260
289
|
private val updateMutex = Mutex()
|
|
261
290
|
private val builtInAssetResolver =
|
|
262
291
|
builtInAssetResolver ?: AndroidBuiltInAssetResolver(
|
|
@@ -608,6 +637,8 @@ class BundleFileStorageService(
|
|
|
608
637
|
}
|
|
609
638
|
|
|
610
639
|
private var hasPreparedLaunch = false
|
|
640
|
+
private val settleLock = Any()
|
|
641
|
+
private var hasSettledPreviousLaunch = false
|
|
611
642
|
private var currentLaunchReport: LaunchReport? = null
|
|
612
643
|
|
|
613
644
|
// The staged bundle this process launched before its first content. Only that
|
|
@@ -665,7 +696,7 @@ class BundleFileStorageService(
|
|
|
665
696
|
|
|
666
697
|
private fun resolveUpdateStrategy(): String =
|
|
667
698
|
try {
|
|
668
|
-
if (
|
|
699
|
+
if (fingerprintHashProvider().isNullOrEmpty()) {
|
|
669
700
|
"appVersion"
|
|
670
701
|
} else {
|
|
671
702
|
"fingerprint"
|
|
@@ -751,9 +782,10 @@ class BundleFileStorageService(
|
|
|
751
782
|
val packagedHash = builtInAssetResolver.packagedHashIfSourceMatches(assetPath, asset.fileHash) ?: return false
|
|
752
783
|
return HashUtils.calculateSHA256(file).equals(packagedHash, ignoreCase = true) &&
|
|
753
784
|
runCatching {
|
|
754
|
-
|
|
785
|
+
val publicKey = publicKeyProvider()
|
|
786
|
+
if (publicKey != null) {
|
|
755
787
|
val signature = asset.signature ?: throw SignatureVerificationException.InvalidSignatureFormat()
|
|
756
|
-
SignatureVerifier.verifyHashSignature(
|
|
788
|
+
SignatureVerifier.verifyHashSignature(publicKey, asset.fileHash, signature)
|
|
757
789
|
}
|
|
758
790
|
}.isSuccess
|
|
759
791
|
}
|
|
@@ -1005,15 +1037,13 @@ class BundleFileStorageService(
|
|
|
1005
1037
|
throw SignatureVerificationException.FileHashMismatch()
|
|
1006
1038
|
}
|
|
1007
1039
|
|
|
1008
|
-
|
|
1009
|
-
return
|
|
1010
|
-
}
|
|
1040
|
+
val publicKey = publicKeyProvider() ?: return
|
|
1011
1041
|
|
|
1012
1042
|
val signature =
|
|
1013
1043
|
asset.signature
|
|
1014
1044
|
?: throw SignatureVerificationException.InvalidSignatureFormat()
|
|
1015
1045
|
|
|
1016
|
-
SignatureVerifier.verifyHashSignature(
|
|
1046
|
+
SignatureVerifier.verifyHashSignature(publicKey, asset.fileHash, signature)
|
|
1017
1047
|
}
|
|
1018
1048
|
|
|
1019
1049
|
/**
|
|
@@ -1108,10 +1138,10 @@ class BundleFileStorageService(
|
|
|
1108
1138
|
private fun prepareMetadataForNewStagingBundle(
|
|
1109
1139
|
metadata: BundleMetadata,
|
|
1110
1140
|
bundleId: String,
|
|
1141
|
+
incomingSelection: PersistedSelection?,
|
|
1111
1142
|
): BundleMetadata {
|
|
1112
1143
|
val currentVerifiedBundleId =
|
|
1113
1144
|
getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
|
|
1114
|
-
val incomingSelection = pendingInstallSelections.remove(bundleId)
|
|
1115
1145
|
if (incomingSelection != null && !isSelectionCurrent(incomingSelection)) {
|
|
1116
1146
|
throw StaleReleaseSelectionException()
|
|
1117
1147
|
}
|
|
@@ -1141,6 +1171,7 @@ class BundleFileStorageService(
|
|
|
1141
1171
|
),
|
|
1142
1172
|
verificationPending = true,
|
|
1143
1173
|
launchInProgress = false,
|
|
1174
|
+
currentSelectionContexts = contextsAfterCommitting(metadata.currentSelectionContexts, incomingSelection),
|
|
1144
1175
|
updatedAt = System.currentTimeMillis(),
|
|
1145
1176
|
)
|
|
1146
1177
|
}
|
|
@@ -1259,6 +1290,22 @@ class BundleFileStorageService(
|
|
|
1259
1290
|
scopeKey: String,
|
|
1260
1291
|
): String = "$catalogId|$scopeKey"
|
|
1261
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* The selection contexts that stay current once [selection] commits: only
|
|
1295
|
+
* its own scope's, or none for a staging without a Release selection. A
|
|
1296
|
+
* check made before this commit, of another scope such as another channel,
|
|
1297
|
+
* no longer stages over it.
|
|
1298
|
+
*/
|
|
1299
|
+
private fun contextsAfterCommitting(
|
|
1300
|
+
contexts: Map<String, String>,
|
|
1301
|
+
selection: PersistedSelection?,
|
|
1302
|
+
): Map<String, String> {
|
|
1303
|
+
val catalogId = selection?.catalogId ?: return emptyMap()
|
|
1304
|
+
val scopeKey = selection.scopeKey ?: return emptyMap()
|
|
1305
|
+
val stateKey = releaseScopeStateKey(catalogId, scopeKey)
|
|
1306
|
+
return contexts.filterKeys { it == stateKey }
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1262
1309
|
private fun selectionContextValue(
|
|
1263
1310
|
channel: String,
|
|
1264
1311
|
selectionContextHash: String,
|
|
@@ -1355,12 +1402,6 @@ class BundleFileStorageService(
|
|
|
1355
1402
|
)
|
|
1356
1403
|
}
|
|
1357
1404
|
|
|
1358
|
-
override fun stageReleaseSelection(selection: PersistedSelection): Boolean {
|
|
1359
|
-
if (!isSelectionCurrent(selection)) return false
|
|
1360
|
-
pendingInstallSelections[selection.bundleId] = selection
|
|
1361
|
-
return true
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1405
|
override suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean =
|
|
1365
1406
|
withContext(Dispatchers.IO) {
|
|
1366
1407
|
synchronized(releaseStateLock) {
|
|
@@ -1374,6 +1415,15 @@ class BundleFileStorageService(
|
|
|
1374
1415
|
metadata.copy(
|
|
1375
1416
|
stagingBundleId = selection.bundleId,
|
|
1376
1417
|
stagingSelection = selection,
|
|
1418
|
+
// The launch of the staged bundle reports this Release as applied.
|
|
1419
|
+
pendingTransition =
|
|
1420
|
+
metadata.pendingTransition?.let { transition ->
|
|
1421
|
+
if (transition.toBundleId == selection.bundleId) {
|
|
1422
|
+
transition.copy(toReleaseId = selection.releaseId)
|
|
1423
|
+
} else {
|
|
1424
|
+
transition
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1377
1427
|
updatedAt = System.currentTimeMillis(),
|
|
1378
1428
|
)
|
|
1379
1429
|
}
|
|
@@ -1405,7 +1455,11 @@ class BundleFileStorageService(
|
|
|
1405
1455
|
updatedAt = System.currentTimeMillis(),
|
|
1406
1456
|
)
|
|
1407
1457
|
}
|
|
1408
|
-
|
|
1458
|
+
val committedMetadata =
|
|
1459
|
+
nextMetadata.copy(
|
|
1460
|
+
currentSelectionContexts = contextsAfterCommitting(nextMetadata.currentSelectionContexts, selection),
|
|
1461
|
+
)
|
|
1462
|
+
if (!saveMetadata(committedMetadata)) return@synchronized false
|
|
1409
1463
|
|
|
1410
1464
|
if (selection.channel == defaultChannelProvider()) {
|
|
1411
1465
|
preferences.setItem("HotUpdaterChannel", null)
|
|
@@ -1517,6 +1571,16 @@ class BundleFileStorageService(
|
|
|
1517
1571
|
}
|
|
1518
1572
|
}
|
|
1519
1573
|
|
|
1574
|
+
override fun clearLaunchStarted(launchedBundleId: String?) {
|
|
1575
|
+
synchronized(releaseStateLock) {
|
|
1576
|
+
val metadata = loadMetadataOrNull() ?: return
|
|
1577
|
+
if (!metadata.launchInProgress || metadata.stagingBundleId != launchedBundleId) {
|
|
1578
|
+
return
|
|
1579
|
+
}
|
|
1580
|
+
saveMetadata(metadata.copy(launchInProgress = false))
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1520
1584
|
override fun markLaunchCompleted(currentBundleId: String?) {
|
|
1521
1585
|
readyInterruptedLaunchRetry(currentBundleId)
|
|
1522
1586
|
val metadata = loadMetadataOrNull() ?: return
|
|
@@ -1614,19 +1678,40 @@ class BundleFileStorageService(
|
|
|
1614
1678
|
return resolvedBundle?.absolutePath ?: urlString
|
|
1615
1679
|
}
|
|
1616
1680
|
|
|
1617
|
-
override fun getFallbackBundleURL(): String =
|
|
1681
|
+
override fun getFallbackBundleURL(): String = fallbackBundleURL
|
|
1618
1682
|
|
|
1619
|
-
override fun
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1683
|
+
override fun settlePreviousLaunch(pendingRecovery: () -> PendingCrashRecovery?) {
|
|
1684
|
+
synchronized(settleLock) {
|
|
1685
|
+
if (hasSettledPreviousLaunch) return
|
|
1686
|
+
hasSettledPreviousLaunch = true
|
|
1687
|
+
recoverInterruptedPromotions()
|
|
1688
|
+
// Cleared before the rollbacks below, so the launch reports their RECOVERED.
|
|
1689
|
+
saveLaunchReport(null)
|
|
1690
|
+
applyPendingRecoveryIfNeeded(pendingRecovery())
|
|
1691
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1692
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1626
1693
|
val metadata = loadMetadataOrNull()
|
|
1627
1694
|
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1628
1695
|
metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
|
|
1629
1696
|
}
|
|
1697
|
+
// A pending bundle whose file is gone rolls back, as selectLaunch does.
|
|
1698
|
+
val settled = loadMetadataOrNull()
|
|
1699
|
+
settled?.stagingBundleId?.let { stagingBundleId ->
|
|
1700
|
+
if (settled.verificationPending && getBundleUrlForId(stagingBundleId) == null) {
|
|
1701
|
+
rollbackPendingBundle(stagingBundleId)
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
1708
|
+
if (hasPreparedLaunch) {
|
|
1709
|
+
// A later launch in this process, after a reset or a reload.
|
|
1710
|
+
recoverInterruptedPromotions()
|
|
1711
|
+
saveLaunchReport(null)
|
|
1712
|
+
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1713
|
+
} else {
|
|
1714
|
+
settlePreviousLaunch { pendingRecovery }
|
|
1630
1715
|
}
|
|
1631
1716
|
hasPreparedLaunch = true
|
|
1632
1717
|
|
|
@@ -1640,6 +1725,17 @@ class BundleFileStorageService(
|
|
|
1640
1725
|
return selection
|
|
1641
1726
|
}
|
|
1642
1727
|
|
|
1728
|
+
override fun peekLaunchBundleId(): String? {
|
|
1729
|
+
val metadata = loadMetadataOrNull() ?: return null
|
|
1730
|
+
metadata.stagingBundleId?.let { stagingBundleId ->
|
|
1731
|
+
if (getBundleUrlForId(stagingBundleId) != null) return stagingBundleId
|
|
1732
|
+
}
|
|
1733
|
+
metadata.stableBundleId?.let { stableBundleId ->
|
|
1734
|
+
if (getBundleUrlForId(stableBundleId) != null) return stableBundleId
|
|
1735
|
+
}
|
|
1736
|
+
return null
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1643
1739
|
override suspend fun updateBundle(
|
|
1644
1740
|
bundleId: String,
|
|
1645
1741
|
manifestUrl: String,
|
|
@@ -1647,9 +1743,15 @@ class BundleFileStorageService(
|
|
|
1647
1743
|
assets: Map<String, ChangedAssetDescriptor>,
|
|
1648
1744
|
archiveUrl: String?,
|
|
1649
1745
|
fileResolver: ArtifactFileResolver?,
|
|
1746
|
+
selection: PersistedSelection?,
|
|
1650
1747
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1651
1748
|
): UpdateBundleResult =
|
|
1652
1749
|
updateMutex.withLock {
|
|
1750
|
+
// The selection travels with this staging: another staging of the
|
|
1751
|
+
// same bundle carries its own.
|
|
1752
|
+
if (selection != null && !isSelectionCurrent(selection)) {
|
|
1753
|
+
throw StaleReleaseSelectionException()
|
|
1754
|
+
}
|
|
1653
1755
|
updateBundleSerialized(
|
|
1654
1756
|
bundleId = bundleId,
|
|
1655
1757
|
manifestUrl = manifestUrl,
|
|
@@ -1657,6 +1759,7 @@ class BundleFileStorageService(
|
|
|
1657
1759
|
changedAssets = assets,
|
|
1658
1760
|
archiveUrl = archiveUrl,
|
|
1659
1761
|
fileResolver = fileResolver,
|
|
1762
|
+
selection = selection,
|
|
1660
1763
|
progressCallback = progressCallback,
|
|
1661
1764
|
)
|
|
1662
1765
|
}
|
|
@@ -1668,6 +1771,7 @@ class BundleFileStorageService(
|
|
|
1668
1771
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1669
1772
|
archiveUrl: String?,
|
|
1670
1773
|
fileResolver: ArtifactFileResolver?,
|
|
1774
|
+
selection: PersistedSelection?,
|
|
1671
1775
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1672
1776
|
): UpdateBundleResult {
|
|
1673
1777
|
Log.d(
|
|
@@ -1700,6 +1804,7 @@ class BundleFileStorageService(
|
|
|
1700
1804
|
changedAssets = changedAssets,
|
|
1701
1805
|
archiveUrl = archiveUrl,
|
|
1702
1806
|
fileResolver = fileResolver,
|
|
1807
|
+
selection = selection,
|
|
1703
1808
|
bundleStoreDir = bundleStoreDir,
|
|
1704
1809
|
finalBundleDir = finalBundleDir,
|
|
1705
1810
|
progressCallback = progressCallback,
|
|
@@ -1769,9 +1874,14 @@ class BundleFileStorageService(
|
|
|
1769
1874
|
}
|
|
1770
1875
|
for (path in paths) {
|
|
1771
1876
|
val file = files[path] ?: throw HotUpdaterException.invalidBundle()
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1877
|
+
val url =
|
|
1878
|
+
try {
|
|
1879
|
+
URL(file.url)
|
|
1880
|
+
} catch (error: java.net.MalformedURLException) {
|
|
1881
|
+
throw HotUpdaterException.invalidFileUrl(path, file.url, UpdateFailureResource.FILE)
|
|
1882
|
+
}
|
|
1883
|
+
// java.net.URL also takes file: URLs and a hostless "https://"; a download needs neither.
|
|
1884
|
+
if (url.protocol.lowercase() !in setOf("http", "https") || url.host.isNullOrEmpty()) {
|
|
1775
1885
|
throw HotUpdaterException.invalidFileUrl(path, file.url, UpdateFailureResource.FILE)
|
|
1776
1886
|
}
|
|
1777
1887
|
downloads[path] = downloads.getValue(path).copy(fileUrl = file.url, fileCompression = file.compression)
|
|
@@ -2149,6 +2259,7 @@ class BundleFileStorageService(
|
|
|
2149
2259
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
2150
2260
|
archiveUrl: String?,
|
|
2151
2261
|
fileResolver: ArtifactFileResolver?,
|
|
2262
|
+
selection: PersistedSelection?,
|
|
2152
2263
|
bundleStoreDir: File,
|
|
2153
2264
|
finalBundleDir: File,
|
|
2154
2265
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
@@ -2201,7 +2312,7 @@ class BundleFileStorageService(
|
|
|
2201
2312
|
}
|
|
2202
2313
|
|
|
2203
2314
|
try {
|
|
2204
|
-
SignatureVerifier.verifyBundle(
|
|
2315
|
+
SignatureVerifier.verifyBundle(publicKeyProvider(), manifestFile, manifestFileHash)
|
|
2205
2316
|
} catch (e: SignatureVerificationException) {
|
|
2206
2317
|
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
2207
2318
|
}
|
|
@@ -2415,7 +2526,7 @@ class BundleFileStorageService(
|
|
|
2415
2526
|
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
2416
2527
|
|
|
2417
2528
|
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
2418
|
-
val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
2529
|
+
val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId, selection)
|
|
2419
2530
|
if (!setBundleURL(finalIndexFile.absolutePath)) {
|
|
2420
2531
|
throw HotUpdaterException.storageFailed("Failed to persist bundle URL")
|
|
2421
2532
|
}
|
|
@@ -2612,6 +2723,9 @@ class BundleFileStorageService(
|
|
|
2612
2723
|
pendingTransition = null,
|
|
2613
2724
|
verificationPending = false,
|
|
2614
2725
|
launchInProgress = false,
|
|
2726
|
+
// Selections made before the reset no longer stage; the
|
|
2727
|
+
// high-water marks stay, so an older catalog is still refused.
|
|
2728
|
+
currentSelectionContexts = emptyMap(),
|
|
2615
2729
|
updatedAt = System.currentTimeMillis(),
|
|
2616
2730
|
)
|
|
2617
2731
|
|
|
@@ -14,6 +14,12 @@ class CohortService(
|
|
|
14
14
|
companion object {
|
|
15
15
|
private const val COHORT_KEY = "custom_cohort"
|
|
16
16
|
private const val FALLBACK_IDENTIFIER_KEY = "fallback_identifier"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* App-wide: every instance's service reads and creates the same stored
|
|
20
|
+
* cohort, so instances that check at once never pick different ones.
|
|
21
|
+
*/
|
|
22
|
+
private val lock = Any()
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
private fun hashString(value: String): Int {
|
|
@@ -46,10 +52,12 @@ class CohortService(
|
|
|
46
52
|
return
|
|
47
53
|
}
|
|
48
54
|
// Cohort changes can be followed immediately by a process restart during OTA flows.
|
|
49
|
-
prefs.edit().putString(COHORT_KEY, cohort).commit()
|
|
55
|
+
synchronized(lock) { prefs.edit().putString(COHORT_KEY, cohort).commit() }
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
fun getCohort(): String {
|
|
58
|
+
fun getCohort(): String = synchronized(lock) { readOrCreateCohort() }
|
|
59
|
+
|
|
60
|
+
private fun readOrCreateCohort(): String {
|
|
53
61
|
val cohort = prefs.getString(COHORT_KEY, null)
|
|
54
62
|
if (!cohort.isNullOrEmpty()) {
|
|
55
63
|
return cohort
|
|
@@ -99,6 +99,8 @@ interface FileSystemService {
|
|
|
99
99
|
*/
|
|
100
100
|
class FileManagerService(
|
|
101
101
|
private val context: Context,
|
|
102
|
+
// A named instance's root beside the app's files; null is the app's files directory.
|
|
103
|
+
private val internalFilesDir: File? = null,
|
|
102
104
|
) : FileSystemService {
|
|
103
105
|
override fun fileExists(path: String): Boolean = File(path).exists()
|
|
104
106
|
|
|
@@ -145,5 +147,5 @@ class FileManagerService(
|
|
|
145
147
|
return directory.listFiles()?.map { it.name } ?: listOf()
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
override fun getInternalFilesDir(): File? = context.filesDir
|
|
150
|
+
override fun getInternalFilesDir(): File? = internalFilesDir ?: context.filesDir
|
|
149
151
|
}
|