@hot-updater/react-native 1.0.0-rc.1 → 1.0.0-rc.15
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/android/proguard-rules.pro +17 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +651 -637
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +10 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
- package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +149 -51
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/checkForUpdate.js +32 -23
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +97 -28
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +46 -29
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +57 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +31 -14
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +56 -11
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +15 -14
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +128 -22
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +28 -21
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/commonjs/notifyAppReadyInsights.js +188 -0
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +161 -29
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +14 -7
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/wrap.js +11 -12
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +97 -25
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +32 -23
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +97 -28
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +48 -31
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +57 -5
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +32 -15
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +56 -11
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +13 -12
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +128 -22
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +23 -16
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/module/notifyAppReadyInsights.js +181 -0
- package/lib/module/notifyAppReadyInsights.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +153 -21
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +11 -4
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/wrap.js +11 -12
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +99 -25
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -3
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -6
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -7
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +5 -9
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +5 -8
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -3
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -6
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -7
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +5 -9
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +5 -8
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +137 -61
- package/src/checkForUpdate.ts +35 -29
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +70 -5
- package/src/httpClient.ts +78 -64
- package/src/index.spec.ts +59 -11
- package/src/index.ts +41 -17
- package/src/native.spec.ts +133 -22
- package/src/native.ts +21 -32
- package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +25 -17
- package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +141 -22
- package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
- package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +13 -5
- package/src/notifyAppReadyInsights.ts +293 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/wrap.spec.ts +109 -31
- package/src/wrap.tsx +11 -11
- package/src/wrap.types.ts +5 -8
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
- package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
- package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.js +0 -128
- package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/src/notifyAppReadyAnalytics.ts +0 -194
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
|
-
import android.os.StatFs
|
|
4
3
|
import android.util.Log
|
|
5
4
|
import com.hotupdater.vendor.brotli.dec.BrotliInputStream
|
|
6
5
|
import kotlinx.coroutines.Dispatchers
|
|
6
|
+
import kotlinx.coroutines.async
|
|
7
|
+
import kotlinx.coroutines.awaitAll
|
|
8
|
+
import kotlinx.coroutines.coroutineScope
|
|
9
|
+
import kotlinx.coroutines.sync.Mutex
|
|
10
|
+
import kotlinx.coroutines.sync.Semaphore
|
|
11
|
+
import kotlinx.coroutines.sync.withLock
|
|
12
|
+
import kotlinx.coroutines.sync.withPermit
|
|
7
13
|
import kotlinx.coroutines.withContext
|
|
8
14
|
import org.json.JSONObject
|
|
9
15
|
import java.io.File
|
|
@@ -25,13 +31,12 @@ data class BsdiffPatchDescriptor(
|
|
|
25
31
|
val baseFileHash: String,
|
|
26
32
|
val patchFileHash: String,
|
|
27
33
|
val patchUrl: String,
|
|
34
|
+
val byteSize: Long? = null,
|
|
28
35
|
)
|
|
29
36
|
|
|
30
37
|
data class UpdateProgressPayload(
|
|
31
38
|
val progress: Double,
|
|
32
39
|
val artifactType: String,
|
|
33
|
-
val downloadedBytes: Long? = null,
|
|
34
|
-
val totalBytes: Long? = null,
|
|
35
40
|
val details: DiffProgressDetails? = null,
|
|
36
41
|
)
|
|
37
42
|
|
|
@@ -84,18 +89,17 @@ interface BundleStorageService {
|
|
|
84
89
|
/**
|
|
85
90
|
* Updates the bundle from the specified URL
|
|
86
91
|
* @param bundleId ID of the bundle to update
|
|
87
|
-
* @param
|
|
88
|
-
* @param
|
|
92
|
+
* @param manifestUrl URL of the target bundle manifest
|
|
93
|
+
* @param manifestFileHash Manifest hash or signature
|
|
89
94
|
* @param progressCallback Callback for download progress updates
|
|
90
95
|
* @throws HotUpdaterException if the update fails
|
|
91
96
|
*/
|
|
92
97
|
suspend fun updateBundle(
|
|
93
98
|
bundleId: String,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
99
|
+
manifestUrl: String,
|
|
100
|
+
manifestFileHash: String,
|
|
101
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
102
|
+
archiveUrl: String? = null,
|
|
99
103
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
100
104
|
)
|
|
101
105
|
|
|
@@ -149,7 +153,7 @@ interface BundleStorageService {
|
|
|
149
153
|
fun getUsername(): String?
|
|
150
154
|
|
|
151
155
|
/**
|
|
152
|
-
* Persists the optional user envelope for
|
|
156
|
+
* Persists the optional user envelope for insights.
|
|
153
157
|
* Passing null clears the stored user identity.
|
|
154
158
|
*/
|
|
155
159
|
fun setUser(
|
|
@@ -183,7 +187,6 @@ interface BundleStorageService {
|
|
|
183
187
|
|
|
184
188
|
/**
|
|
185
189
|
* Gets the current active bundle ID from bundle storage.
|
|
186
|
-
* Reads manifest.json first and falls back to older metadata when needed.
|
|
187
190
|
*/
|
|
188
191
|
fun getBundleId(): String?
|
|
189
192
|
|
|
@@ -213,33 +216,31 @@ class BundleFileStorageService(
|
|
|
213
216
|
private val context: android.content.Context,
|
|
214
217
|
private val fileSystem: FileSystemService,
|
|
215
218
|
private val downloadService: DownloadService,
|
|
216
|
-
private val decompressService: DecompressService,
|
|
217
219
|
private val preferences: PreferencesService,
|
|
218
220
|
private val isolationKey: String,
|
|
219
221
|
private val defaultChannelProvider: () -> String = { HotUpdaterImpl.getChannel(context) },
|
|
222
|
+
builtInAssetResolver: BuiltInAssetResolver? = null,
|
|
223
|
+
private val metadataWriter: (BundleMetadata, File) -> Boolean = { metadata, file ->
|
|
224
|
+
metadata.saveToFile(file)
|
|
225
|
+
},
|
|
226
|
+
private val directoryRenamer: (File, File) -> Boolean = { source, destination ->
|
|
227
|
+
source.renameTo(destination)
|
|
228
|
+
},
|
|
220
229
|
) : BundleStorageService {
|
|
221
230
|
companion object {
|
|
222
231
|
private const val TAG = "BundleStorage"
|
|
232
|
+
private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991L
|
|
233
|
+
private val SHA256_PATTERN = Regex("^[0-9a-f]{64}$")
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
private val releaseStateLock = Any()
|
|
226
237
|
private val pendingInstallSelections = java.util.concurrent.ConcurrentHashMap<String, PersistedSelection>()
|
|
227
|
-
|
|
228
|
-
private
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
totalBytes: Long? = null,
|
|
233
|
-
) {
|
|
234
|
-
progressCallback(
|
|
235
|
-
UpdateProgressPayload(
|
|
236
|
-
progress = progress.coerceIn(0.0, 1.0),
|
|
237
|
-
artifactType = "archive",
|
|
238
|
-
downloadedBytes = downloadedBytes,
|
|
239
|
-
totalBytes = totalBytes,
|
|
240
|
-
),
|
|
238
|
+
private val updateMutex = Mutex()
|
|
239
|
+
private val builtInAssetResolver =
|
|
240
|
+
builtInAssetResolver ?: AndroidBuiltInAssetResolver(
|
|
241
|
+
context,
|
|
242
|
+
File(getBundleStoreDir(), "builtin-index-v1.json"),
|
|
241
243
|
)
|
|
242
|
-
}
|
|
243
244
|
|
|
244
245
|
private fun createDiffProgressFiles(changedAssets: Map<String, ChangedAssetDescriptor>): MutableList<DiffProgressFileSnapshot> =
|
|
245
246
|
changedAssets.keys
|
|
@@ -517,11 +518,20 @@ class BundleFileStorageService(
|
|
|
517
518
|
private data class ParsedBundleManifest(
|
|
518
519
|
val bundleId: String,
|
|
519
520
|
val assets: Map<String, ParsedManifestAsset>,
|
|
521
|
+
val archive: ParsedArchive?,
|
|
520
522
|
)
|
|
521
523
|
|
|
522
524
|
private data class ParsedManifestAsset(
|
|
523
525
|
val fileHash: String,
|
|
524
526
|
val signature: String?,
|
|
527
|
+
val byteSize: Long?,
|
|
528
|
+
val downloadByteSize: Long?,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
private data class ParsedArchive(
|
|
532
|
+
val downloadFileHash: String,
|
|
533
|
+
val downloadByteSize: Long,
|
|
534
|
+
val tarByteSize: Long,
|
|
525
535
|
)
|
|
526
536
|
|
|
527
537
|
private data class ActiveBundleMetadataSnapshot(
|
|
@@ -536,8 +546,11 @@ class BundleFileStorageService(
|
|
|
536
546
|
|
|
537
547
|
// Clean up old bundles if isolationKey format changed
|
|
538
548
|
checkAndCleanupIfIsolationKeyChanged()
|
|
549
|
+
|
|
550
|
+
recoverInterruptedPromotions()
|
|
539
551
|
}
|
|
540
552
|
|
|
553
|
+
private var hasPreparedLaunch = false
|
|
541
554
|
private var currentLaunchReport: LaunchReport? = null
|
|
542
555
|
|
|
543
556
|
@Volatile
|
|
@@ -569,7 +582,7 @@ class BundleFileStorageService(
|
|
|
569
582
|
|
|
570
583
|
private fun saveMetadata(metadata: BundleMetadata): Boolean {
|
|
571
584
|
val updatedMetadata = metadata.copy(isolationKey = isolationKey)
|
|
572
|
-
return updatedMetadata
|
|
585
|
+
return metadataWriter(updatedMetadata, getMetadataFile())
|
|
573
586
|
}
|
|
574
587
|
|
|
575
588
|
private fun loadLaunchReport(): LaunchReport? =
|
|
@@ -622,15 +635,7 @@ class BundleFileStorageService(
|
|
|
622
635
|
private fun getKnownLaunchBundleId(metadata: BundleMetadata): String =
|
|
623
636
|
getCurrentVerifiedBundleId(metadata) ?: HotUpdaterImpl.getMinBundleId()
|
|
624
637
|
|
|
625
|
-
private fun createInitialMetadata(): BundleMetadata
|
|
626
|
-
val currentBundleId = extractBundleIdFromCurrentURL()
|
|
627
|
-
Log.d(TAG, "Creating initial metadata with stagingBundleId: $currentBundleId")
|
|
628
|
-
return BundleMetadata(
|
|
629
|
-
stableBundleId = null,
|
|
630
|
-
stagingBundleId = currentBundleId,
|
|
631
|
-
verificationPending = false,
|
|
632
|
-
)
|
|
633
|
-
}
|
|
638
|
+
private fun createInitialMetadata(): BundleMetadata = BundleMetadata()
|
|
634
639
|
|
|
635
640
|
private fun extractBundleIdFromCurrentURL(): String? {
|
|
636
641
|
val currentUrl = preferences.getItem("HotUpdaterBundleURL") ?: return null
|
|
@@ -663,7 +668,7 @@ class BundleFileStorageService(
|
|
|
663
668
|
.singleOrNull()
|
|
664
669
|
}
|
|
665
670
|
|
|
666
|
-
return
|
|
671
|
+
return null
|
|
667
672
|
}
|
|
668
673
|
|
|
669
674
|
private fun findBundleFile(bundleId: String): File? {
|
|
@@ -671,6 +676,40 @@ class BundleFileStorageService(
|
|
|
671
676
|
return resolveBundleFile(bundleDir, bundleId)
|
|
672
677
|
}
|
|
673
678
|
|
|
679
|
+
private fun installBackupDir(bundleId: String): File = File(getBundleStoreDir(), "$bundleId.install-backup")
|
|
680
|
+
|
|
681
|
+
private fun isCompleteBundleDirectory(
|
|
682
|
+
bundleDir: File,
|
|
683
|
+
bundleId: String,
|
|
684
|
+
): Boolean {
|
|
685
|
+
val manifest = parseBundleManifestFromFile(File(bundleDir, "manifest.json")) ?: return false
|
|
686
|
+
if (manifest.bundleId != bundleId) return false
|
|
687
|
+
if (resolveBundleFile(bundleDir, bundleId) == null) return false
|
|
688
|
+
return manifest.assets.all { (assetPath, asset) ->
|
|
689
|
+
val file = RelativePathResolver.resolveInside(bundleDir, assetPath)
|
|
690
|
+
file?.isFile == true && runCatching { verifyManifestAssetFileOrThrow(file, asset) }.isSuccess
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private fun recoverInterruptedPromotions(): Boolean {
|
|
695
|
+
val bundleStoreDir = getBundleStoreDir()
|
|
696
|
+
val backups =
|
|
697
|
+
bundleStoreDir.listFiles { file ->
|
|
698
|
+
file.isDirectory && file.name.endsWith(".install-backup")
|
|
699
|
+
} ?: return true
|
|
700
|
+
|
|
701
|
+
return backups.all { backupDir ->
|
|
702
|
+
val bundleId = backupDir.name.removeSuffix(".install-backup")
|
|
703
|
+
val finalDir = File(bundleStoreDir, bundleId)
|
|
704
|
+
when {
|
|
705
|
+
!finalDir.exists() -> directoryRenamer(backupDir, finalDir)
|
|
706
|
+
isCompleteBundleDirectory(finalDir, bundleId) -> backupDir.deleteRecursively()
|
|
707
|
+
!finalDir.deleteRecursively() -> false
|
|
708
|
+
else -> directoryRenamer(backupDir, finalDir)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
674
713
|
private fun getBundleUrlForId(bundleId: String): String? = findBundleFile(bundleId)?.absolutePath
|
|
675
714
|
|
|
676
715
|
private fun getCurrentVerifiedBundleId(metadata: BundleMetadata): String? =
|
|
@@ -681,12 +720,16 @@ class BundleFileStorageService(
|
|
|
681
720
|
}
|
|
682
721
|
|
|
683
722
|
private fun getActiveBundleId(): String? {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
723
|
+
val metadata = loadMetadataOrNull() ?: return null
|
|
724
|
+
val cachedBundleId = extractBundleIdFromCurrentURL()
|
|
725
|
+
if (cachedBundleId != null &&
|
|
726
|
+
(cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId)
|
|
727
|
+
) {
|
|
728
|
+
return cachedBundleId
|
|
729
|
+
}
|
|
687
730
|
return when {
|
|
688
|
-
metadata
|
|
689
|
-
metadata
|
|
731
|
+
metadata.stagingBundleId != null && !metadata.verificationPending -> metadata.stagingBundleId
|
|
732
|
+
metadata.stableBundleId != null -> metadata.stableBundleId
|
|
690
733
|
else -> null
|
|
691
734
|
}
|
|
692
735
|
}
|
|
@@ -734,7 +777,7 @@ class BundleFileStorageService(
|
|
|
734
777
|
|
|
735
778
|
return ActiveBundleMetadataSnapshot(
|
|
736
779
|
activeBundleId = bundleDir.name,
|
|
737
|
-
bundleId = manifestBundleId
|
|
780
|
+
bundleId = manifestBundleId,
|
|
738
781
|
manifest = manifest,
|
|
739
782
|
)
|
|
740
783
|
}
|
|
@@ -752,25 +795,6 @@ class BundleFileStorageService(
|
|
|
752
795
|
return resolveActiveBundleMetadataSnapshot(bundleDir)
|
|
753
796
|
}
|
|
754
797
|
|
|
755
|
-
private fun readCompatibilityBundleIdFromBundleDir(bundleDir: File): String? {
|
|
756
|
-
val compatibilityBundleIdFile = File(bundleDir, compatibilityBundleIdFilename())
|
|
757
|
-
if (!compatibilityBundleIdFile.exists()) {
|
|
758
|
-
return null
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return try {
|
|
762
|
-
compatibilityBundleIdFile.readText().trim().takeIf { it.isNotEmpty() }
|
|
763
|
-
} catch (e: Exception) {
|
|
764
|
-
Log.w(
|
|
765
|
-
TAG,
|
|
766
|
-
"Failed to read compatibility bundle metadata from ${compatibilityBundleIdFile.absolutePath}: ${e.message}",
|
|
767
|
-
)
|
|
768
|
-
null
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
private fun compatibilityBundleIdFilename(): String = "BUNDLE_ID"
|
|
773
|
-
|
|
774
798
|
private fun readManifestFromBundleDir(bundleDir: File): Map<String, Any?>? {
|
|
775
799
|
val manifestFile = File(bundleDir, "manifest.json")
|
|
776
800
|
if (!manifestFile.exists()) {
|
|
@@ -810,6 +834,33 @@ class BundleFileStorageService(
|
|
|
810
834
|
else -> value
|
|
811
835
|
}
|
|
812
836
|
|
|
837
|
+
private fun parseSafeInteger(value: Any?): Long? {
|
|
838
|
+
val parsed =
|
|
839
|
+
when (value) {
|
|
840
|
+
is Byte -> value.toLong()
|
|
841
|
+
is Short -> value.toLong()
|
|
842
|
+
is Int -> value.toLong()
|
|
843
|
+
is Long -> value
|
|
844
|
+
is Float -> value.toDouble().takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
845
|
+
is Double -> value.takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
846
|
+
else -> null
|
|
847
|
+
} ?: return null
|
|
848
|
+
return parsed.takeIf { it in 0..MAX_SAFE_INTEGER }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private fun parseArchive(value: Any?): ParsedArchive? {
|
|
852
|
+
val archive = value as? Map<*, *> ?: return null
|
|
853
|
+
val downloadFileHash = archive["downloadFileHash"] as? String ?: return null
|
|
854
|
+
if (!SHA256_PATTERN.matches(downloadFileHash)) return null
|
|
855
|
+
val downloadByteSize = parseSafeInteger(archive["downloadByteSize"])?.takeIf { it > 0 } ?: return null
|
|
856
|
+
val tarByteSize = parseSafeInteger(archive["tarByteSize"])?.takeIf { it >= 1024 } ?: return null
|
|
857
|
+
return ParsedArchive(
|
|
858
|
+
downloadFileHash = downloadFileHash,
|
|
859
|
+
downloadByteSize = downloadByteSize,
|
|
860
|
+
tarByteSize = tarByteSize,
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
|
|
813
864
|
private fun parseBundleManifestFromMap(manifest: Map<String, Any?>): ParsedBundleManifest? {
|
|
814
865
|
val manifestBundleId =
|
|
815
866
|
(manifest["bundleId"] as? String)
|
|
@@ -835,12 +886,15 @@ class BundleFileStorageService(
|
|
|
835
886
|
ParsedManifestAsset(
|
|
836
887
|
fileHash = fileHash,
|
|
837
888
|
signature = signature?.takeIf { it.isNotBlank() },
|
|
889
|
+
byteSize = parseSafeInteger(assetMap["byteSize"]),
|
|
890
|
+
downloadByteSize = parseSafeInteger(assetMap["downloadByteSize"]),
|
|
838
891
|
)
|
|
839
892
|
}
|
|
840
893
|
|
|
841
894
|
return ParsedBundleManifest(
|
|
842
895
|
bundleId = manifestBundleId,
|
|
843
896
|
assets = assets,
|
|
897
|
+
archive = parseArchive(manifest["archive"]),
|
|
844
898
|
)
|
|
845
899
|
}
|
|
846
900
|
|
|
@@ -858,52 +912,12 @@ class BundleFileStorageService(
|
|
|
858
912
|
}
|
|
859
913
|
}
|
|
860
914
|
|
|
861
|
-
private fun writeBundleManifestFile(
|
|
862
|
-
destination: File,
|
|
863
|
-
manifest: ParsedBundleManifest,
|
|
864
|
-
) {
|
|
865
|
-
destination.parentFile?.mkdirs()
|
|
866
|
-
val assetsObject = JSONObject()
|
|
867
|
-
|
|
868
|
-
manifest.assets.toSortedMap().forEach { (assetPath, asset) ->
|
|
869
|
-
val assetObject = JSONObject().put("fileHash", asset.fileHash)
|
|
870
|
-
if (!asset.signature.isNullOrBlank()) {
|
|
871
|
-
assetObject.put("signature", asset.signature)
|
|
872
|
-
}
|
|
873
|
-
assetsObject.put(
|
|
874
|
-
assetPath,
|
|
875
|
-
assetObject,
|
|
876
|
-
)
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
val manifestObject =
|
|
880
|
-
JSONObject()
|
|
881
|
-
.put("bundleId", manifest.bundleId)
|
|
882
|
-
.put("assets", assetsObject)
|
|
883
|
-
|
|
884
|
-
destination.writeText("${manifestObject}\n")
|
|
885
|
-
}
|
|
886
|
-
|
|
887
915
|
private fun getActiveBundleDir(): File? {
|
|
888
916
|
val activeBundleId = getActiveBundleId() ?: return null
|
|
889
917
|
val bundleDir = File(getBundleStoreDir(), activeBundleId)
|
|
890
918
|
return bundleDir.takeIf { it.exists() }
|
|
891
919
|
}
|
|
892
920
|
|
|
893
|
-
private fun canUseManifestDrivenInstall(): Boolean {
|
|
894
|
-
val activeBundleDir = getActiveBundleDir() ?: return false
|
|
895
|
-
if (!activeBundleDir.exists()) {
|
|
896
|
-
return false
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
val currentManifest =
|
|
900
|
-
getActiveBundleMetadataSnapshot()
|
|
901
|
-
?.manifest
|
|
902
|
-
?.let(::parseBundleManifestFromMap) ?: return false
|
|
903
|
-
|
|
904
|
-
return currentManifest.assets.isNotEmpty()
|
|
905
|
-
}
|
|
906
|
-
|
|
907
921
|
private fun copyBundleFile(
|
|
908
922
|
source: File,
|
|
909
923
|
destination: File,
|
|
@@ -1046,6 +1060,7 @@ class BundleFileStorageService(
|
|
|
1046
1060
|
toBundleId = stagingSelection.bundleId,
|
|
1047
1061
|
),
|
|
1048
1062
|
verificationPending = true,
|
|
1063
|
+
launchInProgress = false,
|
|
1049
1064
|
updatedAt = System.currentTimeMillis(),
|
|
1050
1065
|
)
|
|
1051
1066
|
}
|
|
@@ -1076,6 +1091,7 @@ class BundleFileStorageService(
|
|
|
1076
1091
|
pendingUpdateStrategy = null,
|
|
1077
1092
|
pendingTransition = null,
|
|
1078
1093
|
verificationPending = false,
|
|
1094
|
+
launchInProgress = false,
|
|
1079
1095
|
updatedAt = System.currentTimeMillis(),
|
|
1080
1096
|
)
|
|
1081
1097
|
|
|
@@ -1113,10 +1129,9 @@ class BundleFileStorageService(
|
|
|
1113
1129
|
private fun selectLaunch(): LaunchSelection {
|
|
1114
1130
|
val metadata = loadMetadataOrNull()
|
|
1115
1131
|
if (metadata == null) {
|
|
1116
|
-
val cached = getCachedBundleURL()
|
|
1117
1132
|
return LaunchSelection(
|
|
1118
|
-
bundleUrl =
|
|
1119
|
-
launchedBundleId =
|
|
1133
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1134
|
+
launchedBundleId = null,
|
|
1120
1135
|
shouldRollbackOnCrash = false,
|
|
1121
1136
|
)
|
|
1122
1137
|
}
|
|
@@ -1147,10 +1162,9 @@ class BundleFileStorageService(
|
|
|
1147
1162
|
}
|
|
1148
1163
|
}
|
|
1149
1164
|
|
|
1150
|
-
val cached = getCachedBundleURL()
|
|
1151
1165
|
return LaunchSelection(
|
|
1152
|
-
bundleUrl =
|
|
1153
|
-
launchedBundleId =
|
|
1166
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1167
|
+
launchedBundleId = null,
|
|
1154
1168
|
shouldRollbackOnCrash = false,
|
|
1155
1169
|
)
|
|
1156
1170
|
}
|
|
@@ -1302,6 +1316,7 @@ class BundleFileStorageService(
|
|
|
1302
1316
|
pendingUpdateStrategy = null,
|
|
1303
1317
|
pendingTransition = null,
|
|
1304
1318
|
verificationPending = false,
|
|
1319
|
+
launchInProgress = false,
|
|
1305
1320
|
updatedAt = System.currentTimeMillis(),
|
|
1306
1321
|
)
|
|
1307
1322
|
}
|
|
@@ -1354,6 +1369,7 @@ class BundleFileStorageService(
|
|
|
1354
1369
|
pendingUpdateStrategy = null,
|
|
1355
1370
|
pendingTransition = null,
|
|
1356
1371
|
verificationPending = false,
|
|
1372
|
+
launchInProgress = false,
|
|
1357
1373
|
updatedAt = System.currentTimeMillis(),
|
|
1358
1374
|
),
|
|
1359
1375
|
)
|
|
@@ -1413,9 +1429,14 @@ class BundleFileStorageService(
|
|
|
1413
1429
|
|
|
1414
1430
|
override fun setBundleURL(localPath: String?): Boolean {
|
|
1415
1431
|
Log.d(TAG, "setBundleURL: $localPath")
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1432
|
+
return try {
|
|
1433
|
+
preferences.setItem("HotUpdaterBundleURL", localPath)
|
|
1434
|
+
clearActiveBundleMetadataSnapshot()
|
|
1435
|
+
true
|
|
1436
|
+
} catch (e: Exception) {
|
|
1437
|
+
Log.e(TAG, "Failed to persist bundle URL", e)
|
|
1438
|
+
false
|
|
1439
|
+
}
|
|
1419
1440
|
}
|
|
1420
1441
|
|
|
1421
1442
|
override fun getCachedBundleURL(): String? {
|
|
@@ -1446,10 +1467,25 @@ class BundleFileStorageService(
|
|
|
1446
1467
|
override fun getFallbackBundleURL(): String = "assets://index.android.bundle"
|
|
1447
1468
|
|
|
1448
1469
|
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
1470
|
+
recoverInterruptedPromotions()
|
|
1449
1471
|
saveLaunchReport(null)
|
|
1450
1472
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1473
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1474
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1475
|
+
if (!hasPreparedLaunch) {
|
|
1476
|
+
val metadata = loadMetadataOrNull()
|
|
1477
|
+
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1478
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it) }
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
hasPreparedLaunch = true
|
|
1451
1482
|
|
|
1452
1483
|
val selection = selectLaunch()
|
|
1484
|
+
if (selection.shouldRollbackOnCrash) {
|
|
1485
|
+
loadMetadataOrNull()?.let { metadata ->
|
|
1486
|
+
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1453
1489
|
Log.d(
|
|
1454
1490
|
TAG,
|
|
1455
1491
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -1459,49 +1495,34 @@ class BundleFileStorageService(
|
|
|
1459
1495
|
|
|
1460
1496
|
override suspend fun updateBundle(
|
|
1461
1497
|
bundleId: String,
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1498
|
+
manifestUrl: String,
|
|
1499
|
+
manifestFileHash: String,
|
|
1500
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
1501
|
+
archiveUrl: String?,
|
|
1502
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1503
|
+
) = updateMutex.withLock {
|
|
1504
|
+
updateBundleSerialized(
|
|
1505
|
+
bundleId = bundleId,
|
|
1506
|
+
manifestUrl = manifestUrl,
|
|
1507
|
+
manifestFileHash = manifestFileHash,
|
|
1508
|
+
changedAssets = assets,
|
|
1509
|
+
archiveUrl = archiveUrl,
|
|
1510
|
+
progressCallback = progressCallback,
|
|
1511
|
+
)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private suspend fun updateBundleSerialized(
|
|
1515
|
+
bundleId: String,
|
|
1516
|
+
manifestUrl: String,
|
|
1517
|
+
manifestFileHash: String,
|
|
1518
|
+
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1519
|
+
archiveUrl: String?,
|
|
1467
1520
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1468
1521
|
) {
|
|
1469
1522
|
Log.d(
|
|
1470
1523
|
TAG,
|
|
1471
|
-
"updateBundle bundleId $bundleId
|
|
1524
|
+
"updateBundle bundleId $bundleId manifestUrl $manifestUrl",
|
|
1472
1525
|
)
|
|
1473
|
-
// If no URL is provided, reset to fallback and clean up all bundles
|
|
1474
|
-
if (fileUrl.isNullOrEmpty()) {
|
|
1475
|
-
Log.d(TAG, "fileUrl is null or empty, resetting to fallback bundle")
|
|
1476
|
-
|
|
1477
|
-
withContext(Dispatchers.IO) {
|
|
1478
|
-
// 1. Set bundle URL to null (reset preference)
|
|
1479
|
-
val setResult = setBundleURL(null)
|
|
1480
|
-
if (!setResult) {
|
|
1481
|
-
Log.w(TAG, "Failed to reset bundle URL")
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// 2. Reset metadata to initial state (clear all bundle references)
|
|
1485
|
-
val previousMetadata = loadMetadataOrNull()
|
|
1486
|
-
val metadata =
|
|
1487
|
-
createInitialMetadata().copy(
|
|
1488
|
-
highestSeenCatalogs = previousMetadata?.highestSeenCatalogs ?: emptyMap(),
|
|
1489
|
-
currentSelectionContexts = previousMetadata?.currentSelectionContexts ?: emptyMap(),
|
|
1490
|
-
)
|
|
1491
|
-
val saveResult = saveMetadata(metadata)
|
|
1492
|
-
if (!saveResult) {
|
|
1493
|
-
Log.w(TAG, "Failed to reset metadata")
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// 3. Clean up all downloaded bundles
|
|
1497
|
-
// Pass null for currentBundleId to remove all bundles except the new bundleId
|
|
1498
|
-
val bundleStoreDir = getBundleStoreDir()
|
|
1499
|
-
cleanupOldBundles(bundleStoreDir, null, bundleId)
|
|
1500
|
-
|
|
1501
|
-
Log.d(TAG, "Successfully reset to fallback bundle and cleaned up downloads")
|
|
1502
|
-
}
|
|
1503
|
-
return
|
|
1504
|
-
}
|
|
1505
1526
|
|
|
1506
1527
|
// Check if bundle is in crashed history
|
|
1507
1528
|
if (isBundleInCrashedHistory(bundleId)) {
|
|
@@ -1509,299 +1530,373 @@ class BundleFileStorageService(
|
|
|
1509
1530
|
throw HotUpdaterException.bundleInCrashedHistory(bundleId)
|
|
1510
1531
|
}
|
|
1511
1532
|
|
|
1512
|
-
// Initialize metadata if it doesn't exist (lazy initialization)
|
|
1513
|
-
val existingMetadata = loadMetadataOrNull()
|
|
1514
|
-
val metadata =
|
|
1515
|
-
existingMetadata ?: createInitialMetadata().also {
|
|
1516
|
-
saveMetadata(it)
|
|
1517
|
-
Log.d(TAG, "Created initial metadata during updateBundle")
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
val baseDir = fileSystem.getInternalFilesDir()
|
|
1521
1533
|
val bundleStoreDir = getBundleStoreDir()
|
|
1522
1534
|
if (!bundleStoreDir.exists()) {
|
|
1523
1535
|
bundleStoreDir.mkdirs()
|
|
1524
1536
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (finalBundleDir.exists()) {
|
|
1528
|
-
Log.d(TAG, "Bundle for bundleId $bundleId already exists. Using cached bundle.")
|
|
1529
|
-
val existingBundleFile = resolveBundleFile(finalBundleDir, bundleId)
|
|
1530
|
-
if (existingBundleFile != null) {
|
|
1531
|
-
// Update last modified time
|
|
1532
|
-
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
1533
|
-
|
|
1534
|
-
// Update metadata: set as staging
|
|
1535
|
-
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
1536
|
-
val updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
1537
|
-
saveMetadata(updatedMetadata)
|
|
1538
|
-
|
|
1539
|
-
// Set bundle URL for backwards compatibility
|
|
1540
|
-
setBundleURL(existingBundleFile.absolutePath)
|
|
1541
|
-
|
|
1542
|
-
// Keep the current verified bundle as a fallback if one exists.
|
|
1543
|
-
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
1544
|
-
|
|
1545
|
-
Log.d(TAG, "Existing bundle set as staging bundle for next launch")
|
|
1546
|
-
return
|
|
1547
|
-
} else {
|
|
1548
|
-
// If index.android.bundle is missing, delete and re-download
|
|
1549
|
-
finalBundleDir.deleteRecursively()
|
|
1550
|
-
}
|
|
1537
|
+
if (!recoverInterruptedPromotions()) {
|
|
1538
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
1551
1539
|
}
|
|
1552
1540
|
|
|
1553
|
-
val
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
manifestFileHash = manifestFileHash!!,
|
|
1565
|
-
changedAssets = changedAssets!!,
|
|
1566
|
-
bundleStoreDir = bundleStoreDir,
|
|
1567
|
-
finalBundleDir = finalBundleDir,
|
|
1568
|
-
progressCallback = progressCallback,
|
|
1569
|
-
)
|
|
1570
|
-
}
|
|
1571
|
-
return
|
|
1572
|
-
} catch (e: Exception) {
|
|
1573
|
-
if (fileUrl.isNullOrEmpty()) {
|
|
1574
|
-
throw e
|
|
1575
|
-
}
|
|
1576
|
-
Log.w(
|
|
1577
|
-
TAG,
|
|
1578
|
-
"Manifest-driven install failed for $bundleId. Falling back to archive: ${e.message}",
|
|
1579
|
-
e,
|
|
1580
|
-
)
|
|
1581
|
-
}
|
|
1582
|
-
} else if (hasManifestDrivenArtifacts) {
|
|
1583
|
-
Log.d(
|
|
1584
|
-
TAG,
|
|
1585
|
-
"Skipping manifest-driven install for $bundleId because no active OTA manifest is available. Using archive.",
|
|
1541
|
+
val finalBundleDir = File(bundleStoreDir, bundleId)
|
|
1542
|
+
withContext(Dispatchers.IO) {
|
|
1543
|
+
updateBundleFromManifest(
|
|
1544
|
+
bundleId = bundleId,
|
|
1545
|
+
manifestUrl = manifestUrl,
|
|
1546
|
+
manifestFileHash = manifestFileHash,
|
|
1547
|
+
changedAssets = changedAssets,
|
|
1548
|
+
archiveUrl = archiveUrl,
|
|
1549
|
+
bundleStoreDir = bundleStoreDir,
|
|
1550
|
+
finalBundleDir = finalBundleDir,
|
|
1551
|
+
progressCallback = progressCallback,
|
|
1586
1552
|
)
|
|
1587
1553
|
}
|
|
1554
|
+
}
|
|
1588
1555
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1556
|
+
private suspend fun downloadManifestAsset(
|
|
1557
|
+
assetPath: String,
|
|
1558
|
+
expectedAsset: ParsedManifestAsset,
|
|
1559
|
+
changedAsset: ChangedAssetDescriptor,
|
|
1560
|
+
currentBundleId: String?,
|
|
1561
|
+
activeBundleDir: File?,
|
|
1562
|
+
targetFile: File,
|
|
1563
|
+
tempDir: File,
|
|
1564
|
+
progressFile: DiffProgressFileSnapshot,
|
|
1565
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1566
|
+
) {
|
|
1567
|
+
val expectedHash = expectedAsset.fileHash
|
|
1568
|
+
val diffFiles = mutableListOf(progressFile)
|
|
1569
|
+
val patched =
|
|
1570
|
+
applyPatchAssetIfPossible(
|
|
1571
|
+
assetPath = assetPath,
|
|
1572
|
+
changedAsset = changedAsset,
|
|
1573
|
+
currentBundleId = currentBundleId,
|
|
1574
|
+
activeBundleDir = activeBundleDir,
|
|
1575
|
+
targetFile = targetFile,
|
|
1576
|
+
expectedHash = expectedHash,
|
|
1577
|
+
tempDir = tempDir,
|
|
1578
|
+
diffFiles = diffFiles,
|
|
1579
|
+
progressCallback = progressCallback,
|
|
1580
|
+
)
|
|
1581
|
+
if (patched) {
|
|
1582
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1583
|
+
updateDiffProgressFile(
|
|
1584
|
+
files = diffFiles,
|
|
1585
|
+
assetPath = assetPath,
|
|
1586
|
+
status = "downloaded",
|
|
1587
|
+
progress = 1.0,
|
|
1588
|
+
)
|
|
1589
|
+
emitDiffProgress(
|
|
1590
|
+
progressCallback = progressCallback,
|
|
1591
|
+
phase =
|
|
1592
|
+
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1593
|
+
"finalizing"
|
|
1594
|
+
} else {
|
|
1595
|
+
"downloading"
|
|
1596
|
+
},
|
|
1597
|
+
files = diffFiles,
|
|
1598
|
+
)
|
|
1599
|
+
return
|
|
1593
1600
|
}
|
|
1594
|
-
tempDir.mkdirs()
|
|
1595
|
-
|
|
1596
|
-
withContext(Dispatchers.IO) {
|
|
1597
|
-
val downloadUrl = URL(fileUrl)
|
|
1598
1601
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1602
|
+
val changedAssetFileUrl =
|
|
1603
|
+
changedAsset.fileUrl
|
|
1604
|
+
?: run {
|
|
1605
|
+
updateDiffProgressFile(
|
|
1606
|
+
files = diffFiles,
|
|
1607
|
+
assetPath = assetPath,
|
|
1608
|
+
status = "failed",
|
|
1609
|
+
progress = 0.0,
|
|
1610
|
+
)
|
|
1611
|
+
emitDiffProgress(
|
|
1612
|
+
progressCallback = progressCallback,
|
|
1613
|
+
phase = "downloading",
|
|
1614
|
+
files = diffFiles,
|
|
1615
|
+
)
|
|
1616
|
+
throw HotUpdaterException.downloadFailed(
|
|
1617
|
+
IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
|
|
1618
|
+
)
|
|
1605
1619
|
}
|
|
1606
|
-
val tempBundleFile = File(tempDir, bundleFileName)
|
|
1607
1620
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1621
|
+
val downloadPath = downloadPathForChangedAsset(assetPath, changedAsset)
|
|
1622
|
+
val downloadedAssetFile =
|
|
1623
|
+
if (changedAsset.fileCompression == "br") {
|
|
1624
|
+
compressedTempFile(tempDir, assetPath, "br")
|
|
1625
|
+
} else {
|
|
1626
|
+
targetFile
|
|
1627
|
+
}
|
|
1611
1628
|
|
|
1612
|
-
|
|
1629
|
+
when (
|
|
1630
|
+
val assetDownloadResult =
|
|
1613
1631
|
downloadService.downloadFile(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
fileSizeCallback = { fileSize ->
|
|
1617
|
-
// Perform disk space check when file size is known
|
|
1618
|
-
if (baseDir != null) {
|
|
1619
|
-
val stat = StatFs(baseDir.absolutePath)
|
|
1620
|
-
val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
|
|
1621
|
-
val requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
1622
|
-
|
|
1623
|
-
Log.d(
|
|
1624
|
-
"BundleStorage",
|
|
1625
|
-
"File size: $fileSize bytes, Available: $availableBytes bytes, Required: $requiredSpace bytes",
|
|
1626
|
-
)
|
|
1627
|
-
|
|
1628
|
-
if (availableBytes < requiredSpace) {
|
|
1629
|
-
Log.d(
|
|
1630
|
-
TAG,
|
|
1631
|
-
"Insufficient disk space detected: need $requiredSpace bytes, available $availableBytes bytes",
|
|
1632
|
-
)
|
|
1633
|
-
// Store error to be thrown after download completes/cancels
|
|
1634
|
-
diskSpaceError = HotUpdaterException.insufficientDiskSpace(requiredSpace, availableBytes)
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
},
|
|
1632
|
+
URL(changedAssetFileUrl),
|
|
1633
|
+
downloadedAssetFile,
|
|
1638
1634
|
) { downloadProgress ->
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
downloadProgress.
|
|
1644
|
-
|
|
1635
|
+
updateDiffProgressFile(
|
|
1636
|
+
files = diffFiles,
|
|
1637
|
+
assetPath = assetPath,
|
|
1638
|
+
status = "downloading",
|
|
1639
|
+
progress = downloadProgress.progress,
|
|
1640
|
+
downloadPath = downloadPath,
|
|
1641
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
1642
|
+
totalBytes = downloadProgress.totalBytes,
|
|
1643
|
+
)
|
|
1644
|
+
emitDiffProgress(
|
|
1645
|
+
progressCallback = progressCallback,
|
|
1646
|
+
phase = "downloading",
|
|
1647
|
+
files = diffFiles,
|
|
1645
1648
|
)
|
|
1646
1649
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1650
|
+
) {
|
|
1651
|
+
is DownloadResult.Error -> {
|
|
1652
|
+
updateDiffProgressFile(
|
|
1653
|
+
files = diffFiles,
|
|
1654
|
+
assetPath = assetPath,
|
|
1655
|
+
status = "failed",
|
|
1656
|
+
progress =
|
|
1657
|
+
diffFiles
|
|
1658
|
+
.firstOrNull { it.path == assetPath }
|
|
1659
|
+
?.progress ?: 0.0,
|
|
1660
|
+
)
|
|
1661
|
+
emitDiffProgress(
|
|
1662
|
+
progressCallback = progressCallback,
|
|
1663
|
+
phase = "downloading",
|
|
1664
|
+
files = diffFiles,
|
|
1665
|
+
)
|
|
1666
|
+
if (assetDownloadResult.exception is IncompleteDownloadException) {
|
|
1667
|
+
val incompleteEx =
|
|
1668
|
+
assetDownloadResult.exception as IncompleteDownloadException
|
|
1669
|
+
throw HotUpdaterException.incompleteDownload(
|
|
1670
|
+
incompleteEx.expectedSize,
|
|
1671
|
+
incompleteEx.actualSize,
|
|
1672
|
+
)
|
|
1673
|
+
}
|
|
1674
|
+
throw HotUpdaterException.downloadFailed(assetDownloadResult.exception)
|
|
1653
1675
|
}
|
|
1654
1676
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (downloadResult.exception is IncompleteDownloadException) {
|
|
1662
|
-
val incompleteEx = downloadResult.exception as IncompleteDownloadException
|
|
1663
|
-
throw HotUpdaterException.incompleteDownload(
|
|
1664
|
-
incompleteEx.expectedSize,
|
|
1665
|
-
incompleteEx.actualSize,
|
|
1677
|
+
is DownloadResult.Success -> {
|
|
1678
|
+
try {
|
|
1679
|
+
if (changedAsset.fileCompression == "br") {
|
|
1680
|
+
decompressBrotliFile(
|
|
1681
|
+
assetDownloadResult.file,
|
|
1682
|
+
targetFile,
|
|
1666
1683
|
)
|
|
1667
|
-
} else {
|
|
1668
|
-
throw HotUpdaterException.downloadFailed(downloadResult.exception)
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
is DownloadResult.Success -> {
|
|
1673
|
-
Log.d("BundleStorage", "Download successful")
|
|
1674
|
-
// 1) Verify bundle integrity (hash or signature based on fileHash format)
|
|
1675
|
-
Log.d("BundleStorage", "Verifying bundle integrity...")
|
|
1676
|
-
try {
|
|
1677
|
-
SignatureVerifier.verifyBundle(context, tempBundleFile, fileHash)
|
|
1678
|
-
Log.d("BundleStorage", "Bundle verification completed successfully")
|
|
1679
|
-
} catch (e: SignatureVerificationException) {
|
|
1680
|
-
Log.e("BundleStorage", "Bundle verification failed", e)
|
|
1681
|
-
tempDir.deleteRecursively()
|
|
1682
|
-
tempBundleFile.delete()
|
|
1683
|
-
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// 2) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
|
|
1687
|
-
val tmpDir = File(bundleStoreDir, "$bundleId.tmp")
|
|
1688
|
-
if (tmpDir.exists()) {
|
|
1689
|
-
tmpDir.deleteRecursively()
|
|
1690
1684
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
throw HotUpdaterException.extractionFormatError()
|
|
1685
|
+
verifyManifestAssetFileOrThrow(
|
|
1686
|
+
targetFile,
|
|
1687
|
+
expectedAsset,
|
|
1688
|
+
)
|
|
1689
|
+
} catch (e: Exception) {
|
|
1690
|
+
updateDiffProgressFile(
|
|
1691
|
+
files = diffFiles,
|
|
1692
|
+
assetPath = assetPath,
|
|
1693
|
+
status = "failed",
|
|
1694
|
+
progress = 1.0,
|
|
1695
|
+
)
|
|
1696
|
+
emitDiffProgress(
|
|
1697
|
+
progressCallback = progressCallback,
|
|
1698
|
+
phase = "downloading",
|
|
1699
|
+
files = diffFiles,
|
|
1700
|
+
)
|
|
1701
|
+
if (e is HotUpdaterException) {
|
|
1702
|
+
throw e
|
|
1710
1703
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
Log.d("BundleStorage", "Android bundle file could not be resolved in tmpDir.")
|
|
1716
|
-
tempDir.deleteRecursively()
|
|
1717
|
-
tmpDir.deleteRecursively()
|
|
1718
|
-
throw HotUpdaterException.invalidBundle()
|
|
1704
|
+
throw HotUpdaterException.downloadFailed(e)
|
|
1705
|
+
} finally {
|
|
1706
|
+
if (changedAsset.fileCompression == "br") {
|
|
1707
|
+
assetDownloadResult.file.delete()
|
|
1719
1708
|
}
|
|
1709
|
+
}
|
|
1710
|
+
updateDiffProgressFile(
|
|
1711
|
+
files = diffFiles,
|
|
1712
|
+
assetPath = assetPath,
|
|
1713
|
+
status = "downloaded",
|
|
1714
|
+
progress = 1.0,
|
|
1715
|
+
)
|
|
1716
|
+
emitDiffProgress(
|
|
1717
|
+
progressCallback = progressCallback,
|
|
1718
|
+
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
1719
|
+
files = diffFiles,
|
|
1720
|
+
)
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1720
1724
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1725
|
+
private fun pruneStagingDirectory(
|
|
1726
|
+
directory: File,
|
|
1727
|
+
assets: Set<String>,
|
|
1728
|
+
prefix: String = "",
|
|
1729
|
+
) {
|
|
1730
|
+
directory.listFiles()?.forEach { file ->
|
|
1731
|
+
val relativePath = prefix + file.name
|
|
1732
|
+
if (file.canonicalFile != File(directory.canonicalFile, file.name)) {
|
|
1733
|
+
check(file.delete()) { "Cannot remove staging link: $relativePath" }
|
|
1734
|
+
} else if (file.isDirectory && assets.any { it.startsWith("$relativePath/") }) {
|
|
1735
|
+
pruneStagingDirectory(file, assets, "$relativePath/")
|
|
1736
|
+
} else if (!file.isFile || relativePath !in assets) {
|
|
1737
|
+
check(file.deleteRecursively()) { "Cannot remove stale staging file: $relativePath" }
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1724
1741
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1742
|
+
private fun selectArchive(
|
|
1743
|
+
manifest: ParsedBundleManifest,
|
|
1744
|
+
downloads: Map<String, ChangedAssetDescriptor>,
|
|
1745
|
+
archiveUrl: String?,
|
|
1746
|
+
): ParsedArchive? {
|
|
1747
|
+
if (archiveUrl.isNullOrBlank() || downloads.size < 2) return null
|
|
1748
|
+
val archive = manifest.archive ?: return null
|
|
1749
|
+
if (manifest.assets.values.any { it.byteSize == null }) return null
|
|
1750
|
+
|
|
1751
|
+
var individualBytes = 0L
|
|
1752
|
+
for ((assetPath, descriptor) in downloads) {
|
|
1753
|
+
val originalBytes = manifest.assets.getValue(assetPath).downloadByteSize ?: return null
|
|
1754
|
+
val expectedBytes =
|
|
1755
|
+
descriptor.patch?.let { patch ->
|
|
1756
|
+
val patchBytes = patch.byteSize?.takeIf { it in 0..MAX_SAFE_INTEGER } ?: return null
|
|
1757
|
+
minOf(originalBytes, patchBytes)
|
|
1758
|
+
} ?: originalBytes
|
|
1759
|
+
if (individualBytes > MAX_SAFE_INTEGER - expectedBytes) return null
|
|
1760
|
+
individualBytes += expectedBytes
|
|
1761
|
+
}
|
|
1762
|
+
var logicalBytes = 0L
|
|
1763
|
+
for (asset in manifest.assets.values) {
|
|
1764
|
+
val byteSize = asset.byteSize ?: return null
|
|
1765
|
+
if (logicalBytes > MAX_SAFE_INTEGER - byteSize) return null
|
|
1766
|
+
logicalBytes += byteSize
|
|
1767
|
+
}
|
|
1768
|
+
if (archive.tarByteSize < logicalBytes) return null
|
|
1769
|
+
val tarFramingBytes = archive.tarByteSize - logicalBytes
|
|
1770
|
+
if (individualBytes > MAX_SAFE_INTEGER - tarFramingBytes) return null
|
|
1771
|
+
val framedByteLimit = individualBytes + tarFramingBytes
|
|
1772
|
+
|
|
1773
|
+
if (archive.downloadByteSize <= individualBytes) return archive
|
|
1774
|
+
if (downloads.size != manifest.assets.size || downloads.values.any { it.patch != null }) return null
|
|
1775
|
+
return archive.takeIf { it.downloadByteSize <= framedByteLimit }
|
|
1776
|
+
}
|
|
1729
1777
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1778
|
+
private suspend fun tryInstallArchive(
|
|
1779
|
+
archiveUrl: String,
|
|
1780
|
+
archive: ParsedArchive,
|
|
1781
|
+
manifest: ParsedBundleManifest,
|
|
1782
|
+
tempDir: File,
|
|
1783
|
+
bundleStoreDir: File,
|
|
1784
|
+
tmpDir: File,
|
|
1785
|
+
bundleId: String,
|
|
1786
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1787
|
+
): MutableList<DiffProgressFileSnapshot>? {
|
|
1788
|
+
val archiveScratch = File(tempDir, "archive")
|
|
1789
|
+
val archiveFile = File(archiveScratch, "bundle.tar.br")
|
|
1790
|
+
val extractedDir = File(bundleStoreDir, "$bundleId.archive.tmp")
|
|
1791
|
+
val localBackupDir = File(bundleStoreDir, "$bundleId.local.tmp")
|
|
1792
|
+
archiveScratch.deleteRecursively()
|
|
1793
|
+
extractedDir.deleteRecursively()
|
|
1794
|
+
localBackupDir.deleteRecursively()
|
|
1795
|
+
archiveScratch.mkdirs()
|
|
1796
|
+
|
|
1797
|
+
val progressFiles =
|
|
1798
|
+
mutableListOf(
|
|
1799
|
+
DiffProgressFileSnapshot(
|
|
1800
|
+
path = "bundle.tar.br",
|
|
1801
|
+
downloadPath = "bundle.tar.br",
|
|
1802
|
+
status = "pending",
|
|
1803
|
+
progress = 0.0,
|
|
1804
|
+
order = 0,
|
|
1805
|
+
),
|
|
1806
|
+
)
|
|
1807
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1750
1808
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1809
|
+
return try {
|
|
1810
|
+
when (
|
|
1811
|
+
val result =
|
|
1812
|
+
downloadService.downloadFileOnce(
|
|
1813
|
+
URL(archiveUrl),
|
|
1814
|
+
archiveFile,
|
|
1815
|
+
) { progress ->
|
|
1816
|
+
updateDiffProgressFile(
|
|
1817
|
+
files = progressFiles,
|
|
1818
|
+
assetPath = "bundle.tar.br",
|
|
1819
|
+
status = "downloading",
|
|
1820
|
+
progress = progress.progress,
|
|
1821
|
+
downloadPath = "bundle.tar.br",
|
|
1822
|
+
downloadedBytes = progress.downloadedBytes,
|
|
1823
|
+
totalBytes = progress.totalBytes,
|
|
1824
|
+
)
|
|
1825
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1758
1826
|
}
|
|
1827
|
+
) {
|
|
1828
|
+
is DownloadResult.Error -> throw result.exception
|
|
1829
|
+
is DownloadResult.Success -> Unit
|
|
1830
|
+
}
|
|
1759
1831
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
// Update metadata: set new bundle as staging
|
|
1768
|
-
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
1769
|
-
val updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
1770
|
-
saveMetadata(updatedMetadata)
|
|
1771
|
-
|
|
1772
|
-
// Also update HotUpdaterBundleURL for backwards compatibility
|
|
1773
|
-
// This will point to the staging bundle that will be loaded
|
|
1774
|
-
setBundleURL(bundlePath)
|
|
1775
|
-
|
|
1776
|
-
// 11) Clean up temporary and download folders
|
|
1777
|
-
tempDir.deleteRecursively()
|
|
1832
|
+
if (archiveFile.length() != archive.downloadByteSize) {
|
|
1833
|
+
throw IllegalStateException("Archive compressed size mismatch")
|
|
1834
|
+
}
|
|
1835
|
+
if (!HashUtils.verifyHash(archiveFile, archive.downloadFileHash)) {
|
|
1836
|
+
throw IllegalStateException("Archive compressed hash mismatch")
|
|
1837
|
+
}
|
|
1778
1838
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1839
|
+
TarBrArchiveExtractor.extract(
|
|
1840
|
+
archiveFile = archiveFile,
|
|
1841
|
+
destination = extractedDir,
|
|
1842
|
+
expectedTarByteSize = archive.tarByteSize,
|
|
1843
|
+
expectedFiles = manifest.assets.mapValues { it.value.byteSize!! },
|
|
1844
|
+
)
|
|
1845
|
+
manifest.assets.forEach { (assetPath, expectedAsset) ->
|
|
1846
|
+
val extractedFile =
|
|
1847
|
+
RelativePathResolver.resolveInside(extractedDir, assetPath)
|
|
1848
|
+
?: throw IllegalStateException("Archive output path is invalid")
|
|
1849
|
+
verifyManifestAssetFileOrThrow(extractedFile, expectedAsset)
|
|
1850
|
+
}
|
|
1781
1851
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1852
|
+
if (!tmpDir.renameTo(localBackupDir)) {
|
|
1853
|
+
throw IllegalStateException("Cannot preserve locally prepared staging directory")
|
|
1854
|
+
}
|
|
1855
|
+
if (!extractedDir.renameTo(tmpDir)) {
|
|
1856
|
+
check(localBackupDir.renameTo(tmpDir)) { "Cannot restore locally prepared staging directory" }
|
|
1857
|
+
throw IllegalStateException("Cannot promote verified archive extraction")
|
|
1858
|
+
}
|
|
1859
|
+
localBackupDir.deleteRecursively()
|
|
1860
|
+
|
|
1861
|
+
updateDiffProgressFile(
|
|
1862
|
+
files = progressFiles,
|
|
1863
|
+
assetPath = "bundle.tar.br",
|
|
1864
|
+
status = "downloaded",
|
|
1865
|
+
progress = 1.0,
|
|
1866
|
+
downloadedBytes = archive.downloadByteSize,
|
|
1867
|
+
totalBytes = archive.downloadByteSize,
|
|
1868
|
+
)
|
|
1869
|
+
emitDiffProgress(progressCallback, "finalizing", progressFiles)
|
|
1870
|
+
progressFiles
|
|
1871
|
+
} catch (error: Exception) {
|
|
1872
|
+
Log.w(TAG, "Archive optimization failed; using individual files: ${error.message}")
|
|
1873
|
+
val canUseIndividualPlan =
|
|
1874
|
+
tmpDir.exists() ||
|
|
1875
|
+
(localBackupDir.exists() && localBackupDir.renameTo(tmpDir))
|
|
1876
|
+
if (!canUseIndividualPlan) {
|
|
1877
|
+
throw error
|
|
1785
1878
|
}
|
|
1879
|
+
extractedDir.deleteRecursively()
|
|
1880
|
+
null
|
|
1881
|
+
} finally {
|
|
1882
|
+
archiveScratch.deleteRecursively()
|
|
1883
|
+
extractedDir.deleteRecursively()
|
|
1884
|
+
if (tmpDir.exists()) localBackupDir.deleteRecursively()
|
|
1786
1885
|
}
|
|
1787
1886
|
}
|
|
1788
1887
|
|
|
1789
|
-
/**
|
|
1790
|
-
* Removes old bundles except for the specified bundle IDs, and any leftover .tmp directories
|
|
1791
|
-
*/
|
|
1792
1888
|
private suspend fun updateBundleFromManifest(
|
|
1793
1889
|
bundleId: String,
|
|
1794
1890
|
manifestUrl: String,
|
|
1795
1891
|
manifestFileHash: String,
|
|
1796
1892
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1893
|
+
archiveUrl: String?,
|
|
1797
1894
|
bundleStoreDir: File,
|
|
1798
1895
|
finalBundleDir: File,
|
|
1799
1896
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1800
1897
|
) {
|
|
1801
1898
|
val activeBundleDir = getActiveBundleDir()
|
|
1802
1899
|
val currentBundleId = getBundleId()
|
|
1803
|
-
val currentManifest =
|
|
1804
|
-
getActiveBundleMetadataSnapshot()?.manifest?.let(::parseBundleManifestFromMap)
|
|
1805
1900
|
val baseDir =
|
|
1806
1901
|
fileSystem.getInternalFilesDir() ?: bundleStoreDir.parentFile ?: bundleStoreDir
|
|
1807
1902
|
val tempDir = File(baseDir, "bundle-manifest-temp")
|
|
@@ -1812,7 +1907,7 @@ class BundleFileStorageService(
|
|
|
1812
1907
|
}
|
|
1813
1908
|
tempDir.mkdirs()
|
|
1814
1909
|
|
|
1815
|
-
|
|
1910
|
+
var diffFiles = mutableListOf<DiffProgressFileSnapshot>()
|
|
1816
1911
|
|
|
1817
1912
|
try {
|
|
1818
1913
|
val manifestFile = File(tempDir, "manifest.json")
|
|
@@ -1863,233 +1958,116 @@ class BundleFileStorageService(
|
|
|
1863
1958
|
if (targetManifest.bundleId != bundleId) {
|
|
1864
1959
|
throw HotUpdaterException.invalidBundle()
|
|
1865
1960
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1961
|
+
// Reuse never bypasses the complete artifact contract.
|
|
1962
|
+
if (targetManifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
|
|
1963
|
+
targetManifest.assets.forEach { (path, asset) ->
|
|
1964
|
+
val descriptor = changedAssets.getValue(path)
|
|
1965
|
+
if (RelativePathResolver.normalizeRelativePath(path) != path ||
|
|
1966
|
+
path == "manifest.json" ||
|
|
1967
|
+
descriptor.fileUrl.isNullOrBlank() ||
|
|
1968
|
+
!descriptor.fileHash.equals(asset.fileHash, ignoreCase = true) ||
|
|
1969
|
+
RelativePathResolver.resolveInside(tmpDir, path) == null
|
|
1970
|
+
) {
|
|
1971
|
+
throw HotUpdaterException.invalidBundle()
|
|
1972
|
+
}
|
|
1874
1973
|
}
|
|
1875
|
-
tmpDir.mkdirs()
|
|
1876
|
-
|
|
1877
|
-
val targetEntries = targetManifest.assets.entries.
|
|
1974
|
+
if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
|
|
1975
|
+
pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
|
|
1976
|
+
val targetEntries = targetManifest.assets.entries.sortedBy { it.key }
|
|
1977
|
+
val localDirectories = listOfNotNull(activeBundleDir, finalBundleDir).distinct().filter { it.isDirectory }
|
|
1978
|
+
val downloads = mutableMapOf<String, ChangedAssetDescriptor>()
|
|
1878
1979
|
targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1980
|
+
progressCallback(
|
|
1981
|
+
UpdateProgressPayload(
|
|
1982
|
+
progress = 0.15 + 0.05 * index / targetEntries.size.coerceAtLeast(1),
|
|
1983
|
+
artifactType = "diff",
|
|
1984
|
+
details = DiffProgressDetails(totalFilesCount = 0, completedFilesCount = 0),
|
|
1985
|
+
),
|
|
1986
|
+
)
|
|
1987
|
+
val targetFile = RelativePathResolver.resolveInside(tmpDir, assetPath) ?: throw HotUpdaterException.invalidBundle()
|
|
1988
|
+
targetFile.parentFile?.mkdirs()
|
|
1989
|
+
if (targetFile.isFile && runCatching { verifyManifestAssetFileOrThrow(targetFile, expectedAsset) }.isSuccess) {
|
|
1990
|
+
return@forEachIndexed
|
|
1991
|
+
}
|
|
1992
|
+
if (targetFile.exists()) targetFile.delete()
|
|
1993
|
+
var reused = false
|
|
1994
|
+
for (directory in localDirectories) {
|
|
1995
|
+
val sourceFile = RelativePathResolver.resolveInside(directory, assetPath) ?: continue
|
|
1996
|
+
if (!sourceFile.isFile || runCatching { verifyManifestAssetFileOrThrow(sourceFile, expectedAsset) }.isFailure) continue
|
|
1997
|
+
if (runCatching {
|
|
1998
|
+
copyBundleFile(sourceFile, targetFile)
|
|
1999
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2000
|
+
}.isSuccess
|
|
2001
|
+
) {
|
|
2002
|
+
reused = true
|
|
2003
|
+
break
|
|
1898
2004
|
}
|
|
1899
|
-
|
|
2005
|
+
targetFile.delete()
|
|
2006
|
+
}
|
|
2007
|
+
if (reused) return@forEachIndexed
|
|
2008
|
+
if (builtInAssetResolver.copyIfMatches(assetPath, expectedAsset.fileHash, targetFile)) {
|
|
1900
2009
|
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1901
2010
|
return@forEachIndexed
|
|
1902
2011
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
)
|
|
1913
|
-
emitDiffProgress(
|
|
1914
|
-
progressCallback = progressCallback,
|
|
1915
|
-
phase = "downloading",
|
|
1916
|
-
files = diffFiles,
|
|
1917
|
-
)
|
|
1918
|
-
throw HotUpdaterException.downloadFailed(
|
|
1919
|
-
IllegalStateException("Changed asset missing from update response: $assetPath"),
|
|
1920
|
-
)
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
if (!changedAsset.fileHash.equals(expectedHash, ignoreCase = true)) {
|
|
1924
|
-
updateDiffProgressFile(
|
|
1925
|
-
files = diffFiles,
|
|
1926
|
-
assetPath = assetPath,
|
|
1927
|
-
status = "failed",
|
|
1928
|
-
progress = 0.0,
|
|
1929
|
-
)
|
|
1930
|
-
emitDiffProgress(
|
|
1931
|
-
progressCallback = progressCallback,
|
|
1932
|
-
phase = "downloading",
|
|
1933
|
-
files = diffFiles,
|
|
1934
|
-
)
|
|
1935
|
-
throw HotUpdaterException.signatureVerificationFailed(
|
|
1936
|
-
SignatureVerificationException.FileHashMismatch(),
|
|
1937
|
-
)
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
val patched =
|
|
1941
|
-
applyPatchAssetIfPossible(
|
|
1942
|
-
assetPath = assetPath,
|
|
1943
|
-
changedAsset = changedAsset,
|
|
1944
|
-
currentBundleId = currentBundleId,
|
|
1945
|
-
activeBundleDir = activeBundleDir,
|
|
1946
|
-
targetFile = targetFile,
|
|
1947
|
-
expectedHash = expectedHash,
|
|
2012
|
+
downloads[assetPath] = changedAssets.getValue(assetPath)
|
|
2013
|
+
}
|
|
2014
|
+
val selectedArchive = selectArchive(targetManifest, downloads, archiveUrl)
|
|
2015
|
+
val archiveProgress =
|
|
2016
|
+
if (selectedArchive != null) {
|
|
2017
|
+
tryInstallArchive(
|
|
2018
|
+
archiveUrl = archiveUrl!!,
|
|
2019
|
+
archive = selectedArchive,
|
|
2020
|
+
manifest = targetManifest,
|
|
1948
2021
|
tempDir = tempDir,
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (patched) {
|
|
1953
|
-
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1954
|
-
updateDiffProgressFile(
|
|
1955
|
-
files = diffFiles,
|
|
1956
|
-
assetPath = assetPath,
|
|
1957
|
-
status = "downloaded",
|
|
1958
|
-
progress = 1.0,
|
|
1959
|
-
)
|
|
1960
|
-
emitDiffProgress(
|
|
2022
|
+
bundleStoreDir = bundleStoreDir,
|
|
2023
|
+
tmpDir = tmpDir,
|
|
2024
|
+
bundleId = bundleId,
|
|
1961
2025
|
progressCallback = progressCallback,
|
|
1962
|
-
phase =
|
|
1963
|
-
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1964
|
-
"finalizing"
|
|
1965
|
-
} else {
|
|
1966
|
-
"downloading"
|
|
1967
|
-
},
|
|
1968
|
-
files = diffFiles,
|
|
1969
2026
|
)
|
|
1970
|
-
|
|
2027
|
+
} else {
|
|
2028
|
+
null
|
|
1971
2029
|
}
|
|
1972
2030
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
downloadPath = downloadPath,
|
|
2012
|
-
downloadedBytes = downloadProgress.downloadedBytes,
|
|
2013
|
-
totalBytes = downloadProgress.totalBytes,
|
|
2014
|
-
)
|
|
2015
|
-
emitDiffProgress(
|
|
2016
|
-
progressCallback = progressCallback,
|
|
2017
|
-
phase = "downloading",
|
|
2018
|
-
files = diffFiles,
|
|
2019
|
-
)
|
|
2020
|
-
}
|
|
2021
|
-
) {
|
|
2022
|
-
is DownloadResult.Error -> {
|
|
2023
|
-
updateDiffProgressFile(
|
|
2024
|
-
files = diffFiles,
|
|
2025
|
-
assetPath = assetPath,
|
|
2026
|
-
status = "failed",
|
|
2027
|
-
progress =
|
|
2028
|
-
diffFiles
|
|
2029
|
-
.firstOrNull { it.path == assetPath }
|
|
2030
|
-
?.progress ?: 0.0,
|
|
2031
|
-
)
|
|
2032
|
-
emitDiffProgress(
|
|
2033
|
-
progressCallback = progressCallback,
|
|
2034
|
-
phase = "downloading",
|
|
2035
|
-
files = diffFiles,
|
|
2036
|
-
)
|
|
2037
|
-
if (assetDownloadResult.exception is IncompleteDownloadException) {
|
|
2038
|
-
val incompleteEx =
|
|
2039
|
-
assetDownloadResult.exception as IncompleteDownloadException
|
|
2040
|
-
throw HotUpdaterException.incompleteDownload(
|
|
2041
|
-
incompleteEx.expectedSize,
|
|
2042
|
-
incompleteEx.actualSize,
|
|
2043
|
-
)
|
|
2044
|
-
}
|
|
2045
|
-
throw HotUpdaterException.downloadFailed(assetDownloadResult.exception)
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
is DownloadResult.Success -> {
|
|
2049
|
-
try {
|
|
2050
|
-
if (changedAsset.fileCompression == "br") {
|
|
2051
|
-
decompressBrotliFile(
|
|
2052
|
-
assetDownloadResult.file,
|
|
2053
|
-
targetFile,
|
|
2054
|
-
)
|
|
2055
|
-
}
|
|
2056
|
-
verifyManifestAssetFileOrThrow(
|
|
2057
|
-
targetFile,
|
|
2058
|
-
expectedAsset,
|
|
2059
|
-
)
|
|
2060
|
-
} catch (e: Exception) {
|
|
2061
|
-
updateDiffProgressFile(
|
|
2062
|
-
files = diffFiles,
|
|
2063
|
-
assetPath = assetPath,
|
|
2064
|
-
status = "failed",
|
|
2065
|
-
progress = 1.0,
|
|
2066
|
-
)
|
|
2067
|
-
emitDiffProgress(
|
|
2068
|
-
progressCallback = progressCallback,
|
|
2069
|
-
phase = "downloading",
|
|
2070
|
-
files = diffFiles,
|
|
2071
|
-
)
|
|
2072
|
-
if (e is HotUpdaterException) {
|
|
2073
|
-
throw e
|
|
2074
|
-
}
|
|
2075
|
-
throw HotUpdaterException.downloadFailed(e)
|
|
2076
|
-
} finally {
|
|
2077
|
-
if (changedAsset.fileCompression == "br") {
|
|
2078
|
-
assetDownloadResult.file.delete()
|
|
2031
|
+
if (archiveProgress != null) {
|
|
2032
|
+
diffFiles = archiveProgress
|
|
2033
|
+
} else {
|
|
2034
|
+
diffFiles = createDiffProgressFiles(downloads)
|
|
2035
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2036
|
+
val progressLock = Any()
|
|
2037
|
+
coroutineScope {
|
|
2038
|
+
val permits = Semaphore(4)
|
|
2039
|
+
diffFiles
|
|
2040
|
+
.toList()
|
|
2041
|
+
.map { progressFile ->
|
|
2042
|
+
async {
|
|
2043
|
+
permits.withPermit {
|
|
2044
|
+
val path = progressFile.path
|
|
2045
|
+
val workerTemp =
|
|
2046
|
+
File(tempDir, progressFile.order.toString()).apply { mkdirs() }
|
|
2047
|
+
downloadManifestAsset(
|
|
2048
|
+
assetPath = path,
|
|
2049
|
+
expectedAsset = targetManifest.assets.getValue(path),
|
|
2050
|
+
changedAsset = downloads.getValue(path),
|
|
2051
|
+
currentBundleId = currentBundleId,
|
|
2052
|
+
activeBundleDir = activeBundleDir,
|
|
2053
|
+
targetFile =
|
|
2054
|
+
RelativePathResolver.resolveInside(tmpDir, path)
|
|
2055
|
+
?: throw HotUpdaterException.invalidBundle(),
|
|
2056
|
+
tempDir = workerTemp,
|
|
2057
|
+
progressFile = progressFile,
|
|
2058
|
+
progressCallback = { payload ->
|
|
2059
|
+
synchronized(progressLock) {
|
|
2060
|
+
payload.details?.files?.firstOrNull()?.let { snapshot ->
|
|
2061
|
+
val index = diffFiles.indexOfFirst { it.path == path }
|
|
2062
|
+
diffFiles[index] = snapshot
|
|
2063
|
+
}
|
|
2064
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
)
|
|
2068
|
+
}
|
|
2079
2069
|
}
|
|
2080
|
-
}
|
|
2081
|
-
updateDiffProgressFile(
|
|
2082
|
-
files = diffFiles,
|
|
2083
|
-
assetPath = assetPath,
|
|
2084
|
-
status = "downloaded",
|
|
2085
|
-
progress = 1.0,
|
|
2086
|
-
)
|
|
2087
|
-
emitDiffProgress(
|
|
2088
|
-
progressCallback = progressCallback,
|
|
2089
|
-
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
2090
|
-
files = diffFiles,
|
|
2091
|
-
)
|
|
2092
|
-
}
|
|
2070
|
+
}.awaitAll()
|
|
2093
2071
|
}
|
|
2094
2072
|
}
|
|
2095
2073
|
|
|
@@ -2099,39 +2077,72 @@ class BundleFileStorageService(
|
|
|
2099
2077
|
files = diffFiles,
|
|
2100
2078
|
)
|
|
2101
2079
|
|
|
2102
|
-
|
|
2080
|
+
manifestFile.copyTo(File(tmpDir, "manifest.json"), overwrite = true)
|
|
2103
2081
|
|
|
2104
2082
|
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
2105
2083
|
if (extractedIndex == null) {
|
|
2106
2084
|
throw HotUpdaterException.invalidBundle()
|
|
2107
2085
|
}
|
|
2108
2086
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
val renamed = tmpDir.renameTo(finalBundleDir)
|
|
2114
|
-
if (!renamed) {
|
|
2115
|
-
if (!fileSystem.moveItem(tmpDir.absolutePath, finalBundleDir.absolutePath)) {
|
|
2116
|
-
if (!fileSystem.copyItem(tmpDir.absolutePath, finalBundleDir.absolutePath)) {
|
|
2087
|
+
val backupDir = installBackupDir(bundleId)
|
|
2088
|
+
val updatedMetadata =
|
|
2089
|
+
synchronized(releaseStateLock) {
|
|
2090
|
+
if (backupDir.exists()) {
|
|
2117
2091
|
throw HotUpdaterException.moveOperationFailed()
|
|
2118
2092
|
}
|
|
2119
|
-
tmpDir.deleteRecursively()
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
2093
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2094
|
+
val hadExistingFinal = finalBundleDir.exists()
|
|
2095
|
+
var promoted = false
|
|
2096
|
+
val previousBundleUrl = preferences.getItem("HotUpdaterBundleURL")
|
|
2097
|
+
try {
|
|
2098
|
+
if (hadExistingFinal && !directoryRenamer(finalBundleDir, backupDir)) {
|
|
2099
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2100
|
+
}
|
|
2101
|
+
if (!directoryRenamer(tmpDir, finalBundleDir)) {
|
|
2102
|
+
if (hadExistingFinal) {
|
|
2103
|
+
directoryRenamer(backupDir, finalBundleDir)
|
|
2104
|
+
}
|
|
2105
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2106
|
+
}
|
|
2107
|
+
promoted = true
|
|
2108
|
+
|
|
2109
|
+
val finalIndexFile =
|
|
2110
|
+
resolveBundleFile(finalBundleDir, bundleId)
|
|
2111
|
+
?: throw HotUpdaterException.invalidBundle()
|
|
2112
|
+
if (!isCompleteBundleDirectory(finalBundleDir, bundleId)) {
|
|
2113
|
+
throw HotUpdaterException.invalidBundle()
|
|
2114
|
+
}
|
|
2115
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
2128
2116
|
|
|
2129
|
-
|
|
2117
|
+
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
2118
|
+
val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
2119
|
+
if (!setBundleURL(finalIndexFile.absolutePath)) {
|
|
2120
|
+
throw IllegalStateException("Failed to persist bundle URL")
|
|
2121
|
+
}
|
|
2122
|
+
if (!saveMetadata(nextMetadata)) {
|
|
2123
|
+
throw IllegalStateException("Failed to persist bundle metadata")
|
|
2124
|
+
}
|
|
2130
2125
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2126
|
+
if (backupDir.exists() && !backupDir.deleteRecursively()) {
|
|
2127
|
+
Log.w(TAG, "Failed to remove committed install backup: ${backupDir.absolutePath}")
|
|
2128
|
+
}
|
|
2129
|
+
nextMetadata
|
|
2130
|
+
} catch (e: Exception) {
|
|
2131
|
+
runCatching { preferences.setItem("HotUpdaterBundleURL", previousBundleUrl) }
|
|
2132
|
+
clearActiveBundleMetadataSnapshot()
|
|
2133
|
+
if (promoted && finalBundleDir.exists() && !finalBundleDir.deleteRecursively()) {
|
|
2134
|
+
Log.e(TAG, "Failed to remove uncommitted bundle: ${finalBundleDir.absolutePath}")
|
|
2135
|
+
}
|
|
2136
|
+
if (hadExistingFinal &&
|
|
2137
|
+
!finalBundleDir.exists() &&
|
|
2138
|
+
backupDir.exists() &&
|
|
2139
|
+
!directoryRenamer(backupDir, finalBundleDir)
|
|
2140
|
+
) {
|
|
2141
|
+
Log.e(TAG, "Failed to restore install backup: ${backupDir.absolutePath}")
|
|
2142
|
+
}
|
|
2143
|
+
throw e
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2135
2146
|
|
|
2136
2147
|
tempDir.deleteRecursively()
|
|
2137
2148
|
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
@@ -2149,7 +2160,7 @@ class BundleFileStorageService(
|
|
|
2149
2160
|
)
|
|
2150
2161
|
} catch (e: Exception) {
|
|
2151
2162
|
tempDir.deleteRecursively()
|
|
2152
|
-
|
|
2163
|
+
// A retry rehashes completed files and rejects partial bytes.
|
|
2153
2164
|
throw e
|
|
2154
2165
|
}
|
|
2155
2166
|
}
|
|
@@ -2164,7 +2175,9 @@ class BundleFileStorageService(
|
|
|
2164
2175
|
val bundles =
|
|
2165
2176
|
bundleStoreDir
|
|
2166
2177
|
.listFiles { file ->
|
|
2167
|
-
file.isDirectory &&
|
|
2178
|
+
file.isDirectory &&
|
|
2179
|
+
!file.name.endsWith(".tmp") &&
|
|
2180
|
+
!file.name.endsWith(".install-backup")
|
|
2168
2181
|
}?.toList() ?: return
|
|
2169
2182
|
|
|
2170
2183
|
// Keep only the specified bundle IDs (filter out null values)
|
|
@@ -2287,6 +2300,7 @@ class BundleFileStorageService(
|
|
|
2287
2300
|
pendingUpdateStrategy = null,
|
|
2288
2301
|
pendingTransition = null,
|
|
2289
2302
|
verificationPending = false,
|
|
2303
|
+
launchInProgress = false,
|
|
2290
2304
|
updatedAt = System.currentTimeMillis(),
|
|
2291
2305
|
)
|
|
2292
2306
|
|