@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.31
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 +954 -837
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
- 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/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -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 +109 -84
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +98 -74
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
- package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -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 +13 -45
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- 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/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +124 -69
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +13 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +16 -27
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/fetchUpdateResponse.js +54 -0
- package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
- package/lib/commonjs/httpClient.js +63 -94
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +71 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +78 -60
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +44 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +36 -87
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- 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/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -26
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +117 -64
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +4 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +14 -26
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/fetchUpdateResponse.js +49 -0
- package/lib/module/fetchUpdateResponse.js.map +1 -0
- package/lib/module/httpClient.js +62 -93
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +58 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +71 -53
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +35 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +32 -82
- package/lib/module/releaseCatalogCache.js.map +1 -1
- 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/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/wrap.js +43 -26
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +23 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- 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 +5 -5
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/commonjs/httpClient.d.ts +4 -33
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +31 -22
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- 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 +42 -47
- 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/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +23 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +16 -4
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +2 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- 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 +5 -5
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/module/httpClient.d.ts +4 -33
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +31 -22
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +4 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- 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 +42 -47
- 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/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -7
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/appReady.ts +149 -0
- package/src/checkForUpdate.ts +151 -88
- package/src/clientPlugin.ts +18 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +14 -30
- package/src/fetchUpdateResponse.ts +54 -0
- package/src/httpClient.ts +88 -159
- package/src/index.ts +90 -24
- package/src/native.ts +106 -102
- package/src/pluginHost.ts +45 -0
- package/src/releaseCatalogCache.ts +43 -172
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +41 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +153 -0
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -7
- 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/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- 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/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -615
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -102
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -302
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -883
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -133
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -76
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -316
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -329
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -614
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -102
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -301
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -882
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -128
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -245
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -195
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -77
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -315
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -330
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -611
- package/src/httpClient.spec.ts +0 -132
- package/src/index.spec.ts +0 -337
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -945
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
- package/src/notifyAppReadyInsights.spec.ts +0 -243
- package/src/notifyAppReadyInsights.test-utils.ts +0 -31
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
- package/src/notifyAppReadyInsights.ts +0 -194
- package/src/releaseCatalogCache.spec.ts +0 -282
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -92
- package/src/store.spec.ts +0 -372
- package/src/wrap.spec.ts +0 -406
|
@@ -1,16 +1,21 @@
|
|
|
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
|
|
10
16
|
import java.io.FileInputStream
|
|
11
|
-
import java.io.FileOutputStream
|
|
12
17
|
import java.net.URL
|
|
13
|
-
import
|
|
18
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
14
19
|
|
|
15
20
|
data class ChangedAssetDescriptor(
|
|
16
21
|
val fileUrl: String?,
|
|
@@ -25,13 +30,12 @@ data class BsdiffPatchDescriptor(
|
|
|
25
30
|
val baseFileHash: String,
|
|
26
31
|
val patchFileHash: String,
|
|
27
32
|
val patchUrl: String,
|
|
33
|
+
val byteSize: Long? = null,
|
|
28
34
|
)
|
|
29
35
|
|
|
30
36
|
data class UpdateProgressPayload(
|
|
31
37
|
val progress: Double,
|
|
32
38
|
val artifactType: String,
|
|
33
|
-
val downloadedBytes: Long? = null,
|
|
34
|
-
val totalBytes: Long? = null,
|
|
35
39
|
val details: DiffProgressDetails? = null,
|
|
36
40
|
)
|
|
37
41
|
|
|
@@ -51,6 +55,30 @@ data class DiffProgressDetails(
|
|
|
51
55
|
val files: List<DiffProgressFileSnapshot> = emptyList(),
|
|
52
56
|
)
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* How a staged bundle arrived, the `delivery` of the `updateBundle` result:
|
|
60
|
+
* [PATCH] when a bsdiff patch produced at least one file, [MANIFEST] when only
|
|
61
|
+
* the changed files were downloaded (possibly none), and [ARCHIVE] when the
|
|
62
|
+
* full tar.br archive was downloaded.
|
|
63
|
+
*/
|
|
64
|
+
enum class BundleDelivery(
|
|
65
|
+
val value: String,
|
|
66
|
+
) {
|
|
67
|
+
PATCH("patch"),
|
|
68
|
+
MANIFEST("manifest"),
|
|
69
|
+
ARCHIVE("archive"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The `updateBundle` result. [patchFallback] is true when a patch for the
|
|
74
|
+
* running bundle could not produce its file, so the whole file was downloaded
|
|
75
|
+
* instead.
|
|
76
|
+
*/
|
|
77
|
+
data class UpdateBundleResult(
|
|
78
|
+
val delivery: BundleDelivery,
|
|
79
|
+
val patchFallback: Boolean,
|
|
80
|
+
)
|
|
81
|
+
|
|
54
82
|
/**
|
|
55
83
|
* Interface for bundle storage operations
|
|
56
84
|
*/
|
|
@@ -84,20 +112,20 @@ interface BundleStorageService {
|
|
|
84
112
|
/**
|
|
85
113
|
* Updates the bundle from the specified URL
|
|
86
114
|
* @param bundleId ID of the bundle to update
|
|
87
|
-
* @param
|
|
88
|
-
* @param
|
|
115
|
+
* @param manifestUrl URL of the target bundle manifest
|
|
116
|
+
* @param manifestFileHash Manifest hash or signature
|
|
89
117
|
* @param progressCallback Callback for download progress updates
|
|
118
|
+
* @return how the staged bundle arrived
|
|
90
119
|
* @throws HotUpdaterException if the update fails
|
|
91
120
|
*/
|
|
92
121
|
suspend fun updateBundle(
|
|
93
122
|
bundleId: String,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
123
|
+
manifestUrl: String,
|
|
124
|
+
manifestFileHash: String,
|
|
125
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
126
|
+
archiveUrl: String? = null,
|
|
99
127
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
100
|
-
)
|
|
128
|
+
): UpdateBundleResult
|
|
101
129
|
|
|
102
130
|
fun stageReleaseSelection(selection: PersistedSelection): Boolean = false
|
|
103
131
|
|
|
@@ -133,30 +161,6 @@ interface BundleStorageService {
|
|
|
133
161
|
*/
|
|
134
162
|
fun notifyAppReady(): Map<String, Any?>
|
|
135
163
|
|
|
136
|
-
/**
|
|
137
|
-
* Returns the stable install ID for this app installation.
|
|
138
|
-
*/
|
|
139
|
-
fun getInstallId(): String
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Returns the persisted nullable user id for this app installation.
|
|
143
|
-
*/
|
|
144
|
-
fun getUserId(): String?
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Returns the persisted nullable username for this app installation.
|
|
148
|
-
*/
|
|
149
|
-
fun getUsername(): String?
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Persists the optional user envelope for insights.
|
|
153
|
-
* Passing null clears the stored user identity.
|
|
154
|
-
*/
|
|
155
|
-
fun setUser(
|
|
156
|
-
userId: String?,
|
|
157
|
-
username: String?,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
164
|
/**
|
|
161
165
|
* Gets the crashed bundle history
|
|
162
166
|
* @return CrashedHistory containing crashed bundles
|
|
@@ -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,35 @@ 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}$")
|
|
234
|
+
|
|
235
|
+
/** A patch produced a file that does not match the target, or none at all. */
|
|
236
|
+
private val PATCH_FAILURE =
|
|
237
|
+
UpdateFailure(UpdateFailureStage.INSTALL, UpdateFailureReason.PATCH, resource = UpdateFailureResource.PATCH)
|
|
223
238
|
}
|
|
224
239
|
|
|
225
240
|
private val releaseStateLock = Any()
|
|
226
241
|
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
|
-
),
|
|
242
|
+
private val updateMutex = Mutex()
|
|
243
|
+
private val builtInAssetResolver =
|
|
244
|
+
builtInAssetResolver ?: AndroidBuiltInAssetResolver(
|
|
245
|
+
context,
|
|
246
|
+
File(getBundleStoreDir(), "builtin-index-v1.json"),
|
|
241
247
|
)
|
|
242
|
-
}
|
|
243
248
|
|
|
244
249
|
private fun createDiffProgressFiles(changedAssets: Map<String, ChangedAssetDescriptor>): MutableList<DiffProgressFileSnapshot> =
|
|
245
250
|
changedAssets.keys
|
|
@@ -394,6 +399,10 @@ class BundleFileStorageService(
|
|
|
394
399
|
return File(compressedDir, "$safeName.$compression")
|
|
395
400
|
}
|
|
396
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Decompresses a downloaded `.br` asset. A decoding failure is reported as
|
|
404
|
+
* an extract failure and a failure writing the output as a storage failure.
|
|
405
|
+
*/
|
|
397
406
|
private fun decompressBrotliFile(
|
|
398
407
|
sourceFile: File,
|
|
399
408
|
targetFile: File,
|
|
@@ -408,7 +417,7 @@ class BundleFileStorageService(
|
|
|
408
417
|
try {
|
|
409
418
|
FileInputStream(sourceFile).use { fileInputStream ->
|
|
410
419
|
BrotliInputStream(fileInputStream).use { brotliInputStream ->
|
|
411
|
-
|
|
420
|
+
LocalStorageOutputStream.open(tempOutputFile).use { outputStream ->
|
|
412
421
|
brotliInputStream.copyTo(outputStream)
|
|
413
422
|
}
|
|
414
423
|
}
|
|
@@ -418,15 +427,30 @@ class BundleFileStorageService(
|
|
|
418
427
|
targetFile.delete()
|
|
419
428
|
}
|
|
420
429
|
if (!tempOutputFile.renameTo(targetFile)) {
|
|
421
|
-
tempOutputFile.copyTo(targetFile, overwrite = true)
|
|
430
|
+
storageOperation { tempOutputFile.copyTo(targetFile, overwrite = true) }
|
|
422
431
|
tempOutputFile.delete()
|
|
423
432
|
}
|
|
433
|
+
} catch (error: Exception) {
|
|
434
|
+
tempOutputFile.delete()
|
|
435
|
+
throw HotUpdaterException.downloadFailed(
|
|
436
|
+
error,
|
|
437
|
+
if (error is LocalStorageException) {
|
|
438
|
+
UpdateFailure.install(UpdateFailureReason.STORAGE)
|
|
439
|
+
} else {
|
|
440
|
+
UpdateFailure.install(UpdateFailureReason.EXTRACT)
|
|
441
|
+
},
|
|
442
|
+
)
|
|
424
443
|
} catch (error: Throwable) {
|
|
425
444
|
tempOutputFile.delete()
|
|
426
445
|
throw error
|
|
427
446
|
}
|
|
428
447
|
}
|
|
429
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Tries to produce [targetFile] from a bsdiff patch against the running
|
|
451
|
+
* bundle. A patch for another base bundle or algorithm is not attempted.
|
|
452
|
+
* Once attempted, a failure leaves the file to be downloaded instead.
|
|
453
|
+
*/
|
|
430
454
|
private suspend fun applyPatchAssetIfPossible(
|
|
431
455
|
assetPath: String,
|
|
432
456
|
changedAsset: ChangedAssetDescriptor,
|
|
@@ -437,91 +461,116 @@ class BundleFileStorageService(
|
|
|
437
461
|
tempDir: File,
|
|
438
462
|
diffFiles: MutableList<DiffProgressFileSnapshot>,
|
|
439
463
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
440
|
-
):
|
|
441
|
-
val patch = changedAsset.patch ?: return
|
|
464
|
+
): PatchOutcome {
|
|
465
|
+
val patch = changedAsset.patch ?: return PatchOutcome.NOT_ATTEMPTED
|
|
442
466
|
if (patch.algorithm != "bsdiff" || currentBundleId != patch.baseBundleId) {
|
|
443
|
-
return
|
|
467
|
+
return PatchOutcome.NOT_ATTEMPTED
|
|
444
468
|
}
|
|
445
469
|
|
|
446
|
-
|
|
447
|
-
val
|
|
470
|
+
// The patch targets the running bundle, so from here a miss is a fallback.
|
|
471
|
+
val sourceDir = activeBundleDir ?: return PatchOutcome.FAILED
|
|
472
|
+
val sourceFile = RelativePathResolver.resolveInside(sourceDir, assetPath) ?: return PatchOutcome.FAILED
|
|
448
473
|
if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, patch.baseFileHash)) {
|
|
449
|
-
return
|
|
474
|
+
return PatchOutcome.FAILED
|
|
450
475
|
}
|
|
451
476
|
|
|
452
477
|
val patchFile = patchTempFile(tempDir, assetPath)
|
|
453
478
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
479
|
+
val patched =
|
|
480
|
+
try {
|
|
481
|
+
when (
|
|
482
|
+
val patchDownloadResult =
|
|
483
|
+
downloadService.downloadFile(
|
|
484
|
+
URL(patch.patchUrl),
|
|
485
|
+
patchFile,
|
|
486
|
+
) { downloadProgress ->
|
|
487
|
+
updateDiffProgressFile(
|
|
488
|
+
files = diffFiles,
|
|
489
|
+
assetPath = assetPath,
|
|
490
|
+
status = "downloading",
|
|
491
|
+
progress = downloadProgress.progress,
|
|
492
|
+
downloadPath = patchDownloadPath(assetPath),
|
|
493
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
494
|
+
totalBytes = downloadProgress.totalBytes,
|
|
495
|
+
)
|
|
496
|
+
emitDiffProgress(
|
|
497
|
+
progressCallback = progressCallback,
|
|
498
|
+
phase = "downloading",
|
|
499
|
+
files = diffFiles,
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
) {
|
|
503
|
+
is DownloadResult.Error -> {
|
|
504
|
+
false
|
|
475
505
|
}
|
|
476
|
-
) {
|
|
477
|
-
is DownloadResult.Error -> {
|
|
478
|
-
false
|
|
479
|
-
}
|
|
480
506
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
507
|
+
is DownloadResult.Success -> {
|
|
508
|
+
if (!HashUtils.verifyHash(patchDownloadResult.file, patch.patchFileHash)) {
|
|
509
|
+
false
|
|
510
|
+
} else {
|
|
511
|
+
withContext(Dispatchers.IO) {
|
|
512
|
+
BsdiffPatch.apply(sourceFile, patchDownloadResult.file, targetFile)
|
|
513
|
+
}
|
|
514
|
+
HashUtils.verifyHash(targetFile, expectedHash).also { matches ->
|
|
515
|
+
if (matches) {
|
|
516
|
+
Log.d(
|
|
517
|
+
TAG,
|
|
518
|
+
"HotUpdaterBsdiffPatchApplied asset=$assetPath baseBundleId=${patch.baseBundleId}",
|
|
519
|
+
)
|
|
520
|
+
}
|
|
494
521
|
}
|
|
495
522
|
}
|
|
496
523
|
}
|
|
497
524
|
}
|
|
525
|
+
} catch (_: Exception) {
|
|
526
|
+
false
|
|
527
|
+
} finally {
|
|
528
|
+
patchFile.delete()
|
|
529
|
+
if (!targetFile.exists() || !HashUtils.verifyHash(targetFile, expectedHash)) {
|
|
530
|
+
targetFile.delete()
|
|
531
|
+
}
|
|
498
532
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}.also { patched ->
|
|
507
|
-
if (!patched) {
|
|
508
|
-
resetDiffProgressFile(
|
|
509
|
-
files = diffFiles,
|
|
510
|
-
assetPath = assetPath,
|
|
511
|
-
progressCallback = progressCallback,
|
|
512
|
-
)
|
|
513
|
-
}
|
|
533
|
+
if (!patched) {
|
|
534
|
+
resetDiffProgressFile(
|
|
535
|
+
files = diffFiles,
|
|
536
|
+
assetPath = assetPath,
|
|
537
|
+
progressCallback = progressCallback,
|
|
538
|
+
)
|
|
539
|
+
return PatchOutcome.FAILED
|
|
514
540
|
}
|
|
541
|
+
return PatchOutcome.APPLIED
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private enum class PatchOutcome {
|
|
545
|
+
NOT_ATTEMPTED,
|
|
546
|
+
APPLIED,
|
|
547
|
+
FAILED,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** How one file arrived under the per-file plan. */
|
|
551
|
+
private enum class FileDelivery {
|
|
552
|
+
PATCHED,
|
|
553
|
+
DOWNLOADED,
|
|
554
|
+
DOWNLOADED_AFTER_PATCH_FAILURE,
|
|
515
555
|
}
|
|
516
556
|
|
|
517
557
|
private data class ParsedBundleManifest(
|
|
518
558
|
val bundleId: String,
|
|
519
559
|
val assets: Map<String, ParsedManifestAsset>,
|
|
560
|
+
val archive: ParsedArchive?,
|
|
520
561
|
)
|
|
521
562
|
|
|
522
563
|
private data class ParsedManifestAsset(
|
|
523
564
|
val fileHash: String,
|
|
524
565
|
val signature: String?,
|
|
566
|
+
val byteSize: Long?,
|
|
567
|
+
val downloadByteSize: Long?,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
private data class ParsedArchive(
|
|
571
|
+
val downloadFileHash: String,
|
|
572
|
+
val downloadByteSize: Long,
|
|
573
|
+
val tarByteSize: Long,
|
|
525
574
|
)
|
|
526
575
|
|
|
527
576
|
private data class ActiveBundleMetadataSnapshot(
|
|
@@ -536,14 +585,13 @@ class BundleFileStorageService(
|
|
|
536
585
|
|
|
537
586
|
// Clean up old bundles if isolationKey format changed
|
|
538
587
|
checkAndCleanupIfIsolationKeyChanged()
|
|
588
|
+
|
|
589
|
+
recoverInterruptedPromotions()
|
|
539
590
|
}
|
|
540
591
|
|
|
592
|
+
private var hasPreparedLaunch = false
|
|
541
593
|
private var currentLaunchReport: LaunchReport? = null
|
|
542
594
|
|
|
543
|
-
@Volatile
|
|
544
|
-
private var currentInstallationIdentity: InstallationIdentity? = null
|
|
545
|
-
private val installationIdentityLock = Any()
|
|
546
|
-
|
|
547
595
|
@Volatile
|
|
548
596
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
|
|
549
597
|
private val activeBundleMetadataLock = Any()
|
|
@@ -561,15 +609,13 @@ class BundleFileStorageService(
|
|
|
561
609
|
|
|
562
610
|
private fun getLaunchReportFile(): File = File(getBundleStoreDir(), LaunchReport.LAUNCH_REPORT_FILENAME)
|
|
563
611
|
|
|
564
|
-
private fun getInstallationIdentityFile(): File = File(getBundleStoreDir(), InstallationIdentity.IDENTITY_FILENAME)
|
|
565
|
-
|
|
566
612
|
// MARK: - Metadata Operations
|
|
567
613
|
|
|
568
614
|
private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
|
|
569
615
|
|
|
570
616
|
private fun saveMetadata(metadata: BundleMetadata): Boolean {
|
|
571
617
|
val updatedMetadata = metadata.copy(isolationKey = isolationKey)
|
|
572
|
-
return updatedMetadata
|
|
618
|
+
return metadataWriter(updatedMetadata, getMetadataFile())
|
|
573
619
|
}
|
|
574
620
|
|
|
575
621
|
private fun loadLaunchReport(): LaunchReport? =
|
|
@@ -600,37 +646,10 @@ class BundleFileStorageService(
|
|
|
600
646
|
"appVersion"
|
|
601
647
|
}
|
|
602
648
|
|
|
603
|
-
private fun loadInstallationIdentity(): InstallationIdentity? =
|
|
604
|
-
currentInstallationIdentity ?: InstallationIdentity.loadFromFile(getInstallationIdentityFile())?.also {
|
|
605
|
-
currentInstallationIdentity = it
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private fun saveInstallationIdentity(identity: InstallationIdentity): Boolean {
|
|
609
|
-
currentInstallationIdentity = identity
|
|
610
|
-
return identity.saveToFile(getInstallationIdentityFile())
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private fun getOrCreateInstallationIdentity(): InstallationIdentity =
|
|
614
|
-
synchronized(installationIdentityLock) {
|
|
615
|
-
loadInstallationIdentity()?.let { return@synchronized it }
|
|
616
|
-
|
|
617
|
-
InstallationIdentity(installId = UUID.randomUUID().toString()).also {
|
|
618
|
-
saveInstallationIdentity(it)
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
649
|
private fun getKnownLaunchBundleId(metadata: BundleMetadata): String =
|
|
623
650
|
getCurrentVerifiedBundleId(metadata) ?: HotUpdaterImpl.getMinBundleId()
|
|
624
651
|
|
|
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
|
-
}
|
|
652
|
+
private fun createInitialMetadata(): BundleMetadata = BundleMetadata()
|
|
634
653
|
|
|
635
654
|
private fun extractBundleIdFromCurrentURL(): String? {
|
|
636
655
|
val currentUrl = preferences.getItem("HotUpdaterBundleURL") ?: return null
|
|
@@ -663,7 +682,7 @@ class BundleFileStorageService(
|
|
|
663
682
|
.singleOrNull()
|
|
664
683
|
}
|
|
665
684
|
|
|
666
|
-
return
|
|
685
|
+
return null
|
|
667
686
|
}
|
|
668
687
|
|
|
669
688
|
private fun findBundleFile(bundleId: String): File? {
|
|
@@ -671,6 +690,40 @@ class BundleFileStorageService(
|
|
|
671
690
|
return resolveBundleFile(bundleDir, bundleId)
|
|
672
691
|
}
|
|
673
692
|
|
|
693
|
+
private fun installBackupDir(bundleId: String): File = File(getBundleStoreDir(), "$bundleId.install-backup")
|
|
694
|
+
|
|
695
|
+
private fun isCompleteBundleDirectory(
|
|
696
|
+
bundleDir: File,
|
|
697
|
+
bundleId: String,
|
|
698
|
+
): Boolean {
|
|
699
|
+
val manifest = parseBundleManifestFromFile(File(bundleDir, "manifest.json")) ?: return false
|
|
700
|
+
if (manifest.bundleId != bundleId) return false
|
|
701
|
+
if (resolveBundleFile(bundleDir, bundleId) == null) return false
|
|
702
|
+
return manifest.assets.all { (assetPath, asset) ->
|
|
703
|
+
val file = RelativePathResolver.resolveInside(bundleDir, assetPath)
|
|
704
|
+
file?.isFile == true && runCatching { verifyManifestAssetFileOrThrow(file, asset) }.isSuccess
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private fun recoverInterruptedPromotions(): Boolean {
|
|
709
|
+
val bundleStoreDir = getBundleStoreDir()
|
|
710
|
+
val backups =
|
|
711
|
+
bundleStoreDir.listFiles { file ->
|
|
712
|
+
file.isDirectory && file.name.endsWith(".install-backup")
|
|
713
|
+
} ?: return true
|
|
714
|
+
|
|
715
|
+
return backups.all { backupDir ->
|
|
716
|
+
val bundleId = backupDir.name.removeSuffix(".install-backup")
|
|
717
|
+
val finalDir = File(bundleStoreDir, bundleId)
|
|
718
|
+
when {
|
|
719
|
+
!finalDir.exists() -> directoryRenamer(backupDir, finalDir)
|
|
720
|
+
isCompleteBundleDirectory(finalDir, bundleId) -> backupDir.deleteRecursively()
|
|
721
|
+
!finalDir.deleteRecursively() -> false
|
|
722
|
+
else -> directoryRenamer(backupDir, finalDir)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
674
727
|
private fun getBundleUrlForId(bundleId: String): String? = findBundleFile(bundleId)?.absolutePath
|
|
675
728
|
|
|
676
729
|
private fun getCurrentVerifiedBundleId(metadata: BundleMetadata): String? =
|
|
@@ -681,12 +734,16 @@ class BundleFileStorageService(
|
|
|
681
734
|
}
|
|
682
735
|
|
|
683
736
|
private fun getActiveBundleId(): String? {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
737
|
+
val metadata = loadMetadataOrNull() ?: return null
|
|
738
|
+
val cachedBundleId = extractBundleIdFromCurrentURL()
|
|
739
|
+
if (cachedBundleId != null &&
|
|
740
|
+
(cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId)
|
|
741
|
+
) {
|
|
742
|
+
return cachedBundleId
|
|
743
|
+
}
|
|
687
744
|
return when {
|
|
688
|
-
metadata
|
|
689
|
-
metadata
|
|
745
|
+
metadata.stagingBundleId != null && !metadata.verificationPending -> metadata.stagingBundleId
|
|
746
|
+
metadata.stableBundleId != null -> metadata.stableBundleId
|
|
690
747
|
else -> null
|
|
691
748
|
}
|
|
692
749
|
}
|
|
@@ -734,7 +791,7 @@ class BundleFileStorageService(
|
|
|
734
791
|
|
|
735
792
|
return ActiveBundleMetadataSnapshot(
|
|
736
793
|
activeBundleId = bundleDir.name,
|
|
737
|
-
bundleId = manifestBundleId
|
|
794
|
+
bundleId = manifestBundleId,
|
|
738
795
|
manifest = manifest,
|
|
739
796
|
)
|
|
740
797
|
}
|
|
@@ -752,25 +809,6 @@ class BundleFileStorageService(
|
|
|
752
809
|
return resolveActiveBundleMetadataSnapshot(bundleDir)
|
|
753
810
|
}
|
|
754
811
|
|
|
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
812
|
private fun readManifestFromBundleDir(bundleDir: File): Map<String, Any?>? {
|
|
775
813
|
val manifestFile = File(bundleDir, "manifest.json")
|
|
776
814
|
if (!manifestFile.exists()) {
|
|
@@ -810,6 +848,33 @@ class BundleFileStorageService(
|
|
|
810
848
|
else -> value
|
|
811
849
|
}
|
|
812
850
|
|
|
851
|
+
private fun parseSafeInteger(value: Any?): Long? {
|
|
852
|
+
val parsed =
|
|
853
|
+
when (value) {
|
|
854
|
+
is Byte -> value.toLong()
|
|
855
|
+
is Short -> value.toLong()
|
|
856
|
+
is Int -> value.toLong()
|
|
857
|
+
is Long -> value
|
|
858
|
+
is Float -> value.toDouble().takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
859
|
+
is Double -> value.takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
860
|
+
else -> null
|
|
861
|
+
} ?: return null
|
|
862
|
+
return parsed.takeIf { it in 0..MAX_SAFE_INTEGER }
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private fun parseArchive(value: Any?): ParsedArchive? {
|
|
866
|
+
val archive = value as? Map<*, *> ?: return null
|
|
867
|
+
val downloadFileHash = archive["downloadFileHash"] as? String ?: return null
|
|
868
|
+
if (!SHA256_PATTERN.matches(downloadFileHash)) return null
|
|
869
|
+
val downloadByteSize = parseSafeInteger(archive["downloadByteSize"])?.takeIf { it > 0 } ?: return null
|
|
870
|
+
val tarByteSize = parseSafeInteger(archive["tarByteSize"])?.takeIf { it >= 1024 } ?: return null
|
|
871
|
+
return ParsedArchive(
|
|
872
|
+
downloadFileHash = downloadFileHash,
|
|
873
|
+
downloadByteSize = downloadByteSize,
|
|
874
|
+
tarByteSize = tarByteSize,
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
813
878
|
private fun parseBundleManifestFromMap(manifest: Map<String, Any?>): ParsedBundleManifest? {
|
|
814
879
|
val manifestBundleId =
|
|
815
880
|
(manifest["bundleId"] as? String)
|
|
@@ -835,12 +900,15 @@ class BundleFileStorageService(
|
|
|
835
900
|
ParsedManifestAsset(
|
|
836
901
|
fileHash = fileHash,
|
|
837
902
|
signature = signature?.takeIf { it.isNotBlank() },
|
|
903
|
+
byteSize = parseSafeInteger(assetMap["byteSize"]),
|
|
904
|
+
downloadByteSize = parseSafeInteger(assetMap["downloadByteSize"]),
|
|
838
905
|
)
|
|
839
906
|
}
|
|
840
907
|
|
|
841
908
|
return ParsedBundleManifest(
|
|
842
909
|
bundleId = manifestBundleId,
|
|
843
910
|
assets = assets,
|
|
911
|
+
archive = parseArchive(manifest["archive"]),
|
|
844
912
|
)
|
|
845
913
|
}
|
|
846
914
|
|
|
@@ -858,52 +926,12 @@ class BundleFileStorageService(
|
|
|
858
926
|
}
|
|
859
927
|
}
|
|
860
928
|
|
|
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
929
|
private fun getActiveBundleDir(): File? {
|
|
888
930
|
val activeBundleId = getActiveBundleId() ?: return null
|
|
889
931
|
val bundleDir = File(getBundleStoreDir(), activeBundleId)
|
|
890
932
|
return bundleDir.takeIf { it.exists() }
|
|
891
933
|
}
|
|
892
934
|
|
|
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
935
|
private fun copyBundleFile(
|
|
908
936
|
source: File,
|
|
909
937
|
destination: File,
|
|
@@ -935,14 +963,27 @@ class BundleFileStorageService(
|
|
|
935
963
|
SignatureVerifier.verifyHashSignature(context, asset.fileHash, signature)
|
|
936
964
|
}
|
|
937
965
|
|
|
966
|
+
/**
|
|
967
|
+
* @param hashMismatch how a hash mismatch is reported: a downloaded file
|
|
968
|
+
* that does not match is a hash mismatch, while a patched or extracted file
|
|
969
|
+
* that does not match is a patch or extract failure.
|
|
970
|
+
*/
|
|
938
971
|
private fun verifyManifestAssetFileOrThrow(
|
|
939
972
|
file: File,
|
|
940
973
|
asset: ParsedManifestAsset,
|
|
974
|
+
hashMismatch: UpdateFailure = UpdateFailure.download(UpdateFailureReason.HASH_MISMATCH),
|
|
941
975
|
) {
|
|
942
976
|
try {
|
|
943
977
|
verifyManifestAssetFile(file, asset)
|
|
944
978
|
} catch (e: SignatureVerificationException) {
|
|
945
|
-
throw HotUpdaterException.signatureVerificationFailed(
|
|
979
|
+
throw HotUpdaterException.signatureVerificationFailed(
|
|
980
|
+
e,
|
|
981
|
+
if (e is SignatureVerificationException.FileHashMismatch) {
|
|
982
|
+
hashMismatch
|
|
983
|
+
} else {
|
|
984
|
+
UpdateFailure.ofVerificationError(e)
|
|
985
|
+
},
|
|
986
|
+
)
|
|
946
987
|
}
|
|
947
988
|
}
|
|
948
989
|
|
|
@@ -1019,7 +1060,7 @@ class BundleFileStorageService(
|
|
|
1019
1060
|
getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
|
|
1020
1061
|
val incomingSelection = pendingInstallSelections.remove(bundleId)
|
|
1021
1062
|
if (incomingSelection != null && !isSelectionCurrent(incomingSelection)) {
|
|
1022
|
-
throw
|
|
1063
|
+
throw StaleReleaseSelectionException()
|
|
1023
1064
|
}
|
|
1024
1065
|
val currentVerifiedSelection =
|
|
1025
1066
|
when {
|
|
@@ -1046,6 +1087,7 @@ class BundleFileStorageService(
|
|
|
1046
1087
|
toBundleId = stagingSelection.bundleId,
|
|
1047
1088
|
),
|
|
1048
1089
|
verificationPending = true,
|
|
1090
|
+
launchInProgress = false,
|
|
1049
1091
|
updatedAt = System.currentTimeMillis(),
|
|
1050
1092
|
)
|
|
1051
1093
|
}
|
|
@@ -1076,6 +1118,7 @@ class BundleFileStorageService(
|
|
|
1076
1118
|
pendingUpdateStrategy = null,
|
|
1077
1119
|
pendingTransition = null,
|
|
1078
1120
|
verificationPending = false,
|
|
1121
|
+
launchInProgress = false,
|
|
1079
1122
|
updatedAt = System.currentTimeMillis(),
|
|
1080
1123
|
)
|
|
1081
1124
|
|
|
@@ -1113,10 +1156,9 @@ class BundleFileStorageService(
|
|
|
1113
1156
|
private fun selectLaunch(): LaunchSelection {
|
|
1114
1157
|
val metadata = loadMetadataOrNull()
|
|
1115
1158
|
if (metadata == null) {
|
|
1116
|
-
val cached = getCachedBundleURL()
|
|
1117
1159
|
return LaunchSelection(
|
|
1118
|
-
bundleUrl =
|
|
1119
|
-
launchedBundleId =
|
|
1160
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1161
|
+
launchedBundleId = null,
|
|
1120
1162
|
shouldRollbackOnCrash = false,
|
|
1121
1163
|
)
|
|
1122
1164
|
}
|
|
@@ -1147,10 +1189,9 @@ class BundleFileStorageService(
|
|
|
1147
1189
|
}
|
|
1148
1190
|
}
|
|
1149
1191
|
|
|
1150
|
-
val cached = getCachedBundleURL()
|
|
1151
1192
|
return LaunchSelection(
|
|
1152
|
-
bundleUrl =
|
|
1153
|
-
launchedBundleId =
|
|
1193
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1194
|
+
launchedBundleId = null,
|
|
1154
1195
|
shouldRollbackOnCrash = false,
|
|
1155
1196
|
)
|
|
1156
1197
|
}
|
|
@@ -1302,6 +1343,7 @@ class BundleFileStorageService(
|
|
|
1302
1343
|
pendingUpdateStrategy = null,
|
|
1303
1344
|
pendingTransition = null,
|
|
1304
1345
|
verificationPending = false,
|
|
1346
|
+
launchInProgress = false,
|
|
1305
1347
|
updatedAt = System.currentTimeMillis(),
|
|
1306
1348
|
)
|
|
1307
1349
|
}
|
|
@@ -1354,6 +1396,7 @@ class BundleFileStorageService(
|
|
|
1354
1396
|
pendingUpdateStrategy = null,
|
|
1355
1397
|
pendingTransition = null,
|
|
1356
1398
|
verificationPending = false,
|
|
1399
|
+
launchInProgress = false,
|
|
1357
1400
|
updatedAt = System.currentTimeMillis(),
|
|
1358
1401
|
),
|
|
1359
1402
|
)
|
|
@@ -1390,32 +1433,18 @@ class BundleFileStorageService(
|
|
|
1390
1433
|
}
|
|
1391
1434
|
}
|
|
1392
1435
|
|
|
1393
|
-
override fun getInstallId(): String = getOrCreateInstallationIdentity().installId
|
|
1394
|
-
|
|
1395
|
-
override fun getUserId(): String? = getOrCreateInstallationIdentity().userId
|
|
1396
|
-
|
|
1397
|
-
override fun getUsername(): String? = getOrCreateInstallationIdentity().username
|
|
1398
|
-
|
|
1399
|
-
override fun setUser(
|
|
1400
|
-
userId: String?,
|
|
1401
|
-
username: String?,
|
|
1402
|
-
) = synchronized(installationIdentityLock) {
|
|
1403
|
-
saveInstallationIdentity(
|
|
1404
|
-
getOrCreateInstallationIdentity().copy(
|
|
1405
|
-
userId = userId?.trim()?.takeIf { it.isNotEmpty() },
|
|
1406
|
-
username = username?.trim()?.takeIf { it.isNotEmpty() },
|
|
1407
|
-
),
|
|
1408
|
-
)
|
|
1409
|
-
Unit
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
1436
|
// MARK: - Bundle URL Operations
|
|
1413
1437
|
|
|
1414
1438
|
override fun setBundleURL(localPath: String?): Boolean {
|
|
1415
1439
|
Log.d(TAG, "setBundleURL: $localPath")
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1440
|
+
return try {
|
|
1441
|
+
preferences.setItem("HotUpdaterBundleURL", localPath)
|
|
1442
|
+
clearActiveBundleMetadataSnapshot()
|
|
1443
|
+
true
|
|
1444
|
+
} catch (e: Exception) {
|
|
1445
|
+
Log.e(TAG, "Failed to persist bundle URL", e)
|
|
1446
|
+
false
|
|
1447
|
+
}
|
|
1419
1448
|
}
|
|
1420
1449
|
|
|
1421
1450
|
override fun getCachedBundleURL(): String? {
|
|
@@ -1446,10 +1475,25 @@ class BundleFileStorageService(
|
|
|
1446
1475
|
override fun getFallbackBundleURL(): String = "assets://index.android.bundle"
|
|
1447
1476
|
|
|
1448
1477
|
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
1478
|
+
recoverInterruptedPromotions()
|
|
1449
1479
|
saveLaunchReport(null)
|
|
1450
1480
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1481
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1482
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1483
|
+
if (!hasPreparedLaunch) {
|
|
1484
|
+
val metadata = loadMetadataOrNull()
|
|
1485
|
+
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1486
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it) }
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
hasPreparedLaunch = true
|
|
1451
1490
|
|
|
1452
1491
|
val selection = selectLaunch()
|
|
1492
|
+
if (selection.shouldRollbackOnCrash) {
|
|
1493
|
+
loadMetadataOrNull()?.let { metadata ->
|
|
1494
|
+
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1453
1497
|
Log.d(
|
|
1454
1498
|
TAG,
|
|
1455
1499
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -1459,49 +1503,35 @@ class BundleFileStorageService(
|
|
|
1459
1503
|
|
|
1460
1504
|
override suspend fun updateBundle(
|
|
1461
1505
|
bundleId: String,
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
1506
|
+
manifestUrl: String,
|
|
1507
|
+
manifestFileHash: String,
|
|
1508
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
1509
|
+
archiveUrl: String?,
|
|
1467
1510
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1468
|
-
)
|
|
1511
|
+
): UpdateBundleResult =
|
|
1512
|
+
updateMutex.withLock {
|
|
1513
|
+
updateBundleSerialized(
|
|
1514
|
+
bundleId = bundleId,
|
|
1515
|
+
manifestUrl = manifestUrl,
|
|
1516
|
+
manifestFileHash = manifestFileHash,
|
|
1517
|
+
changedAssets = assets,
|
|
1518
|
+
archiveUrl = archiveUrl,
|
|
1519
|
+
progressCallback = progressCallback,
|
|
1520
|
+
)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private suspend fun updateBundleSerialized(
|
|
1524
|
+
bundleId: String,
|
|
1525
|
+
manifestUrl: String,
|
|
1526
|
+
manifestFileHash: String,
|
|
1527
|
+
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1528
|
+
archiveUrl: String?,
|
|
1529
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1530
|
+
): UpdateBundleResult {
|
|
1469
1531
|
Log.d(
|
|
1470
1532
|
TAG,
|
|
1471
|
-
"updateBundle bundleId $bundleId
|
|
1533
|
+
"updateBundle bundleId $bundleId manifestUrl $manifestUrl",
|
|
1472
1534
|
)
|
|
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
1535
|
|
|
1506
1536
|
// Check if bundle is in crashed history
|
|
1507
1537
|
if (isBundleInCrashedHistory(bundleId)) {
|
|
@@ -1509,299 +1539,444 @@ class BundleFileStorageService(
|
|
|
1509
1539
|
throw HotUpdaterException.bundleInCrashedHistory(bundleId)
|
|
1510
1540
|
}
|
|
1511
1541
|
|
|
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
1542
|
val bundleStoreDir = getBundleStoreDir()
|
|
1522
|
-
|
|
1523
|
-
bundleStoreDir.
|
|
1543
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
1544
|
+
if (!bundleStoreDir.exists()) {
|
|
1545
|
+
bundleStoreDir.mkdirs()
|
|
1546
|
+
}
|
|
1547
|
+
if (!recoverInterruptedPromotions()) {
|
|
1548
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
1549
|
+
}
|
|
1524
1550
|
}
|
|
1525
1551
|
|
|
1526
1552
|
val finalBundleDir = File(bundleStoreDir, bundleId)
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
setBundleURL(existingBundleFile.absolutePath)
|
|
1541
|
-
|
|
1542
|
-
// Keep the current verified bundle as a fallback if one exists.
|
|
1543
|
-
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
1553
|
+
return withContext(Dispatchers.IO) {
|
|
1554
|
+
updateBundleFromManifest(
|
|
1555
|
+
bundleId = bundleId,
|
|
1556
|
+
manifestUrl = manifestUrl,
|
|
1557
|
+
manifestFileHash = manifestFileHash,
|
|
1558
|
+
changedAssets = changedAssets,
|
|
1559
|
+
archiveUrl = archiveUrl,
|
|
1560
|
+
bundleStoreDir = bundleStoreDir,
|
|
1561
|
+
finalBundleDir = finalBundleDir,
|
|
1562
|
+
progressCallback = progressCallback,
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1544
1566
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* Runs one step of the update pipeline and gives its failure a
|
|
1569
|
+
* classification for JS: [resource] is what the step fetches or applies,
|
|
1570
|
+
* and an error nothing anticipated is reported as unknown in [stage].
|
|
1571
|
+
*/
|
|
1572
|
+
private inline fun <T> updateStep(
|
|
1573
|
+
stage: UpdateFailureStage,
|
|
1574
|
+
resource: UpdateFailureResource?,
|
|
1575
|
+
block: () -> T,
|
|
1576
|
+
): T =
|
|
1577
|
+
try {
|
|
1578
|
+
block()
|
|
1579
|
+
} catch (e: Exception) {
|
|
1580
|
+
throw classifyUpdateFailure(e, stage, resource)
|
|
1551
1581
|
}
|
|
1552
1582
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1583
|
+
/**
|
|
1584
|
+
* Gives a failure of the update pipeline its classification for JS.
|
|
1585
|
+
* Failures that are not update failures pass through unchanged, and an
|
|
1586
|
+
* already classified failure only gains a [resource] it did not name.
|
|
1587
|
+
*/
|
|
1588
|
+
private fun classifyUpdateFailure(
|
|
1589
|
+
error: Exception,
|
|
1590
|
+
stage: UpdateFailureStage,
|
|
1591
|
+
resource: UpdateFailureResource?,
|
|
1592
|
+
): Exception =
|
|
1593
|
+
when (error) {
|
|
1594
|
+
is StaleReleaseSelectionException, is CancellationException -> {
|
|
1595
|
+
error
|
|
1596
|
+
}
|
|
1557
1597
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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,
|
|
1598
|
+
is HotUpdaterException -> {
|
|
1599
|
+
error.withResource(resource)
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
else -> {
|
|
1603
|
+
HotUpdaterException.unexpected(
|
|
1604
|
+
error,
|
|
1605
|
+
UpdateFailure.ofUnexpectedError(error, stage).copy(resource = resource),
|
|
1580
1606
|
)
|
|
1581
1607
|
}
|
|
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.",
|
|
1586
|
-
)
|
|
1587
1608
|
}
|
|
1588
1609
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
if (
|
|
1592
|
-
|
|
1610
|
+
/** The rejection for a download that returned [error]. */
|
|
1611
|
+
private fun downloadError(error: Exception): HotUpdaterException =
|
|
1612
|
+
if (error is IncompleteDownloadException) {
|
|
1613
|
+
HotUpdaterException.incompleteDownload(error.expectedSize, error.actualSize)
|
|
1614
|
+
} else {
|
|
1615
|
+
HotUpdaterException.downloadFailed(
|
|
1616
|
+
error,
|
|
1617
|
+
UpdateFailure.ofDownloadError(error) { NetworkState.hasNoActiveNetwork(context) },
|
|
1618
|
+
)
|
|
1593
1619
|
}
|
|
1594
|
-
tempDir.mkdirs()
|
|
1595
1620
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1621
|
+
private suspend fun downloadManifestAsset(
|
|
1622
|
+
assetPath: String,
|
|
1623
|
+
expectedAsset: ParsedManifestAsset,
|
|
1624
|
+
changedAsset: ChangedAssetDescriptor,
|
|
1625
|
+
currentBundleId: String?,
|
|
1626
|
+
activeBundleDir: File?,
|
|
1627
|
+
targetFile: File,
|
|
1628
|
+
tempDir: File,
|
|
1629
|
+
progressFile: DiffProgressFileSnapshot,
|
|
1630
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1631
|
+
): FileDelivery {
|
|
1632
|
+
val expectedHash = expectedAsset.fileHash
|
|
1633
|
+
val diffFiles = mutableListOf(progressFile)
|
|
1634
|
+
val patchOutcome =
|
|
1635
|
+
applyPatchAssetIfPossible(
|
|
1636
|
+
assetPath = assetPath,
|
|
1637
|
+
changedAsset = changedAsset,
|
|
1638
|
+
currentBundleId = currentBundleId,
|
|
1639
|
+
activeBundleDir = activeBundleDir,
|
|
1640
|
+
targetFile = targetFile,
|
|
1641
|
+
expectedHash = expectedHash,
|
|
1642
|
+
tempDir = tempDir,
|
|
1643
|
+
diffFiles = diffFiles,
|
|
1644
|
+
progressCallback = progressCallback,
|
|
1645
|
+
)
|
|
1646
|
+
if (patchOutcome == PatchOutcome.APPLIED) {
|
|
1647
|
+
verifyManifestAssetFileOrThrow(
|
|
1648
|
+
targetFile,
|
|
1649
|
+
expectedAsset,
|
|
1650
|
+
hashMismatch = PATCH_FAILURE,
|
|
1651
|
+
)
|
|
1652
|
+
updateDiffProgressFile(
|
|
1653
|
+
files = diffFiles,
|
|
1654
|
+
assetPath = assetPath,
|
|
1655
|
+
status = "downloaded",
|
|
1656
|
+
progress = 1.0,
|
|
1657
|
+
)
|
|
1658
|
+
emitDiffProgress(
|
|
1659
|
+
progressCallback = progressCallback,
|
|
1660
|
+
phase =
|
|
1661
|
+
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1662
|
+
"finalizing"
|
|
1663
|
+
} else {
|
|
1664
|
+
"downloading"
|
|
1665
|
+
},
|
|
1666
|
+
files = diffFiles,
|
|
1667
|
+
)
|
|
1668
|
+
return FileDelivery.PATCHED
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
val changedAssetFileUrl =
|
|
1672
|
+
changedAsset.fileUrl
|
|
1673
|
+
?: run {
|
|
1674
|
+
updateDiffProgressFile(
|
|
1675
|
+
files = diffFiles,
|
|
1676
|
+
assetPath = assetPath,
|
|
1677
|
+
status = "failed",
|
|
1678
|
+
progress = 0.0,
|
|
1679
|
+
)
|
|
1680
|
+
emitDiffProgress(
|
|
1681
|
+
progressCallback = progressCallback,
|
|
1682
|
+
phase = "downloading",
|
|
1683
|
+
files = diffFiles,
|
|
1684
|
+
)
|
|
1685
|
+
throw HotUpdaterException.downloadFailed(
|
|
1686
|
+
IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
|
|
1687
|
+
PATCH_FAILURE,
|
|
1688
|
+
)
|
|
1689
|
+
}
|
|
1607
1690
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1691
|
+
val downloadPath = downloadPathForChangedAsset(assetPath, changedAsset)
|
|
1692
|
+
val downloadedAssetFile =
|
|
1693
|
+
if (changedAsset.fileCompression == "br") {
|
|
1694
|
+
compressedTempFile(tempDir, assetPath, "br")
|
|
1695
|
+
} else {
|
|
1696
|
+
targetFile
|
|
1697
|
+
}
|
|
1611
1698
|
|
|
1612
|
-
|
|
1699
|
+
when (
|
|
1700
|
+
val assetDownloadResult =
|
|
1613
1701
|
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
|
-
},
|
|
1702
|
+
URL(changedAssetFileUrl),
|
|
1703
|
+
downloadedAssetFile,
|
|
1638
1704
|
) { downloadProgress ->
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
downloadProgress.
|
|
1644
|
-
|
|
1705
|
+
updateDiffProgressFile(
|
|
1706
|
+
files = diffFiles,
|
|
1707
|
+
assetPath = assetPath,
|
|
1708
|
+
status = "downloading",
|
|
1709
|
+
progress = downloadProgress.progress,
|
|
1710
|
+
downloadPath = downloadPath,
|
|
1711
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
1712
|
+
totalBytes = downloadProgress.totalBytes,
|
|
1713
|
+
)
|
|
1714
|
+
emitDiffProgress(
|
|
1715
|
+
progressCallback = progressCallback,
|
|
1716
|
+
phase = "downloading",
|
|
1717
|
+
files = diffFiles,
|
|
1645
1718
|
)
|
|
1646
1719
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1720
|
+
) {
|
|
1721
|
+
is DownloadResult.Error -> {
|
|
1722
|
+
updateDiffProgressFile(
|
|
1723
|
+
files = diffFiles,
|
|
1724
|
+
assetPath = assetPath,
|
|
1725
|
+
status = "failed",
|
|
1726
|
+
progress =
|
|
1727
|
+
diffFiles
|
|
1728
|
+
.firstOrNull { it.path == assetPath }
|
|
1729
|
+
?.progress ?: 0.0,
|
|
1730
|
+
)
|
|
1731
|
+
emitDiffProgress(
|
|
1732
|
+
progressCallback = progressCallback,
|
|
1733
|
+
phase = "downloading",
|
|
1734
|
+
files = diffFiles,
|
|
1735
|
+
)
|
|
1736
|
+
throw downloadError(assetDownloadResult.exception)
|
|
1653
1737
|
}
|
|
1654
1738
|
|
|
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,
|
|
1739
|
+
is DownloadResult.Success -> {
|
|
1740
|
+
try {
|
|
1741
|
+
if (changedAsset.fileCompression == "br") {
|
|
1742
|
+
decompressBrotliFile(
|
|
1743
|
+
assetDownloadResult.file,
|
|
1744
|
+
targetFile,
|
|
1666
1745
|
)
|
|
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
1746
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
throw HotUpdaterException.extractionFormatError()
|
|
1747
|
+
verifyManifestAssetFileOrThrow(
|
|
1748
|
+
targetFile,
|
|
1749
|
+
expectedAsset,
|
|
1750
|
+
)
|
|
1751
|
+
} catch (e: Exception) {
|
|
1752
|
+
updateDiffProgressFile(
|
|
1753
|
+
files = diffFiles,
|
|
1754
|
+
assetPath = assetPath,
|
|
1755
|
+
status = "failed",
|
|
1756
|
+
progress = 1.0,
|
|
1757
|
+
)
|
|
1758
|
+
emitDiffProgress(
|
|
1759
|
+
progressCallback = progressCallback,
|
|
1760
|
+
phase = "downloading",
|
|
1761
|
+
files = diffFiles,
|
|
1762
|
+
)
|
|
1763
|
+
if (e is HotUpdaterException) {
|
|
1764
|
+
throw e
|
|
1710
1765
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1766
|
+
// The bytes arrived; what failed is reading them back.
|
|
1767
|
+
throw HotUpdaterException.downloadFailed(
|
|
1768
|
+
e,
|
|
1769
|
+
UpdateFailure.ofUnexpectedError(e, UpdateFailureStage.DOWNLOAD),
|
|
1770
|
+
)
|
|
1771
|
+
} finally {
|
|
1772
|
+
if (changedAsset.fileCompression == "br") {
|
|
1773
|
+
assetDownloadResult.file.delete()
|
|
1719
1774
|
}
|
|
1775
|
+
}
|
|
1776
|
+
updateDiffProgressFile(
|
|
1777
|
+
files = diffFiles,
|
|
1778
|
+
assetPath = assetPath,
|
|
1779
|
+
status = "downloaded",
|
|
1780
|
+
progress = 1.0,
|
|
1781
|
+
)
|
|
1782
|
+
emitDiffProgress(
|
|
1783
|
+
progressCallback = progressCallback,
|
|
1784
|
+
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
1785
|
+
files = diffFiles,
|
|
1786
|
+
)
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return if (patchOutcome == PatchOutcome.FAILED) {
|
|
1790
|
+
FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE
|
|
1791
|
+
} else {
|
|
1792
|
+
FileDelivery.DOWNLOADED
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1720
1795
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1796
|
+
private fun pruneStagingDirectory(
|
|
1797
|
+
directory: File,
|
|
1798
|
+
assets: Set<String>,
|
|
1799
|
+
prefix: String = "",
|
|
1800
|
+
) {
|
|
1801
|
+
directory.listFiles()?.forEach { file ->
|
|
1802
|
+
val relativePath = prefix + file.name
|
|
1803
|
+
if (file.canonicalFile != File(directory.canonicalFile, file.name)) {
|
|
1804
|
+
if (!file.delete()) {
|
|
1805
|
+
throw HotUpdaterException.storageFailed("Cannot remove staging link: $relativePath")
|
|
1806
|
+
}
|
|
1807
|
+
} else if (file.isDirectory && assets.any { it.startsWith("$relativePath/") }) {
|
|
1808
|
+
pruneStagingDirectory(file, assets, "$relativePath/")
|
|
1809
|
+
} else if (!file.isFile || relativePath !in assets) {
|
|
1810
|
+
if (!file.deleteRecursively()) {
|
|
1811
|
+
throw HotUpdaterException.storageFailed("Cannot remove stale staging file: $relativePath")
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1724
1816
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1817
|
+
private fun selectArchive(
|
|
1818
|
+
manifest: ParsedBundleManifest,
|
|
1819
|
+
downloads: Map<String, ChangedAssetDescriptor>,
|
|
1820
|
+
archiveUrl: String?,
|
|
1821
|
+
): ParsedArchive? {
|
|
1822
|
+
if (archiveUrl.isNullOrBlank() || downloads.size < 2) return null
|
|
1823
|
+
val archive = manifest.archive ?: return null
|
|
1824
|
+
if (manifest.assets.values.any { it.byteSize == null }) return null
|
|
1825
|
+
|
|
1826
|
+
var individualBytes = 0L
|
|
1827
|
+
for ((assetPath, descriptor) in downloads) {
|
|
1828
|
+
val originalBytes = manifest.assets.getValue(assetPath).downloadByteSize ?: return null
|
|
1829
|
+
val expectedBytes =
|
|
1830
|
+
descriptor.patch?.let { patch ->
|
|
1831
|
+
val patchBytes = patch.byteSize?.takeIf { it in 0..MAX_SAFE_INTEGER } ?: return null
|
|
1832
|
+
minOf(originalBytes, patchBytes)
|
|
1833
|
+
} ?: originalBytes
|
|
1834
|
+
if (individualBytes > MAX_SAFE_INTEGER - expectedBytes) return null
|
|
1835
|
+
individualBytes += expectedBytes
|
|
1836
|
+
}
|
|
1837
|
+
var logicalBytes = 0L
|
|
1838
|
+
for (asset in manifest.assets.values) {
|
|
1839
|
+
val byteSize = asset.byteSize ?: return null
|
|
1840
|
+
if (logicalBytes > MAX_SAFE_INTEGER - byteSize) return null
|
|
1841
|
+
logicalBytes += byteSize
|
|
1842
|
+
}
|
|
1843
|
+
if (archive.tarByteSize < logicalBytes) return null
|
|
1844
|
+
val tarFramingBytes = archive.tarByteSize - logicalBytes
|
|
1845
|
+
if (individualBytes > MAX_SAFE_INTEGER - tarFramingBytes) return null
|
|
1846
|
+
val framedByteLimit = individualBytes + tarFramingBytes
|
|
1847
|
+
|
|
1848
|
+
if (archive.downloadByteSize <= individualBytes) return archive
|
|
1849
|
+
if (downloads.size != manifest.assets.size || downloads.values.any { it.patch != null }) return null
|
|
1850
|
+
return archive.takeIf { it.downloadByteSize <= framedByteLimit }
|
|
1851
|
+
}
|
|
1729
1852
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1853
|
+
private suspend fun tryInstallArchive(
|
|
1854
|
+
archiveUrl: String,
|
|
1855
|
+
archive: ParsedArchive,
|
|
1856
|
+
manifest: ParsedBundleManifest,
|
|
1857
|
+
tempDir: File,
|
|
1858
|
+
bundleStoreDir: File,
|
|
1859
|
+
tmpDir: File,
|
|
1860
|
+
bundleId: String,
|
|
1861
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1862
|
+
): MutableList<DiffProgressFileSnapshot>? {
|
|
1863
|
+
val archiveScratch = File(tempDir, "archive")
|
|
1864
|
+
val archiveFile = File(archiveScratch, "bundle.tar.br")
|
|
1865
|
+
val extractedDir = File(bundleStoreDir, "$bundleId.archive.tmp")
|
|
1866
|
+
val localBackupDir = File(bundleStoreDir, "$bundleId.local.tmp")
|
|
1867
|
+
archiveScratch.deleteRecursively()
|
|
1868
|
+
extractedDir.deleteRecursively()
|
|
1869
|
+
localBackupDir.deleteRecursively()
|
|
1870
|
+
archiveScratch.mkdirs()
|
|
1871
|
+
|
|
1872
|
+
val progressFiles =
|
|
1873
|
+
mutableListOf(
|
|
1874
|
+
DiffProgressFileSnapshot(
|
|
1875
|
+
path = "bundle.tar.br",
|
|
1876
|
+
downloadPath = "bundle.tar.br",
|
|
1877
|
+
status = "pending",
|
|
1878
|
+
progress = 0.0,
|
|
1879
|
+
order = 0,
|
|
1880
|
+
),
|
|
1881
|
+
)
|
|
1882
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1750
1883
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1884
|
+
return try {
|
|
1885
|
+
when (
|
|
1886
|
+
val result =
|
|
1887
|
+
downloadService.downloadFileOnce(
|
|
1888
|
+
URL(archiveUrl),
|
|
1889
|
+
archiveFile,
|
|
1890
|
+
) { progress ->
|
|
1891
|
+
updateDiffProgressFile(
|
|
1892
|
+
files = progressFiles,
|
|
1893
|
+
assetPath = "bundle.tar.br",
|
|
1894
|
+
status = "downloading",
|
|
1895
|
+
progress = progress.progress,
|
|
1896
|
+
downloadPath = "bundle.tar.br",
|
|
1897
|
+
downloadedBytes = progress.downloadedBytes,
|
|
1898
|
+
totalBytes = progress.totalBytes,
|
|
1899
|
+
)
|
|
1900
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1758
1901
|
}
|
|
1902
|
+
) {
|
|
1903
|
+
is DownloadResult.Error -> throw result.exception
|
|
1904
|
+
is DownloadResult.Success -> Unit
|
|
1905
|
+
}
|
|
1759
1906
|
|
|
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()
|
|
1907
|
+
if (archiveFile.length() != archive.downloadByteSize) {
|
|
1908
|
+
throw IllegalStateException("Archive compressed size mismatch")
|
|
1909
|
+
}
|
|
1910
|
+
if (!HashUtils.verifyHash(archiveFile, archive.downloadFileHash)) {
|
|
1911
|
+
throw IllegalStateException("Archive compressed hash mismatch")
|
|
1912
|
+
}
|
|
1778
1913
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1914
|
+
TarBrArchiveExtractor.extract(
|
|
1915
|
+
archiveFile = archiveFile,
|
|
1916
|
+
destination = extractedDir,
|
|
1917
|
+
expectedTarByteSize = archive.tarByteSize,
|
|
1918
|
+
expectedFiles = manifest.assets.mapValues { it.value.byteSize!! },
|
|
1919
|
+
)
|
|
1920
|
+
manifest.assets.forEach { (assetPath, expectedAsset) ->
|
|
1921
|
+
val extractedFile =
|
|
1922
|
+
RelativePathResolver.resolveInside(extractedDir, assetPath)
|
|
1923
|
+
?: throw IllegalStateException("Archive output path is invalid")
|
|
1924
|
+
verifyManifestAssetFileOrThrow(
|
|
1925
|
+
extractedFile,
|
|
1926
|
+
expectedAsset,
|
|
1927
|
+
hashMismatch = UpdateFailure.install(UpdateFailureReason.EXTRACT),
|
|
1928
|
+
)
|
|
1929
|
+
}
|
|
1781
1930
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1931
|
+
if (!tmpDir.renameTo(localBackupDir)) {
|
|
1932
|
+
throw IllegalStateException("Cannot preserve locally prepared staging directory")
|
|
1933
|
+
}
|
|
1934
|
+
if (!extractedDir.renameTo(tmpDir)) {
|
|
1935
|
+
check(localBackupDir.renameTo(tmpDir)) { "Cannot restore locally prepared staging directory" }
|
|
1936
|
+
throw IllegalStateException("Cannot promote verified archive extraction")
|
|
1937
|
+
}
|
|
1938
|
+
localBackupDir.deleteRecursively()
|
|
1939
|
+
|
|
1940
|
+
updateDiffProgressFile(
|
|
1941
|
+
files = progressFiles,
|
|
1942
|
+
assetPath = "bundle.tar.br",
|
|
1943
|
+
status = "downloaded",
|
|
1944
|
+
progress = 1.0,
|
|
1945
|
+
downloadedBytes = archive.downloadByteSize,
|
|
1946
|
+
totalBytes = archive.downloadByteSize,
|
|
1947
|
+
)
|
|
1948
|
+
emitDiffProgress(progressCallback, "finalizing", progressFiles)
|
|
1949
|
+
progressFiles
|
|
1950
|
+
} catch (error: Exception) {
|
|
1951
|
+
Log.w(TAG, "Archive optimization failed; using individual files: ${error.message}")
|
|
1952
|
+
val canUseIndividualPlan =
|
|
1953
|
+
tmpDir.exists() ||
|
|
1954
|
+
(localBackupDir.exists() && localBackupDir.renameTo(tmpDir))
|
|
1955
|
+
if (!canUseIndividualPlan) {
|
|
1956
|
+
// Only the staging directory swap can leave tmpDir missing.
|
|
1957
|
+
throw HotUpdaterException.unexpected(error, UpdateFailure.install(UpdateFailureReason.STORAGE))
|
|
1785
1958
|
}
|
|
1959
|
+
extractedDir.deleteRecursively()
|
|
1960
|
+
null
|
|
1961
|
+
} finally {
|
|
1962
|
+
archiveScratch.deleteRecursively()
|
|
1963
|
+
extractedDir.deleteRecursively()
|
|
1964
|
+
if (tmpDir.exists()) localBackupDir.deleteRecursively()
|
|
1786
1965
|
}
|
|
1787
1966
|
}
|
|
1788
1967
|
|
|
1789
|
-
/**
|
|
1790
|
-
* Removes old bundles except for the specified bundle IDs, and any leftover .tmp directories
|
|
1791
|
-
*/
|
|
1792
1968
|
private suspend fun updateBundleFromManifest(
|
|
1793
1969
|
bundleId: String,
|
|
1794
1970
|
manifestUrl: String,
|
|
1795
1971
|
manifestFileHash: String,
|
|
1796
1972
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1973
|
+
archiveUrl: String?,
|
|
1797
1974
|
bundleStoreDir: File,
|
|
1798
1975
|
finalBundleDir: File,
|
|
1799
1976
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1800
|
-
) {
|
|
1977
|
+
): UpdateBundleResult {
|
|
1801
1978
|
val activeBundleDir = getActiveBundleDir()
|
|
1802
1979
|
val currentBundleId = getBundleId()
|
|
1803
|
-
val currentManifest =
|
|
1804
|
-
getActiveBundleMetadataSnapshot()?.manifest?.let(::parseBundleManifestFromMap)
|
|
1805
1980
|
val baseDir =
|
|
1806
1981
|
fileSystem.getInternalFilesDir() ?: bundleStoreDir.parentFile ?: bundleStoreDir
|
|
1807
1982
|
val tempDir = File(baseDir, "bundle-manifest-temp")
|
|
@@ -1812,345 +1987,284 @@ class BundleFileStorageService(
|
|
|
1812
1987
|
}
|
|
1813
1988
|
tempDir.mkdirs()
|
|
1814
1989
|
|
|
1815
|
-
|
|
1990
|
+
var diffFiles = mutableListOf<DiffProgressFileSnapshot>()
|
|
1816
1991
|
|
|
1817
1992
|
try {
|
|
1818
1993
|
val manifestFile = File(tempDir, "manifest.json")
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
phase = "manifest",
|
|
1822
|
-
files = diffFiles,
|
|
1823
|
-
manifestProgress = 0.0,
|
|
1824
|
-
)
|
|
1825
|
-
when (
|
|
1826
|
-
val manifestDownloadResult =
|
|
1827
|
-
downloadService.downloadFile(
|
|
1828
|
-
URL(manifestUrl),
|
|
1829
|
-
manifestFile,
|
|
1830
|
-
) { downloadProgress ->
|
|
1831
|
-
emitDiffProgress(
|
|
1832
|
-
progressCallback = progressCallback,
|
|
1833
|
-
phase = "manifest",
|
|
1834
|
-
files = diffFiles,
|
|
1835
|
-
manifestProgress = downloadProgress.progress,
|
|
1836
|
-
)
|
|
1837
|
-
}
|
|
1838
|
-
) {
|
|
1839
|
-
is DownloadResult.Error -> {
|
|
1840
|
-
if (manifestDownloadResult.exception is IncompleteDownloadException) {
|
|
1841
|
-
val incompleteEx =
|
|
1842
|
-
manifestDownloadResult.exception as IncompleteDownloadException
|
|
1843
|
-
throw HotUpdaterException.incompleteDownload(
|
|
1844
|
-
incompleteEx.expectedSize,
|
|
1845
|
-
incompleteEx.actualSize,
|
|
1846
|
-
)
|
|
1847
|
-
}
|
|
1848
|
-
throw HotUpdaterException.downloadFailed(manifestDownloadResult.exception)
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
is DownloadResult.Success -> {
|
|
1852
|
-
Unit
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
try {
|
|
1857
|
-
SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
|
|
1858
|
-
} catch (e: SignatureVerificationException) {
|
|
1859
|
-
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
val targetManifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
|
|
1863
|
-
if (targetManifest.bundleId != bundleId) {
|
|
1864
|
-
throw HotUpdaterException.invalidBundle()
|
|
1865
|
-
}
|
|
1866
|
-
emitDiffProgress(
|
|
1867
|
-
progressCallback = progressCallback,
|
|
1868
|
-
phase = if (diffFiles.isEmpty()) "finalizing" else "downloading",
|
|
1869
|
-
files = diffFiles,
|
|
1870
|
-
)
|
|
1871
|
-
|
|
1872
|
-
if (tmpDir.exists()) {
|
|
1873
|
-
tmpDir.deleteRecursively()
|
|
1874
|
-
}
|
|
1875
|
-
tmpDir.mkdirs()
|
|
1876
|
-
|
|
1877
|
-
val targetEntries = targetManifest.assets.entries.toList()
|
|
1878
|
-
targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
|
|
1879
|
-
val expectedHash = expectedAsset.fileHash
|
|
1880
|
-
val targetFile =
|
|
1881
|
-
RelativePathResolver.resolveInside(tmpDir, assetPath)
|
|
1882
|
-
?: throw HotUpdaterException.invalidBundle()
|
|
1883
|
-
val currentAsset = currentManifest?.assets?.get(assetPath)
|
|
1884
|
-
|
|
1885
|
-
if (currentAsset?.fileHash == expectedHash) {
|
|
1886
|
-
val sourceDir =
|
|
1887
|
-
activeBundleDir
|
|
1888
|
-
?: throw HotUpdaterException.downloadFailed(
|
|
1889
|
-
IllegalStateException("Current bundle directory unavailable for reused asset: $assetPath"),
|
|
1890
|
-
)
|
|
1891
|
-
val sourceFile =
|
|
1892
|
-
RelativePathResolver.resolveInside(sourceDir, assetPath)
|
|
1893
|
-
?: throw HotUpdaterException.invalidBundle()
|
|
1894
|
-
if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, expectedHash)) {
|
|
1895
|
-
throw HotUpdaterException.downloadFailed(
|
|
1896
|
-
IllegalStateException("Reusable asset missing or corrupted: $assetPath"),
|
|
1897
|
-
)
|
|
1898
|
-
}
|
|
1899
|
-
copyBundleFile(sourceFile, targetFile)
|
|
1900
|
-
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1901
|
-
return@forEachIndexed
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
val changedAsset =
|
|
1905
|
-
changedAssets[assetPath]
|
|
1906
|
-
?: run {
|
|
1907
|
-
updateDiffProgressFile(
|
|
1908
|
-
files = diffFiles,
|
|
1909
|
-
assetPath = assetPath,
|
|
1910
|
-
status = "failed",
|
|
1911
|
-
progress = 0.0,
|
|
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,
|
|
1948
|
-
tempDir = tempDir,
|
|
1949
|
-
diffFiles = diffFiles,
|
|
1950
|
-
progressCallback = progressCallback,
|
|
1951
|
-
)
|
|
1952
|
-
if (patched) {
|
|
1953
|
-
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1954
|
-
updateDiffProgressFile(
|
|
1955
|
-
files = diffFiles,
|
|
1956
|
-
assetPath = assetPath,
|
|
1957
|
-
status = "downloaded",
|
|
1958
|
-
progress = 1.0,
|
|
1959
|
-
)
|
|
1994
|
+
val targetManifest =
|
|
1995
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.MANIFEST) {
|
|
1960
1996
|
emitDiffProgress(
|
|
1961
1997
|
progressCallback = progressCallback,
|
|
1962
|
-
phase =
|
|
1963
|
-
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1964
|
-
"finalizing"
|
|
1965
|
-
} else {
|
|
1966
|
-
"downloading"
|
|
1967
|
-
},
|
|
1998
|
+
phase = "manifest",
|
|
1968
1999
|
files = diffFiles,
|
|
2000
|
+
manifestProgress = 0.0,
|
|
1969
2001
|
)
|
|
1970
|
-
|
|
1971
|
-
|
|
2002
|
+
when (
|
|
2003
|
+
val manifestDownloadResult =
|
|
2004
|
+
downloadService.downloadFile(
|
|
2005
|
+
URL(manifestUrl),
|
|
2006
|
+
manifestFile,
|
|
2007
|
+
) { downloadProgress ->
|
|
2008
|
+
emitDiffProgress(
|
|
2009
|
+
progressCallback = progressCallback,
|
|
2010
|
+
phase = "manifest",
|
|
2011
|
+
files = diffFiles,
|
|
2012
|
+
manifestProgress = downloadProgress.progress,
|
|
2013
|
+
)
|
|
2014
|
+
}
|
|
2015
|
+
) {
|
|
2016
|
+
is DownloadResult.Error -> {
|
|
2017
|
+
throw downloadError(manifestDownloadResult.exception)
|
|
2018
|
+
}
|
|
1972
2019
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
?: run {
|
|
1976
|
-
updateDiffProgressFile(
|
|
1977
|
-
files = diffFiles,
|
|
1978
|
-
assetPath = assetPath,
|
|
1979
|
-
status = "failed",
|
|
1980
|
-
progress = 0.0,
|
|
1981
|
-
)
|
|
1982
|
-
emitDiffProgress(
|
|
1983
|
-
progressCallback = progressCallback,
|
|
1984
|
-
phase = "downloading",
|
|
1985
|
-
files = diffFiles,
|
|
1986
|
-
)
|
|
1987
|
-
throw HotUpdaterException.downloadFailed(
|
|
1988
|
-
IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
|
|
1989
|
-
)
|
|
2020
|
+
is DownloadResult.Success -> {
|
|
2021
|
+
Unit
|
|
1990
2022
|
}
|
|
2023
|
+
}
|
|
1991
2024
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
} else {
|
|
1997
|
-
targetFile
|
|
2025
|
+
try {
|
|
2026
|
+
SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
|
|
2027
|
+
} catch (e: SignatureVerificationException) {
|
|
2028
|
+
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
1998
2029
|
}
|
|
1999
2030
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
)
|
|
2015
|
-
emitDiffProgress(
|
|
2016
|
-
progressCallback = progressCallback,
|
|
2017
|
-
phase = "downloading",
|
|
2018
|
-
files = diffFiles,
|
|
2019
|
-
)
|
|
2031
|
+
val manifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
|
|
2032
|
+
if (manifest.bundleId != bundleId) {
|
|
2033
|
+
throw HotUpdaterException.invalidBundle()
|
|
2034
|
+
}
|
|
2035
|
+
// Reuse never bypasses the complete artifact contract.
|
|
2036
|
+
if (manifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
|
|
2037
|
+
manifest.assets.forEach { (path, asset) ->
|
|
2038
|
+
val descriptor = changedAssets.getValue(path)
|
|
2039
|
+
if (RelativePathResolver.normalizeRelativePath(path) != path ||
|
|
2040
|
+
path == "manifest.json" ||
|
|
2041
|
+
descriptor.fileUrl.isNullOrBlank() ||
|
|
2042
|
+
!descriptor.fileHash.equals(asset.fileHash, ignoreCase = true) ||
|
|
2043
|
+
RelativePathResolver.resolveInside(tmpDir, path) == null
|
|
2044
|
+
) {
|
|
2045
|
+
throw HotUpdaterException.invalidBundle()
|
|
2020
2046
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2047
|
+
}
|
|
2048
|
+
manifest
|
|
2049
|
+
}
|
|
2050
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
2051
|
+
if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
|
|
2052
|
+
pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
|
|
2053
|
+
}
|
|
2054
|
+
val downloads =
|
|
2055
|
+
updateStep(UpdateFailureStage.INSTALL, UpdateFailureResource.FILE) {
|
|
2056
|
+
val targetEntries = targetManifest.assets.entries.sortedBy { it.key }
|
|
2057
|
+
val localDirectories = listOfNotNull(activeBundleDir, finalBundleDir).distinct().filter { it.isDirectory }
|
|
2058
|
+
val downloads = mutableMapOf<String, ChangedAssetDescriptor>()
|
|
2059
|
+
targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
|
|
2060
|
+
progressCallback(
|
|
2061
|
+
UpdateProgressPayload(
|
|
2062
|
+
progress = 0.15 + 0.05 * index / targetEntries.size.coerceAtLeast(1),
|
|
2063
|
+
artifactType = "diff",
|
|
2064
|
+
details = DiffProgressDetails(totalFilesCount = 0, completedFilesCount = 0),
|
|
2065
|
+
),
|
|
2036
2066
|
)
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
incompleteEx.expectedSize,
|
|
2042
|
-
incompleteEx.actualSize,
|
|
2043
|
-
)
|
|
2067
|
+
val targetFile = RelativePathResolver.resolveInside(tmpDir, assetPath) ?: throw HotUpdaterException.invalidBundle()
|
|
2068
|
+
targetFile.parentFile?.mkdirs()
|
|
2069
|
+
if (targetFile.isFile && runCatching { verifyManifestAssetFileOrThrow(targetFile, expectedAsset) }.isSuccess) {
|
|
2070
|
+
return@forEachIndexed
|
|
2044
2071
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
targetFile
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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()
|
|
2072
|
+
if (targetFile.exists()) targetFile.delete()
|
|
2073
|
+
var reused = false
|
|
2074
|
+
for (directory in localDirectories) {
|
|
2075
|
+
val sourceFile = RelativePathResolver.resolveInside(directory, assetPath) ?: continue
|
|
2076
|
+
val sourceMatches =
|
|
2077
|
+
sourceFile.isFile && runCatching { verifyManifestAssetFileOrThrow(sourceFile, expectedAsset) }.isSuccess
|
|
2078
|
+
if (!sourceMatches) continue
|
|
2079
|
+
if (runCatching {
|
|
2080
|
+
copyBundleFile(sourceFile, targetFile)
|
|
2081
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2082
|
+
}.isSuccess
|
|
2083
|
+
) {
|
|
2084
|
+
reused = true
|
|
2085
|
+
break
|
|
2079
2086
|
}
|
|
2087
|
+
targetFile.delete()
|
|
2080
2088
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
)
|
|
2087
|
-
|
|
2089
|
+
if (reused) return@forEachIndexed
|
|
2090
|
+
if (builtInAssetResolver.copyIfMatches(assetPath, expectedAsset.fileHash, targetFile)) {
|
|
2091
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2092
|
+
return@forEachIndexed
|
|
2093
|
+
}
|
|
2094
|
+
downloads[assetPath] = changedAssets.getValue(assetPath)
|
|
2095
|
+
}
|
|
2096
|
+
downloads
|
|
2097
|
+
}
|
|
2098
|
+
val selectedArchive = selectArchive(targetManifest, downloads, archiveUrl)
|
|
2099
|
+
val archiveProgress =
|
|
2100
|
+
if (selectedArchive != null) {
|
|
2101
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.ARCHIVE) {
|
|
2102
|
+
tryInstallArchive(
|
|
2103
|
+
archiveUrl = archiveUrl!!,
|
|
2104
|
+
archive = selectedArchive,
|
|
2105
|
+
manifest = targetManifest,
|
|
2106
|
+
tempDir = tempDir,
|
|
2107
|
+
bundleStoreDir = bundleStoreDir,
|
|
2108
|
+
tmpDir = tmpDir,
|
|
2109
|
+
bundleId = bundleId,
|
|
2088
2110
|
progressCallback = progressCallback,
|
|
2089
|
-
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
2090
|
-
files = diffFiles,
|
|
2091
2111
|
)
|
|
2092
2112
|
}
|
|
2113
|
+
} else {
|
|
2114
|
+
null
|
|
2093
2115
|
}
|
|
2094
|
-
}
|
|
2095
2116
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2117
|
+
val fileDeliveries =
|
|
2118
|
+
if (archiveProgress != null) {
|
|
2119
|
+
diffFiles = archiveProgress
|
|
2120
|
+
emptyList()
|
|
2121
|
+
} else {
|
|
2122
|
+
diffFiles = createDiffProgressFiles(downloads)
|
|
2123
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2124
|
+
val progressLock = Any()
|
|
2125
|
+
coroutineScope {
|
|
2126
|
+
val permits = Semaphore(4)
|
|
2127
|
+
diffFiles
|
|
2128
|
+
.toList()
|
|
2129
|
+
.map { progressFile ->
|
|
2130
|
+
async {
|
|
2131
|
+
permits.withPermit {
|
|
2132
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.FILE) {
|
|
2133
|
+
val path = progressFile.path
|
|
2134
|
+
val workerTemp =
|
|
2135
|
+
File(tempDir, progressFile.order.toString()).apply { mkdirs() }
|
|
2136
|
+
downloadManifestAsset(
|
|
2137
|
+
assetPath = path,
|
|
2138
|
+
expectedAsset = targetManifest.assets.getValue(path),
|
|
2139
|
+
changedAsset = downloads.getValue(path),
|
|
2140
|
+
currentBundleId = currentBundleId,
|
|
2141
|
+
activeBundleDir = activeBundleDir,
|
|
2142
|
+
targetFile =
|
|
2143
|
+
RelativePathResolver.resolveInside(tmpDir, path)
|
|
2144
|
+
?: throw HotUpdaterException.invalidBundle(),
|
|
2145
|
+
tempDir = workerTemp,
|
|
2146
|
+
progressFile = progressFile,
|
|
2147
|
+
progressCallback = { payload ->
|
|
2148
|
+
synchronized(progressLock) {
|
|
2149
|
+
payload.details?.files?.firstOrNull()?.let { snapshot ->
|
|
2150
|
+
val index = diffFiles.indexOfFirst { it.path == path }
|
|
2151
|
+
diffFiles[index] = snapshot
|
|
2152
|
+
}
|
|
2153
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
)
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}.awaitAll()
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2103
2163
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2164
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
2165
|
+
emitDiffProgress(
|
|
2166
|
+
progressCallback = progressCallback,
|
|
2167
|
+
phase = "finalizing",
|
|
2168
|
+
files = diffFiles,
|
|
2169
|
+
)
|
|
2108
2170
|
|
|
2109
|
-
|
|
2110
|
-
finalBundleDir.deleteRecursively()
|
|
2111
|
-
}
|
|
2171
|
+
manifestFile.copyTo(File(tmpDir, "manifest.json"), overwrite = true)
|
|
2112
2172
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
throw HotUpdaterException.moveOperationFailed()
|
|
2118
|
-
}
|
|
2119
|
-
tmpDir.deleteRecursively()
|
|
2173
|
+
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
2174
|
+
if (extractedIndex == null) {
|
|
2175
|
+
// The manifest lists no platform bundle.
|
|
2176
|
+
throw HotUpdaterException.invalidBundle().withResource(UpdateFailureResource.MANIFEST)
|
|
2120
2177
|
}
|
|
2121
|
-
}
|
|
2122
2178
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2179
|
+
val backupDir = installBackupDir(bundleId)
|
|
2180
|
+
val updatedMetadata =
|
|
2181
|
+
synchronized(releaseStateLock) {
|
|
2182
|
+
if (backupDir.exists()) {
|
|
2183
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2184
|
+
}
|
|
2128
2185
|
|
|
2129
|
-
|
|
2186
|
+
val hadExistingFinal = finalBundleDir.exists()
|
|
2187
|
+
var promoted = false
|
|
2188
|
+
val previousBundleUrl = preferences.getItem("HotUpdaterBundleURL")
|
|
2189
|
+
try {
|
|
2190
|
+
if (hadExistingFinal && !directoryRenamer(finalBundleDir, backupDir)) {
|
|
2191
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2192
|
+
}
|
|
2193
|
+
if (!directoryRenamer(tmpDir, finalBundleDir)) {
|
|
2194
|
+
if (hadExistingFinal) {
|
|
2195
|
+
directoryRenamer(backupDir, finalBundleDir)
|
|
2196
|
+
}
|
|
2197
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2198
|
+
}
|
|
2199
|
+
promoted = true
|
|
2130
2200
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2201
|
+
val finalIndexFile =
|
|
2202
|
+
resolveBundleFile(finalBundleDir, bundleId)
|
|
2203
|
+
?: throw HotUpdaterException.invalidBundle()
|
|
2204
|
+
if (!isCompleteBundleDirectory(finalBundleDir, bundleId)) {
|
|
2205
|
+
throw HotUpdaterException.invalidBundle()
|
|
2206
|
+
}
|
|
2207
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
2135
2208
|
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2209
|
+
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
2210
|
+
val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
2211
|
+
if (!setBundleURL(finalIndexFile.absolutePath)) {
|
|
2212
|
+
throw HotUpdaterException.storageFailed("Failed to persist bundle URL")
|
|
2213
|
+
}
|
|
2214
|
+
if (!saveMetadata(nextMetadata)) {
|
|
2215
|
+
throw HotUpdaterException.storageFailed("Failed to persist bundle metadata")
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
if (backupDir.exists() && !backupDir.deleteRecursively()) {
|
|
2219
|
+
Log.w(TAG, "Failed to remove committed install backup: ${backupDir.absolutePath}")
|
|
2220
|
+
}
|
|
2221
|
+
nextMetadata
|
|
2222
|
+
} catch (e: Exception) {
|
|
2223
|
+
runCatching { preferences.setItem("HotUpdaterBundleURL", previousBundleUrl) }
|
|
2224
|
+
clearActiveBundleMetadataSnapshot()
|
|
2225
|
+
if (promoted && finalBundleDir.exists() && !finalBundleDir.deleteRecursively()) {
|
|
2226
|
+
Log.e(TAG, "Failed to remove uncommitted bundle: ${finalBundleDir.absolutePath}")
|
|
2227
|
+
}
|
|
2228
|
+
if (hadExistingFinal &&
|
|
2229
|
+
!finalBundleDir.exists() &&
|
|
2230
|
+
backupDir.exists() &&
|
|
2231
|
+
!directoryRenamer(backupDir, finalBundleDir)
|
|
2232
|
+
) {
|
|
2233
|
+
Log.e(TAG, "Failed to restore install backup: ${backupDir.absolutePath}")
|
|
2234
|
+
}
|
|
2235
|
+
throw e
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
tempDir.deleteRecursively()
|
|
2240
|
+
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
2241
|
+
progressCallback(
|
|
2242
|
+
UpdateProgressPayload(
|
|
2243
|
+
progress = 1.0,
|
|
2244
|
+
artifactType = "diff",
|
|
2245
|
+
details =
|
|
2246
|
+
DiffProgressDetails(
|
|
2247
|
+
totalFilesCount = diffFiles.size,
|
|
2248
|
+
completedFilesCount = diffFiles.count { it.status == "downloaded" },
|
|
2249
|
+
files = diffFiles.toList(),
|
|
2250
|
+
),
|
|
2251
|
+
),
|
|
2252
|
+
)
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
return UpdateBundleResult(
|
|
2256
|
+
delivery =
|
|
2257
|
+
when {
|
|
2258
|
+
archiveProgress != null -> BundleDelivery.ARCHIVE
|
|
2259
|
+
FileDelivery.PATCHED in fileDeliveries -> BundleDelivery.PATCH
|
|
2260
|
+
else -> BundleDelivery.MANIFEST
|
|
2261
|
+
},
|
|
2262
|
+
patchFallback = FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE in fileDeliveries,
|
|
2149
2263
|
)
|
|
2150
2264
|
} catch (e: Exception) {
|
|
2151
2265
|
tempDir.deleteRecursively()
|
|
2152
|
-
|
|
2153
|
-
throw e
|
|
2266
|
+
// A retry rehashes completed files and rejects partial bytes.
|
|
2267
|
+
throw classifyUpdateFailure(e, UpdateFailureStage.DOWNLOAD, resource = null)
|
|
2154
2268
|
}
|
|
2155
2269
|
}
|
|
2156
2270
|
|
|
@@ -2164,7 +2278,9 @@ class BundleFileStorageService(
|
|
|
2164
2278
|
val bundles =
|
|
2165
2279
|
bundleStoreDir
|
|
2166
2280
|
.listFiles { file ->
|
|
2167
|
-
file.isDirectory &&
|
|
2281
|
+
file.isDirectory &&
|
|
2282
|
+
!file.name.endsWith(".tmp") &&
|
|
2283
|
+
!file.name.endsWith(".install-backup")
|
|
2168
2284
|
}?.toList() ?: return
|
|
2169
2285
|
|
|
2170
2286
|
// Keep only the specified bundle IDs (filter out null values)
|
|
@@ -2287,6 +2403,7 @@ class BundleFileStorageService(
|
|
|
2287
2403
|
pendingUpdateStrategy = null,
|
|
2288
2404
|
pendingTransition = null,
|
|
2289
2405
|
verificationPending = false,
|
|
2406
|
+
launchInProgress = false,
|
|
2290
2407
|
updatedAt = System.currentTimeMillis(),
|
|
2291
2408
|
)
|
|
2292
2409
|
|