@hot-updater/react-native 1.0.0-rc.4 → 1.0.0-rc.40
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/build.gradle +13 -2
- package/android/proguard-rules.pro +8 -2
- package/android/src/bridge/BridgeReactInstance.kt +38 -0
- package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1064 -844
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +50 -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 +46 -30
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +75 -192
- 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/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/newarch/JavaScriptExceptionHooks.kt +48 -0
- package/android/src/newarch/ReactIntegrationManager.kt +6 -32
- 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/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1032 -1116
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -54
- 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 +32 -67
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +229 -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 +102 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +120 -68
- 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 +60 -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 +74 -60
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +45 -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 +145 -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 +95 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +113 -63
- 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 +55 -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 +67 -53
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +36 -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 +139 -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 +21 -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 +29 -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 +35 -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 +21 -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 +29 -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 +35 -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 +141 -0
- package/src/checkForUpdate.ts +145 -86
- package/src/clientPlugin.ts +18 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +14 -30
- package/src/fetchUpdateResponse.ts +60 -0
- package/src/httpClient.ts +88 -159
- package/src/index.ts +90 -24
- package/src/native.ts +98 -102
- package/src/pluginHost.ts +47 -0
- package/src/releaseCatalogCache.ts +43 -172
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +34 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +167 -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 -616
- 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 -615
- 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 -612
- 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
|
|
|
@@ -123,6 +151,12 @@ interface BundleStorageService {
|
|
|
123
151
|
|
|
124
152
|
suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = false
|
|
125
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Records that the current launch of a verification-pending bundle can show UI.
|
|
156
|
+
* The next process rolls the bundle back unless first content verified it.
|
|
157
|
+
*/
|
|
158
|
+
fun markLaunchStarted(launchedBundleId: String?)
|
|
159
|
+
|
|
126
160
|
/**
|
|
127
161
|
* Marks the current launch as successful after the first content appeared.
|
|
128
162
|
*/
|
|
@@ -134,31 +168,8 @@ interface BundleStorageService {
|
|
|
134
168
|
fun notifyAppReady(): Map<String, Any?>
|
|
135
169
|
|
|
136
170
|
/**
|
|
137
|
-
*
|
|
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
|
-
/**
|
|
161
|
-
* Gets the crashed bundle history
|
|
171
|
+
* Gets the crashed bundle history, plus a bundle that waits for its retry after
|
|
172
|
+
* an unfinished launch while this process still refuses it (see [InterruptedLaunch])
|
|
162
173
|
* @return CrashedHistory containing crashed bundles
|
|
163
174
|
*/
|
|
164
175
|
fun getCrashHistory(): CrashedHistory
|
|
@@ -183,7 +194,6 @@ interface BundleStorageService {
|
|
|
183
194
|
|
|
184
195
|
/**
|
|
185
196
|
* Gets the current active bundle ID from bundle storage.
|
|
186
|
-
* Reads manifest.json first and falls back to older metadata when needed.
|
|
187
197
|
*/
|
|
188
198
|
fun getBundleId(): String?
|
|
189
199
|
|
|
@@ -213,33 +223,35 @@ class BundleFileStorageService(
|
|
|
213
223
|
private val context: android.content.Context,
|
|
214
224
|
private val fileSystem: FileSystemService,
|
|
215
225
|
private val downloadService: DownloadService,
|
|
216
|
-
private val decompressService: DecompressService,
|
|
217
226
|
private val preferences: PreferencesService,
|
|
218
227
|
private val isolationKey: String,
|
|
219
228
|
private val defaultChannelProvider: () -> String = { HotUpdaterImpl.getChannel(context) },
|
|
229
|
+
builtInAssetResolver: BuiltInAssetResolver? = null,
|
|
230
|
+
private val metadataWriter: (BundleMetadata, File) -> Boolean = { metadata, file ->
|
|
231
|
+
metadata.saveToFile(file)
|
|
232
|
+
},
|
|
233
|
+
private val directoryRenamer: (File, File) -> Boolean = { source, destination ->
|
|
234
|
+
source.renameTo(destination)
|
|
235
|
+
},
|
|
220
236
|
) : BundleStorageService {
|
|
221
237
|
companion object {
|
|
222
238
|
private const val TAG = "BundleStorage"
|
|
239
|
+
private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991L
|
|
240
|
+
private val SHA256_PATTERN = Regex("^[0-9a-f]{64}$")
|
|
241
|
+
|
|
242
|
+
/** A patch produced a file that does not match the target, or none at all. */
|
|
243
|
+
private val PATCH_FAILURE =
|
|
244
|
+
UpdateFailure(UpdateFailureStage.INSTALL, UpdateFailureReason.PATCH, resource = UpdateFailureResource.PATCH)
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
private val releaseStateLock = Any()
|
|
226
248
|
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
|
-
),
|
|
249
|
+
private val updateMutex = Mutex()
|
|
250
|
+
private val builtInAssetResolver =
|
|
251
|
+
builtInAssetResolver ?: AndroidBuiltInAssetResolver(
|
|
252
|
+
context,
|
|
253
|
+
File(getBundleStoreDir(), "builtin-index-v1.json"),
|
|
241
254
|
)
|
|
242
|
-
}
|
|
243
255
|
|
|
244
256
|
private fun createDiffProgressFiles(changedAssets: Map<String, ChangedAssetDescriptor>): MutableList<DiffProgressFileSnapshot> =
|
|
245
257
|
changedAssets.keys
|
|
@@ -394,6 +406,10 @@ class BundleFileStorageService(
|
|
|
394
406
|
return File(compressedDir, "$safeName.$compression")
|
|
395
407
|
}
|
|
396
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Decompresses a downloaded `.br` asset. A decoding failure is reported as
|
|
411
|
+
* an extract failure and a failure writing the output as a storage failure.
|
|
412
|
+
*/
|
|
397
413
|
private fun decompressBrotliFile(
|
|
398
414
|
sourceFile: File,
|
|
399
415
|
targetFile: File,
|
|
@@ -408,7 +424,7 @@ class BundleFileStorageService(
|
|
|
408
424
|
try {
|
|
409
425
|
FileInputStream(sourceFile).use { fileInputStream ->
|
|
410
426
|
BrotliInputStream(fileInputStream).use { brotliInputStream ->
|
|
411
|
-
|
|
427
|
+
LocalStorageOutputStream.open(tempOutputFile).use { outputStream ->
|
|
412
428
|
brotliInputStream.copyTo(outputStream)
|
|
413
429
|
}
|
|
414
430
|
}
|
|
@@ -418,15 +434,30 @@ class BundleFileStorageService(
|
|
|
418
434
|
targetFile.delete()
|
|
419
435
|
}
|
|
420
436
|
if (!tempOutputFile.renameTo(targetFile)) {
|
|
421
|
-
tempOutputFile.copyTo(targetFile, overwrite = true)
|
|
437
|
+
storageOperation { tempOutputFile.copyTo(targetFile, overwrite = true) }
|
|
422
438
|
tempOutputFile.delete()
|
|
423
439
|
}
|
|
440
|
+
} catch (error: Exception) {
|
|
441
|
+
tempOutputFile.delete()
|
|
442
|
+
throw HotUpdaterException.downloadFailed(
|
|
443
|
+
error,
|
|
444
|
+
if (error is LocalStorageException) {
|
|
445
|
+
UpdateFailure.install(UpdateFailureReason.STORAGE)
|
|
446
|
+
} else {
|
|
447
|
+
UpdateFailure.install(UpdateFailureReason.EXTRACT)
|
|
448
|
+
},
|
|
449
|
+
)
|
|
424
450
|
} catch (error: Throwable) {
|
|
425
451
|
tempOutputFile.delete()
|
|
426
452
|
throw error
|
|
427
453
|
}
|
|
428
454
|
}
|
|
429
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Tries to produce [targetFile] from a bsdiff patch against the running
|
|
458
|
+
* bundle. A patch for another base bundle or algorithm is not attempted.
|
|
459
|
+
* Once attempted, a failure leaves the file to be downloaded instead.
|
|
460
|
+
*/
|
|
430
461
|
private suspend fun applyPatchAssetIfPossible(
|
|
431
462
|
assetPath: String,
|
|
432
463
|
changedAsset: ChangedAssetDescriptor,
|
|
@@ -437,91 +468,116 @@ class BundleFileStorageService(
|
|
|
437
468
|
tempDir: File,
|
|
438
469
|
diffFiles: MutableList<DiffProgressFileSnapshot>,
|
|
439
470
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
440
|
-
):
|
|
441
|
-
val patch = changedAsset.patch ?: return
|
|
471
|
+
): PatchOutcome {
|
|
472
|
+
val patch = changedAsset.patch ?: return PatchOutcome.NOT_ATTEMPTED
|
|
442
473
|
if (patch.algorithm != "bsdiff" || currentBundleId != patch.baseBundleId) {
|
|
443
|
-
return
|
|
474
|
+
return PatchOutcome.NOT_ATTEMPTED
|
|
444
475
|
}
|
|
445
476
|
|
|
446
|
-
|
|
447
|
-
val
|
|
477
|
+
// The patch targets the running bundle, so from here a miss is a fallback.
|
|
478
|
+
val sourceDir = activeBundleDir ?: return PatchOutcome.FAILED
|
|
479
|
+
val sourceFile = RelativePathResolver.resolveInside(sourceDir, assetPath) ?: return PatchOutcome.FAILED
|
|
448
480
|
if (!sourceFile.exists() || !HashUtils.verifyHash(sourceFile, patch.baseFileHash)) {
|
|
449
|
-
return
|
|
481
|
+
return PatchOutcome.FAILED
|
|
450
482
|
}
|
|
451
483
|
|
|
452
484
|
val patchFile = patchTempFile(tempDir, assetPath)
|
|
453
485
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
486
|
+
val patched =
|
|
487
|
+
try {
|
|
488
|
+
when (
|
|
489
|
+
val patchDownloadResult =
|
|
490
|
+
downloadService.downloadFile(
|
|
491
|
+
URL(patch.patchUrl),
|
|
492
|
+
patchFile,
|
|
493
|
+
) { downloadProgress ->
|
|
494
|
+
updateDiffProgressFile(
|
|
495
|
+
files = diffFiles,
|
|
496
|
+
assetPath = assetPath,
|
|
497
|
+
status = "downloading",
|
|
498
|
+
progress = downloadProgress.progress,
|
|
499
|
+
downloadPath = patchDownloadPath(assetPath),
|
|
500
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
501
|
+
totalBytes = downloadProgress.totalBytes,
|
|
502
|
+
)
|
|
503
|
+
emitDiffProgress(
|
|
504
|
+
progressCallback = progressCallback,
|
|
505
|
+
phase = "downloading",
|
|
506
|
+
files = diffFiles,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
) {
|
|
510
|
+
is DownloadResult.Error -> {
|
|
511
|
+
false
|
|
475
512
|
}
|
|
476
|
-
) {
|
|
477
|
-
is DownloadResult.Error -> {
|
|
478
|
-
false
|
|
479
|
-
}
|
|
480
513
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
514
|
+
is DownloadResult.Success -> {
|
|
515
|
+
if (!HashUtils.verifyHash(patchDownloadResult.file, patch.patchFileHash)) {
|
|
516
|
+
false
|
|
517
|
+
} else {
|
|
518
|
+
withContext(Dispatchers.IO) {
|
|
519
|
+
BsdiffPatch.apply(sourceFile, patchDownloadResult.file, targetFile)
|
|
520
|
+
}
|
|
521
|
+
HashUtils.verifyHash(targetFile, expectedHash).also { matches ->
|
|
522
|
+
if (matches) {
|
|
523
|
+
Log.d(
|
|
524
|
+
TAG,
|
|
525
|
+
"HotUpdaterBsdiffPatchApplied asset=$assetPath baseBundleId=${patch.baseBundleId}",
|
|
526
|
+
)
|
|
527
|
+
}
|
|
494
528
|
}
|
|
495
529
|
}
|
|
496
530
|
}
|
|
497
531
|
}
|
|
532
|
+
} catch (_: Exception) {
|
|
533
|
+
false
|
|
534
|
+
} finally {
|
|
535
|
+
patchFile.delete()
|
|
536
|
+
if (!targetFile.exists() || !HashUtils.verifyHash(targetFile, expectedHash)) {
|
|
537
|
+
targetFile.delete()
|
|
538
|
+
}
|
|
498
539
|
}
|
|
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
|
-
}
|
|
540
|
+
if (!patched) {
|
|
541
|
+
resetDiffProgressFile(
|
|
542
|
+
files = diffFiles,
|
|
543
|
+
assetPath = assetPath,
|
|
544
|
+
progressCallback = progressCallback,
|
|
545
|
+
)
|
|
546
|
+
return PatchOutcome.FAILED
|
|
514
547
|
}
|
|
548
|
+
return PatchOutcome.APPLIED
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private enum class PatchOutcome {
|
|
552
|
+
NOT_ATTEMPTED,
|
|
553
|
+
APPLIED,
|
|
554
|
+
FAILED,
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** How one file arrived under the per-file plan. */
|
|
558
|
+
private enum class FileDelivery {
|
|
559
|
+
PATCHED,
|
|
560
|
+
DOWNLOADED,
|
|
561
|
+
DOWNLOADED_AFTER_PATCH_FAILURE,
|
|
515
562
|
}
|
|
516
563
|
|
|
517
564
|
private data class ParsedBundleManifest(
|
|
518
565
|
val bundleId: String,
|
|
519
566
|
val assets: Map<String, ParsedManifestAsset>,
|
|
567
|
+
val archive: ParsedArchive?,
|
|
520
568
|
)
|
|
521
569
|
|
|
522
570
|
private data class ParsedManifestAsset(
|
|
523
571
|
val fileHash: String,
|
|
524
572
|
val signature: String?,
|
|
573
|
+
val byteSize: Long?,
|
|
574
|
+
val downloadByteSize: Long?,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
private data class ParsedArchive(
|
|
578
|
+
val downloadFileHash: String,
|
|
579
|
+
val downloadByteSize: Long,
|
|
580
|
+
val tarByteSize: Long,
|
|
525
581
|
)
|
|
526
582
|
|
|
527
583
|
private data class ActiveBundleMetadataSnapshot(
|
|
@@ -536,13 +592,22 @@ class BundleFileStorageService(
|
|
|
536
592
|
|
|
537
593
|
// Clean up old bundles if isolationKey format changed
|
|
538
594
|
checkAndCleanupIfIsolationKeyChanged()
|
|
595
|
+
|
|
596
|
+
recoverInterruptedPromotions()
|
|
539
597
|
}
|
|
540
598
|
|
|
599
|
+
private var hasPreparedLaunch = false
|
|
541
600
|
private var currentLaunchReport: LaunchReport? = null
|
|
542
601
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
private
|
|
602
|
+
// The staged bundle this process launched before its first content. Only that
|
|
603
|
+
// launch is pending; a bundle staged later waits for the next launch.
|
|
604
|
+
private var pendingLaunchBundleId: String? = null
|
|
605
|
+
|
|
606
|
+
// Crash history is written at first content on the main thread while JS can read it.
|
|
607
|
+
private val crashedHistoryLock = Any()
|
|
608
|
+
|
|
609
|
+
// A bundle waiting for its retry that this process keeps refusing, see InterruptedLaunch.
|
|
610
|
+
private var retryHeldBundleId: String? = null
|
|
546
611
|
|
|
547
612
|
@Volatile
|
|
548
613
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
|
|
@@ -561,15 +626,13 @@ class BundleFileStorageService(
|
|
|
561
626
|
|
|
562
627
|
private fun getLaunchReportFile(): File = File(getBundleStoreDir(), LaunchReport.LAUNCH_REPORT_FILENAME)
|
|
563
628
|
|
|
564
|
-
private fun getInstallationIdentityFile(): File = File(getBundleStoreDir(), InstallationIdentity.IDENTITY_FILENAME)
|
|
565
|
-
|
|
566
629
|
// MARK: - Metadata Operations
|
|
567
630
|
|
|
568
631
|
private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
|
|
569
632
|
|
|
570
633
|
private fun saveMetadata(metadata: BundleMetadata): Boolean {
|
|
571
634
|
val updatedMetadata = metadata.copy(isolationKey = isolationKey)
|
|
572
|
-
return updatedMetadata
|
|
635
|
+
return metadataWriter(updatedMetadata, getMetadataFile())
|
|
573
636
|
}
|
|
574
637
|
|
|
575
638
|
private fun loadLaunchReport(): LaunchReport? =
|
|
@@ -600,37 +663,10 @@ class BundleFileStorageService(
|
|
|
600
663
|
"appVersion"
|
|
601
664
|
}
|
|
602
665
|
|
|
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
666
|
private fun getKnownLaunchBundleId(metadata: BundleMetadata): String =
|
|
623
667
|
getCurrentVerifiedBundleId(metadata) ?: HotUpdaterImpl.getMinBundleId()
|
|
624
668
|
|
|
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
|
-
}
|
|
669
|
+
private fun createInitialMetadata(): BundleMetadata = BundleMetadata()
|
|
634
670
|
|
|
635
671
|
private fun extractBundleIdFromCurrentURL(): String? {
|
|
636
672
|
val currentUrl = preferences.getItem("HotUpdaterBundleURL") ?: return null
|
|
@@ -663,7 +699,7 @@ class BundleFileStorageService(
|
|
|
663
699
|
.singleOrNull()
|
|
664
700
|
}
|
|
665
701
|
|
|
666
|
-
return
|
|
702
|
+
return null
|
|
667
703
|
}
|
|
668
704
|
|
|
669
705
|
private fun findBundleFile(bundleId: String): File? {
|
|
@@ -671,6 +707,40 @@ class BundleFileStorageService(
|
|
|
671
707
|
return resolveBundleFile(bundleDir, bundleId)
|
|
672
708
|
}
|
|
673
709
|
|
|
710
|
+
private fun installBackupDir(bundleId: String): File = File(getBundleStoreDir(), "$bundleId.install-backup")
|
|
711
|
+
|
|
712
|
+
private fun isCompleteBundleDirectory(
|
|
713
|
+
bundleDir: File,
|
|
714
|
+
bundleId: String,
|
|
715
|
+
): Boolean {
|
|
716
|
+
val manifest = parseBundleManifestFromFile(File(bundleDir, "manifest.json")) ?: return false
|
|
717
|
+
if (manifest.bundleId != bundleId) return false
|
|
718
|
+
if (resolveBundleFile(bundleDir, bundleId) == null) return false
|
|
719
|
+
return manifest.assets.all { (assetPath, asset) ->
|
|
720
|
+
val file = RelativePathResolver.resolveInside(bundleDir, assetPath)
|
|
721
|
+
file?.isFile == true && runCatching { verifyManifestAssetFileOrThrow(file, asset) }.isSuccess
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private fun recoverInterruptedPromotions(): Boolean {
|
|
726
|
+
val bundleStoreDir = getBundleStoreDir()
|
|
727
|
+
val backups =
|
|
728
|
+
bundleStoreDir.listFiles { file ->
|
|
729
|
+
file.isDirectory && file.name.endsWith(".install-backup")
|
|
730
|
+
} ?: return true
|
|
731
|
+
|
|
732
|
+
return backups.all { backupDir ->
|
|
733
|
+
val bundleId = backupDir.name.removeSuffix(".install-backup")
|
|
734
|
+
val finalDir = File(bundleStoreDir, bundleId)
|
|
735
|
+
when {
|
|
736
|
+
!finalDir.exists() -> directoryRenamer(backupDir, finalDir)
|
|
737
|
+
isCompleteBundleDirectory(finalDir, bundleId) -> backupDir.deleteRecursively()
|
|
738
|
+
!finalDir.deleteRecursively() -> false
|
|
739
|
+
else -> directoryRenamer(backupDir, finalDir)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
674
744
|
private fun getBundleUrlForId(bundleId: String): String? = findBundleFile(bundleId)?.absolutePath
|
|
675
745
|
|
|
676
746
|
private fun getCurrentVerifiedBundleId(metadata: BundleMetadata): String? =
|
|
@@ -681,12 +751,16 @@ class BundleFileStorageService(
|
|
|
681
751
|
}
|
|
682
752
|
|
|
683
753
|
private fun getActiveBundleId(): String? {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
754
|
+
val metadata = loadMetadataOrNull() ?: return null
|
|
755
|
+
val cachedBundleId = extractBundleIdFromCurrentURL()
|
|
756
|
+
if (cachedBundleId != null &&
|
|
757
|
+
(cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId)
|
|
758
|
+
) {
|
|
759
|
+
return cachedBundleId
|
|
760
|
+
}
|
|
687
761
|
return when {
|
|
688
|
-
metadata
|
|
689
|
-
metadata
|
|
762
|
+
metadata.stagingBundleId != null && !metadata.verificationPending -> metadata.stagingBundleId
|
|
763
|
+
metadata.stableBundleId != null -> metadata.stableBundleId
|
|
690
764
|
else -> null
|
|
691
765
|
}
|
|
692
766
|
}
|
|
@@ -734,7 +808,7 @@ class BundleFileStorageService(
|
|
|
734
808
|
|
|
735
809
|
return ActiveBundleMetadataSnapshot(
|
|
736
810
|
activeBundleId = bundleDir.name,
|
|
737
|
-
bundleId = manifestBundleId
|
|
811
|
+
bundleId = manifestBundleId,
|
|
738
812
|
manifest = manifest,
|
|
739
813
|
)
|
|
740
814
|
}
|
|
@@ -752,25 +826,6 @@ class BundleFileStorageService(
|
|
|
752
826
|
return resolveActiveBundleMetadataSnapshot(bundleDir)
|
|
753
827
|
}
|
|
754
828
|
|
|
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
829
|
private fun readManifestFromBundleDir(bundleDir: File): Map<String, Any?>? {
|
|
775
830
|
val manifestFile = File(bundleDir, "manifest.json")
|
|
776
831
|
if (!manifestFile.exists()) {
|
|
@@ -810,6 +865,33 @@ class BundleFileStorageService(
|
|
|
810
865
|
else -> value
|
|
811
866
|
}
|
|
812
867
|
|
|
868
|
+
private fun parseSafeInteger(value: Any?): Long? {
|
|
869
|
+
val parsed =
|
|
870
|
+
when (value) {
|
|
871
|
+
is Byte -> value.toLong()
|
|
872
|
+
is Short -> value.toLong()
|
|
873
|
+
is Int -> value.toLong()
|
|
874
|
+
is Long -> value
|
|
875
|
+
is Float -> value.toDouble().takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
876
|
+
is Double -> value.takeIf { it.isFinite() && it % 1.0 == 0.0 }?.toLong()
|
|
877
|
+
else -> null
|
|
878
|
+
} ?: return null
|
|
879
|
+
return parsed.takeIf { it in 0..MAX_SAFE_INTEGER }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private fun parseArchive(value: Any?): ParsedArchive? {
|
|
883
|
+
val archive = value as? Map<*, *> ?: return null
|
|
884
|
+
val downloadFileHash = archive["downloadFileHash"] as? String ?: return null
|
|
885
|
+
if (!SHA256_PATTERN.matches(downloadFileHash)) return null
|
|
886
|
+
val downloadByteSize = parseSafeInteger(archive["downloadByteSize"])?.takeIf { it > 0 } ?: return null
|
|
887
|
+
val tarByteSize = parseSafeInteger(archive["tarByteSize"])?.takeIf { it >= 1024 } ?: return null
|
|
888
|
+
return ParsedArchive(
|
|
889
|
+
downloadFileHash = downloadFileHash,
|
|
890
|
+
downloadByteSize = downloadByteSize,
|
|
891
|
+
tarByteSize = tarByteSize,
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
813
895
|
private fun parseBundleManifestFromMap(manifest: Map<String, Any?>): ParsedBundleManifest? {
|
|
814
896
|
val manifestBundleId =
|
|
815
897
|
(manifest["bundleId"] as? String)
|
|
@@ -835,12 +917,15 @@ class BundleFileStorageService(
|
|
|
835
917
|
ParsedManifestAsset(
|
|
836
918
|
fileHash = fileHash,
|
|
837
919
|
signature = signature?.takeIf { it.isNotBlank() },
|
|
920
|
+
byteSize = parseSafeInteger(assetMap["byteSize"]),
|
|
921
|
+
downloadByteSize = parseSafeInteger(assetMap["downloadByteSize"]),
|
|
838
922
|
)
|
|
839
923
|
}
|
|
840
924
|
|
|
841
925
|
return ParsedBundleManifest(
|
|
842
926
|
bundleId = manifestBundleId,
|
|
843
927
|
assets = assets,
|
|
928
|
+
archive = parseArchive(manifest["archive"]),
|
|
844
929
|
)
|
|
845
930
|
}
|
|
846
931
|
|
|
@@ -858,52 +943,12 @@ class BundleFileStorageService(
|
|
|
858
943
|
}
|
|
859
944
|
}
|
|
860
945
|
|
|
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
946
|
private fun getActiveBundleDir(): File? {
|
|
888
947
|
val activeBundleId = getActiveBundleId() ?: return null
|
|
889
948
|
val bundleDir = File(getBundleStoreDir(), activeBundleId)
|
|
890
949
|
return bundleDir.takeIf { it.exists() }
|
|
891
950
|
}
|
|
892
951
|
|
|
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
952
|
private fun copyBundleFile(
|
|
908
953
|
source: File,
|
|
909
954
|
destination: File,
|
|
@@ -935,14 +980,27 @@ class BundleFileStorageService(
|
|
|
935
980
|
SignatureVerifier.verifyHashSignature(context, asset.fileHash, signature)
|
|
936
981
|
}
|
|
937
982
|
|
|
983
|
+
/**
|
|
984
|
+
* @param hashMismatch how a hash mismatch is reported: a downloaded file
|
|
985
|
+
* that does not match is a hash mismatch, while a patched or extracted file
|
|
986
|
+
* that does not match is a patch or extract failure.
|
|
987
|
+
*/
|
|
938
988
|
private fun verifyManifestAssetFileOrThrow(
|
|
939
989
|
file: File,
|
|
940
990
|
asset: ParsedManifestAsset,
|
|
991
|
+
hashMismatch: UpdateFailure = UpdateFailure.download(UpdateFailureReason.HASH_MISMATCH),
|
|
941
992
|
) {
|
|
942
993
|
try {
|
|
943
994
|
verifyManifestAssetFile(file, asset)
|
|
944
995
|
} catch (e: SignatureVerificationException) {
|
|
945
|
-
throw HotUpdaterException.signatureVerificationFailed(
|
|
996
|
+
throw HotUpdaterException.signatureVerificationFailed(
|
|
997
|
+
e,
|
|
998
|
+
if (e is SignatureVerificationException.FileHashMismatch) {
|
|
999
|
+
hashMismatch
|
|
1000
|
+
} else {
|
|
1001
|
+
UpdateFailure.ofVerificationError(e)
|
|
1002
|
+
},
|
|
1003
|
+
)
|
|
946
1004
|
}
|
|
947
1005
|
}
|
|
948
1006
|
|
|
@@ -1019,7 +1077,7 @@ class BundleFileStorageService(
|
|
|
1019
1077
|
getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
|
|
1020
1078
|
val incomingSelection = pendingInstallSelections.remove(bundleId)
|
|
1021
1079
|
if (incomingSelection != null && !isSelectionCurrent(incomingSelection)) {
|
|
1022
|
-
throw
|
|
1080
|
+
throw StaleReleaseSelectionException()
|
|
1023
1081
|
}
|
|
1024
1082
|
val currentVerifiedSelection =
|
|
1025
1083
|
when {
|
|
@@ -1046,11 +1104,19 @@ class BundleFileStorageService(
|
|
|
1046
1104
|
toBundleId = stagingSelection.bundleId,
|
|
1047
1105
|
),
|
|
1048
1106
|
verificationPending = true,
|
|
1107
|
+
launchInProgress = false,
|
|
1049
1108
|
updatedAt = System.currentTimeMillis(),
|
|
1050
1109
|
)
|
|
1051
1110
|
}
|
|
1052
1111
|
|
|
1053
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* Rolls back the pending staging bundle. [unfinishedLaunch] means its launch
|
|
1114
|
+
* ended before first content without a crash marker.
|
|
1115
|
+
*/
|
|
1116
|
+
private fun rollbackPendingBundle(
|
|
1117
|
+
stagingBundleId: String,
|
|
1118
|
+
unfinishedLaunch: Boolean = false,
|
|
1119
|
+
): Boolean {
|
|
1054
1120
|
val metadata = loadMetadataOrNull() ?: return false
|
|
1055
1121
|
if (metadata.stagingBundleId != stagingBundleId) {
|
|
1056
1122
|
return false
|
|
@@ -1058,9 +1124,7 @@ class BundleFileStorageService(
|
|
|
1058
1124
|
|
|
1059
1125
|
Log.w(TAG, "Rolling back crashed staging bundle: $stagingBundleId")
|
|
1060
1126
|
|
|
1061
|
-
|
|
1062
|
-
crashedHistory.addEntry(stagingBundleId)
|
|
1063
|
-
saveCrashedHistory(crashedHistory)
|
|
1127
|
+
recordFailedLaunch(stagingBundleId, unfinishedLaunch)
|
|
1064
1128
|
|
|
1065
1129
|
val fallbackBundleId =
|
|
1066
1130
|
metadata.stableBundleId?.takeIf { candidate ->
|
|
@@ -1076,6 +1140,7 @@ class BundleFileStorageService(
|
|
|
1076
1140
|
pendingUpdateStrategy = null,
|
|
1077
1141
|
pendingTransition = null,
|
|
1078
1142
|
verificationPending = false,
|
|
1143
|
+
launchInProgress = false,
|
|
1079
1144
|
updatedAt = System.currentTimeMillis(),
|
|
1080
1145
|
)
|
|
1081
1146
|
|
|
@@ -1113,10 +1178,9 @@ class BundleFileStorageService(
|
|
|
1113
1178
|
private fun selectLaunch(): LaunchSelection {
|
|
1114
1179
|
val metadata = loadMetadataOrNull()
|
|
1115
1180
|
if (metadata == null) {
|
|
1116
|
-
val cached = getCachedBundleURL()
|
|
1117
1181
|
return LaunchSelection(
|
|
1118
|
-
bundleUrl =
|
|
1119
|
-
launchedBundleId =
|
|
1182
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1183
|
+
launchedBundleId = null,
|
|
1120
1184
|
shouldRollbackOnCrash = false,
|
|
1121
1185
|
)
|
|
1122
1186
|
}
|
|
@@ -1147,10 +1211,9 @@ class BundleFileStorageService(
|
|
|
1147
1211
|
}
|
|
1148
1212
|
}
|
|
1149
1213
|
|
|
1150
|
-
val cached = getCachedBundleURL()
|
|
1151
1214
|
return LaunchSelection(
|
|
1152
|
-
bundleUrl =
|
|
1153
|
-
launchedBundleId =
|
|
1215
|
+
bundleUrl = getFallbackBundleURL(),
|
|
1216
|
+
launchedBundleId = null,
|
|
1154
1217
|
shouldRollbackOnCrash = false,
|
|
1155
1218
|
)
|
|
1156
1219
|
}
|
|
@@ -1302,6 +1365,7 @@ class BundleFileStorageService(
|
|
|
1302
1365
|
pendingUpdateStrategy = null,
|
|
1303
1366
|
pendingTransition = null,
|
|
1304
1367
|
verificationPending = false,
|
|
1368
|
+
launchInProgress = false,
|
|
1305
1369
|
updatedAt = System.currentTimeMillis(),
|
|
1306
1370
|
)
|
|
1307
1371
|
}
|
|
@@ -1327,18 +1391,98 @@ class BundleFileStorageService(
|
|
|
1327
1391
|
|
|
1328
1392
|
private fun saveCrashedHistory(history: CrashedHistory): Boolean = history.saveToFile(getCrashedHistoryFile())
|
|
1329
1393
|
|
|
1330
|
-
private fun isBundleInCrashedHistory(bundleId: String): Boolean =
|
|
1394
|
+
private fun isBundleInCrashedHistory(bundleId: String): Boolean = getCrashHistory().contains(bundleId)
|
|
1331
1395
|
|
|
1332
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* The bundles this device refuses to install: crash history, plus a bundle
|
|
1398
|
+
* waiting for its retry until a process that may retry it.
|
|
1399
|
+
*/
|
|
1400
|
+
override fun getCrashHistory(): CrashedHistory =
|
|
1401
|
+
synchronized(crashedHistoryLock) {
|
|
1402
|
+
val history = loadCrashedHistory()
|
|
1403
|
+
val interrupted = history.interruptedLaunch
|
|
1404
|
+
if (interrupted != null &&
|
|
1405
|
+
(!interrupted.retryReady || interrupted.bundleId == retryHeldBundleId) &&
|
|
1406
|
+
!history.contains(interrupted.bundleId)
|
|
1407
|
+
) {
|
|
1408
|
+
history.bundles.add(CrashedBundleEntry(interrupted.bundleId, crashedAt = System.currentTimeMillis()))
|
|
1409
|
+
}
|
|
1410
|
+
history
|
|
1411
|
+
}
|
|
1333
1412
|
|
|
1334
1413
|
override fun clearCrashHistory(): Boolean {
|
|
1335
|
-
|
|
1336
|
-
|
|
1414
|
+
synchronized(crashedHistoryLock) {
|
|
1415
|
+
saveCrashedHistory(CrashedHistory())
|
|
1416
|
+
retryHeldBundleId = null
|
|
1417
|
+
}
|
|
1337
1418
|
Log.d(TAG, "Cleared crash history")
|
|
1338
1419
|
return true
|
|
1339
1420
|
}
|
|
1340
1421
|
|
|
1422
|
+
/**
|
|
1423
|
+
* A crash, or a second unfinished launch of the same bundle, adds the bundle to
|
|
1424
|
+
* crash history. A first unfinished launch cannot tell a hang from a user leaving
|
|
1425
|
+
* early, so it leaves the bundle one retry instead.
|
|
1426
|
+
*/
|
|
1427
|
+
private fun recordFailedLaunch(
|
|
1428
|
+
bundleId: String,
|
|
1429
|
+
unfinishedLaunch: Boolean,
|
|
1430
|
+
) {
|
|
1431
|
+
synchronized(crashedHistoryLock) {
|
|
1432
|
+
val history = loadCrashedHistory()
|
|
1433
|
+
val interrupted = history.interruptedLaunch
|
|
1434
|
+
if (unfinishedLaunch && interrupted?.bundleId != bundleId) {
|
|
1435
|
+
Log.w(TAG, "Launch of $bundleId ended before first content; it gets one retry")
|
|
1436
|
+
history.interruptedLaunch = InterruptedLaunch(bundleId)
|
|
1437
|
+
retryHeldBundleId = bundleId
|
|
1438
|
+
} else {
|
|
1439
|
+
history.addEntry(bundleId)
|
|
1440
|
+
if (interrupted?.bundleId == bundleId) {
|
|
1441
|
+
history.interruptedLaunch = null
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
saveCrashedHistory(history)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* First content appeared, so the app works again. A bundle waiting for its retry
|
|
1450
|
+
* may be installed from the next process on. This process keeps refusing it, so
|
|
1451
|
+
* the session that recovered never reloads into it. Content from that bundle
|
|
1452
|
+
* verifies it.
|
|
1453
|
+
*/
|
|
1454
|
+
private fun readyInterruptedLaunchRetry(currentBundleId: String?) {
|
|
1455
|
+
synchronized(crashedHistoryLock) {
|
|
1456
|
+
val history = loadCrashedHistory()
|
|
1457
|
+
val interrupted = history.interruptedLaunch ?: return
|
|
1458
|
+
if (interrupted.bundleId == currentBundleId) {
|
|
1459
|
+
history.interruptedLaunch = null
|
|
1460
|
+
} else if (interrupted.retryReady) {
|
|
1461
|
+
return
|
|
1462
|
+
} else {
|
|
1463
|
+
history.interruptedLaunch = interrupted.copy(retryReady = true)
|
|
1464
|
+
retryHeldBundleId = interrupted.bundleId
|
|
1465
|
+
}
|
|
1466
|
+
saveCrashedHistory(history)
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
override fun markLaunchStarted(launchedBundleId: String?) {
|
|
1471
|
+
// Unlike prepareLaunch, this can run while JS stages a newer bundle.
|
|
1472
|
+
synchronized(releaseStateLock) {
|
|
1473
|
+
val metadata = loadMetadataOrNull() ?: return
|
|
1474
|
+
if (!isVerificationPending(metadata) ||
|
|
1475
|
+
metadata.launchInProgress ||
|
|
1476
|
+
metadata.stagingBundleId != launchedBundleId
|
|
1477
|
+
) {
|
|
1478
|
+
return
|
|
1479
|
+
}
|
|
1480
|
+
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1341
1484
|
override fun markLaunchCompleted(currentBundleId: String?) {
|
|
1485
|
+
readyInterruptedLaunchRetry(currentBundleId)
|
|
1342
1486
|
val metadata = loadMetadataOrNull() ?: return
|
|
1343
1487
|
val stagingBundleId = metadata.stagingBundleId ?: return
|
|
1344
1488
|
if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
|
|
@@ -1354,6 +1498,7 @@ class BundleFileStorageService(
|
|
|
1354
1498
|
pendingUpdateStrategy = null,
|
|
1355
1499
|
pendingTransition = null,
|
|
1356
1500
|
verificationPending = false,
|
|
1501
|
+
launchInProgress = false,
|
|
1357
1502
|
updatedAt = System.currentTimeMillis(),
|
|
1358
1503
|
),
|
|
1359
1504
|
)
|
|
@@ -1375,7 +1520,11 @@ class BundleFileStorageService(
|
|
|
1375
1520
|
val report = loadLaunchReport()
|
|
1376
1521
|
if (report == null) {
|
|
1377
1522
|
val metadata = loadMetadataOrNull()
|
|
1378
|
-
|
|
1523
|
+
val launchedBundleId = pendingLaunchBundleId
|
|
1524
|
+
if (launchedBundleId != null &&
|
|
1525
|
+
metadata?.verificationPending == true &&
|
|
1526
|
+
metadata.stagingBundleId == launchedBundleId
|
|
1527
|
+
) {
|
|
1379
1528
|
return mapOf("status" to "PENDING")
|
|
1380
1529
|
}
|
|
1381
1530
|
return mapOf("status" to "UNCHANGED")
|
|
@@ -1390,32 +1539,18 @@ class BundleFileStorageService(
|
|
|
1390
1539
|
}
|
|
1391
1540
|
}
|
|
1392
1541
|
|
|
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
1542
|
// MARK: - Bundle URL Operations
|
|
1413
1543
|
|
|
1414
1544
|
override fun setBundleURL(localPath: String?): Boolean {
|
|
1415
1545
|
Log.d(TAG, "setBundleURL: $localPath")
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1546
|
+
return try {
|
|
1547
|
+
preferences.setItem("HotUpdaterBundleURL", localPath)
|
|
1548
|
+
clearActiveBundleMetadataSnapshot()
|
|
1549
|
+
true
|
|
1550
|
+
} catch (e: Exception) {
|
|
1551
|
+
Log.e(TAG, "Failed to persist bundle URL", e)
|
|
1552
|
+
false
|
|
1553
|
+
}
|
|
1419
1554
|
}
|
|
1420
1555
|
|
|
1421
1556
|
override fun getCachedBundleURL(): String? {
|
|
@@ -1446,10 +1581,22 @@ class BundleFileStorageService(
|
|
|
1446
1581
|
override fun getFallbackBundleURL(): String = "assets://index.android.bundle"
|
|
1447
1582
|
|
|
1448
1583
|
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
1584
|
+
recoverInterruptedPromotions()
|
|
1449
1585
|
saveLaunchReport(null)
|
|
1450
1586
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1587
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1588
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1589
|
+
if (!hasPreparedLaunch) {
|
|
1590
|
+
val metadata = loadMetadataOrNull()
|
|
1591
|
+
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1592
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
hasPreparedLaunch = true
|
|
1451
1596
|
|
|
1597
|
+
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
1452
1598
|
val selection = selectLaunch()
|
|
1599
|
+
pendingLaunchBundleId = selection.launchedBundleId.takeIf { selection.shouldRollbackOnCrash }
|
|
1453
1600
|
Log.d(
|
|
1454
1601
|
TAG,
|
|
1455
1602
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -1459,49 +1606,35 @@ class BundleFileStorageService(
|
|
|
1459
1606
|
|
|
1460
1607
|
override suspend fun updateBundle(
|
|
1461
1608
|
bundleId: String,
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
1609
|
+
manifestUrl: String,
|
|
1610
|
+
manifestFileHash: String,
|
|
1611
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
1612
|
+
archiveUrl: String?,
|
|
1467
1613
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1468
|
-
)
|
|
1614
|
+
): UpdateBundleResult =
|
|
1615
|
+
updateMutex.withLock {
|
|
1616
|
+
updateBundleSerialized(
|
|
1617
|
+
bundleId = bundleId,
|
|
1618
|
+
manifestUrl = manifestUrl,
|
|
1619
|
+
manifestFileHash = manifestFileHash,
|
|
1620
|
+
changedAssets = assets,
|
|
1621
|
+
archiveUrl = archiveUrl,
|
|
1622
|
+
progressCallback = progressCallback,
|
|
1623
|
+
)
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
private suspend fun updateBundleSerialized(
|
|
1627
|
+
bundleId: String,
|
|
1628
|
+
manifestUrl: String,
|
|
1629
|
+
manifestFileHash: String,
|
|
1630
|
+
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
1631
|
+
archiveUrl: String?,
|
|
1632
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1633
|
+
): UpdateBundleResult {
|
|
1469
1634
|
Log.d(
|
|
1470
1635
|
TAG,
|
|
1471
|
-
"updateBundle bundleId $bundleId
|
|
1636
|
+
"updateBundle bundleId $bundleId manifestUrl $manifestUrl",
|
|
1472
1637
|
)
|
|
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
1638
|
|
|
1506
1639
|
// Check if bundle is in crashed history
|
|
1507
1640
|
if (isBundleInCrashedHistory(bundleId)) {
|
|
@@ -1509,299 +1642,444 @@ class BundleFileStorageService(
|
|
|
1509
1642
|
throw HotUpdaterException.bundleInCrashedHistory(bundleId)
|
|
1510
1643
|
}
|
|
1511
1644
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1645
|
+
val bundleStoreDir = getBundleStoreDir()
|
|
1646
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
1647
|
+
if (!bundleStoreDir.exists()) {
|
|
1648
|
+
bundleStoreDir.mkdirs()
|
|
1649
|
+
}
|
|
1650
|
+
if (!recoverInterruptedPromotions()) {
|
|
1651
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
1518
1652
|
}
|
|
1519
|
-
|
|
1520
|
-
val baseDir = fileSystem.getInternalFilesDir()
|
|
1521
|
-
val bundleStoreDir = getBundleStoreDir()
|
|
1522
|
-
if (!bundleStoreDir.exists()) {
|
|
1523
|
-
bundleStoreDir.mkdirs()
|
|
1524
1653
|
}
|
|
1525
1654
|
|
|
1526
1655
|
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)
|
|
1656
|
+
return withContext(Dispatchers.IO) {
|
|
1657
|
+
updateBundleFromManifest(
|
|
1658
|
+
bundleId = bundleId,
|
|
1659
|
+
manifestUrl = manifestUrl,
|
|
1660
|
+
manifestFileHash = manifestFileHash,
|
|
1661
|
+
changedAssets = changedAssets,
|
|
1662
|
+
archiveUrl = archiveUrl,
|
|
1663
|
+
bundleStoreDir = bundleStoreDir,
|
|
1664
|
+
finalBundleDir = finalBundleDir,
|
|
1665
|
+
progressCallback = progressCallback,
|
|
1666
|
+
)
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1544
1669
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1670
|
+
/**
|
|
1671
|
+
* Runs one step of the update pipeline and gives its failure a
|
|
1672
|
+
* classification for JS: [resource] is what the step fetches or applies,
|
|
1673
|
+
* and an error nothing anticipated is reported as unknown in [stage].
|
|
1674
|
+
*/
|
|
1675
|
+
private inline fun <T> updateStep(
|
|
1676
|
+
stage: UpdateFailureStage,
|
|
1677
|
+
resource: UpdateFailureResource?,
|
|
1678
|
+
block: () -> T,
|
|
1679
|
+
): T =
|
|
1680
|
+
try {
|
|
1681
|
+
block()
|
|
1682
|
+
} catch (e: Exception) {
|
|
1683
|
+
throw classifyUpdateFailure(e, stage, resource)
|
|
1551
1684
|
}
|
|
1552
1685
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1686
|
+
/**
|
|
1687
|
+
* Gives a failure of the update pipeline its classification for JS.
|
|
1688
|
+
* Failures that are not update failures pass through unchanged, and an
|
|
1689
|
+
* already classified failure only gains a [resource] it did not name.
|
|
1690
|
+
*/
|
|
1691
|
+
private fun classifyUpdateFailure(
|
|
1692
|
+
error: Exception,
|
|
1693
|
+
stage: UpdateFailureStage,
|
|
1694
|
+
resource: UpdateFailureResource?,
|
|
1695
|
+
): Exception =
|
|
1696
|
+
when (error) {
|
|
1697
|
+
is StaleReleaseSelectionException, is CancellationException -> {
|
|
1698
|
+
error
|
|
1699
|
+
}
|
|
1557
1700
|
|
|
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,
|
|
1701
|
+
is HotUpdaterException -> {
|
|
1702
|
+
error.withResource(resource)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
else -> {
|
|
1706
|
+
HotUpdaterException.unexpected(
|
|
1707
|
+
error,
|
|
1708
|
+
UpdateFailure.ofUnexpectedError(error, stage).copy(resource = resource),
|
|
1580
1709
|
)
|
|
1581
1710
|
}
|
|
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
1711
|
}
|
|
1588
1712
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
if (
|
|
1592
|
-
|
|
1713
|
+
/** The rejection for a download that returned [error]. */
|
|
1714
|
+
private fun downloadError(error: Exception): HotUpdaterException =
|
|
1715
|
+
if (error is IncompleteDownloadException) {
|
|
1716
|
+
HotUpdaterException.incompleteDownload(error.expectedSize, error.actualSize)
|
|
1717
|
+
} else {
|
|
1718
|
+
HotUpdaterException.downloadFailed(
|
|
1719
|
+
error,
|
|
1720
|
+
UpdateFailure.ofDownloadError(error) { NetworkState.hasNoActiveNetwork(context) },
|
|
1721
|
+
)
|
|
1593
1722
|
}
|
|
1594
|
-
tempDir.mkdirs()
|
|
1595
1723
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1724
|
+
private suspend fun downloadManifestAsset(
|
|
1725
|
+
assetPath: String,
|
|
1726
|
+
expectedAsset: ParsedManifestAsset,
|
|
1727
|
+
changedAsset: ChangedAssetDescriptor,
|
|
1728
|
+
currentBundleId: String?,
|
|
1729
|
+
activeBundleDir: File?,
|
|
1730
|
+
targetFile: File,
|
|
1731
|
+
tempDir: File,
|
|
1732
|
+
progressFile: DiffProgressFileSnapshot,
|
|
1733
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1734
|
+
): FileDelivery {
|
|
1735
|
+
val expectedHash = expectedAsset.fileHash
|
|
1736
|
+
val diffFiles = mutableListOf(progressFile)
|
|
1737
|
+
val patchOutcome =
|
|
1738
|
+
applyPatchAssetIfPossible(
|
|
1739
|
+
assetPath = assetPath,
|
|
1740
|
+
changedAsset = changedAsset,
|
|
1741
|
+
currentBundleId = currentBundleId,
|
|
1742
|
+
activeBundleDir = activeBundleDir,
|
|
1743
|
+
targetFile = targetFile,
|
|
1744
|
+
expectedHash = expectedHash,
|
|
1745
|
+
tempDir = tempDir,
|
|
1746
|
+
diffFiles = diffFiles,
|
|
1747
|
+
progressCallback = progressCallback,
|
|
1748
|
+
)
|
|
1749
|
+
if (patchOutcome == PatchOutcome.APPLIED) {
|
|
1750
|
+
verifyManifestAssetFileOrThrow(
|
|
1751
|
+
targetFile,
|
|
1752
|
+
expectedAsset,
|
|
1753
|
+
hashMismatch = PATCH_FAILURE,
|
|
1754
|
+
)
|
|
1755
|
+
updateDiffProgressFile(
|
|
1756
|
+
files = diffFiles,
|
|
1757
|
+
assetPath = assetPath,
|
|
1758
|
+
status = "downloaded",
|
|
1759
|
+
progress = 1.0,
|
|
1760
|
+
)
|
|
1761
|
+
emitDiffProgress(
|
|
1762
|
+
progressCallback = progressCallback,
|
|
1763
|
+
phase =
|
|
1764
|
+
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1765
|
+
"finalizing"
|
|
1766
|
+
} else {
|
|
1767
|
+
"downloading"
|
|
1768
|
+
},
|
|
1769
|
+
files = diffFiles,
|
|
1770
|
+
)
|
|
1771
|
+
return FileDelivery.PATCHED
|
|
1772
|
+
}
|
|
1598
1773
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1774
|
+
val changedAssetFileUrl =
|
|
1775
|
+
changedAsset.fileUrl
|
|
1776
|
+
?: run {
|
|
1777
|
+
updateDiffProgressFile(
|
|
1778
|
+
files = diffFiles,
|
|
1779
|
+
assetPath = assetPath,
|
|
1780
|
+
status = "failed",
|
|
1781
|
+
progress = 0.0,
|
|
1782
|
+
)
|
|
1783
|
+
emitDiffProgress(
|
|
1784
|
+
progressCallback = progressCallback,
|
|
1785
|
+
phase = "downloading",
|
|
1786
|
+
files = diffFiles,
|
|
1787
|
+
)
|
|
1788
|
+
throw HotUpdaterException.downloadFailed(
|
|
1789
|
+
IllegalStateException("Changed asset fileUrl missing and patch could not be applied: $assetPath"),
|
|
1790
|
+
PATCH_FAILURE,
|
|
1791
|
+
)
|
|
1605
1792
|
}
|
|
1606
|
-
val tempBundleFile = File(tempDir, bundleFileName)
|
|
1607
1793
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1794
|
+
val downloadPath = downloadPathForChangedAsset(assetPath, changedAsset)
|
|
1795
|
+
val downloadedAssetFile =
|
|
1796
|
+
if (changedAsset.fileCompression == "br") {
|
|
1797
|
+
compressedTempFile(tempDir, assetPath, "br")
|
|
1798
|
+
} else {
|
|
1799
|
+
targetFile
|
|
1800
|
+
}
|
|
1611
1801
|
|
|
1612
|
-
|
|
1802
|
+
when (
|
|
1803
|
+
val assetDownloadResult =
|
|
1613
1804
|
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
|
-
},
|
|
1805
|
+
URL(changedAssetFileUrl),
|
|
1806
|
+
downloadedAssetFile,
|
|
1638
1807
|
) { downloadProgress ->
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
downloadProgress.
|
|
1644
|
-
|
|
1808
|
+
updateDiffProgressFile(
|
|
1809
|
+
files = diffFiles,
|
|
1810
|
+
assetPath = assetPath,
|
|
1811
|
+
status = "downloading",
|
|
1812
|
+
progress = downloadProgress.progress,
|
|
1813
|
+
downloadPath = downloadPath,
|
|
1814
|
+
downloadedBytes = downloadProgress.downloadedBytes,
|
|
1815
|
+
totalBytes = downloadProgress.totalBytes,
|
|
1816
|
+
)
|
|
1817
|
+
emitDiffProgress(
|
|
1818
|
+
progressCallback = progressCallback,
|
|
1819
|
+
phase = "downloading",
|
|
1820
|
+
files = diffFiles,
|
|
1645
1821
|
)
|
|
1646
1822
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1823
|
+
) {
|
|
1824
|
+
is DownloadResult.Error -> {
|
|
1825
|
+
updateDiffProgressFile(
|
|
1826
|
+
files = diffFiles,
|
|
1827
|
+
assetPath = assetPath,
|
|
1828
|
+
status = "failed",
|
|
1829
|
+
progress =
|
|
1830
|
+
diffFiles
|
|
1831
|
+
.firstOrNull { it.path == assetPath }
|
|
1832
|
+
?.progress ?: 0.0,
|
|
1833
|
+
)
|
|
1834
|
+
emitDiffProgress(
|
|
1835
|
+
progressCallback = progressCallback,
|
|
1836
|
+
phase = "downloading",
|
|
1837
|
+
files = diffFiles,
|
|
1838
|
+
)
|
|
1839
|
+
throw downloadError(assetDownloadResult.exception)
|
|
1653
1840
|
}
|
|
1654
1841
|
|
|
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,
|
|
1842
|
+
is DownloadResult.Success -> {
|
|
1843
|
+
try {
|
|
1844
|
+
if (changedAsset.fileCompression == "br") {
|
|
1845
|
+
decompressBrotliFile(
|
|
1846
|
+
assetDownloadResult.file,
|
|
1847
|
+
targetFile,
|
|
1666
1848
|
)
|
|
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
1849
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
)
|
|
1704
|
-
}
|
|
1705
|
-
) {
|
|
1706
|
-
Log.d("BundleStorage", "Failed to extract archive into tmpDir.")
|
|
1707
|
-
tempDir.deleteRecursively()
|
|
1708
|
-
tmpDir.deleteRecursively()
|
|
1709
|
-
throw HotUpdaterException.extractionFormatError()
|
|
1850
|
+
verifyManifestAssetFileOrThrow(
|
|
1851
|
+
targetFile,
|
|
1852
|
+
expectedAsset,
|
|
1853
|
+
)
|
|
1854
|
+
} catch (e: Exception) {
|
|
1855
|
+
updateDiffProgressFile(
|
|
1856
|
+
files = diffFiles,
|
|
1857
|
+
assetPath = assetPath,
|
|
1858
|
+
status = "failed",
|
|
1859
|
+
progress = 1.0,
|
|
1860
|
+
)
|
|
1861
|
+
emitDiffProgress(
|
|
1862
|
+
progressCallback = progressCallback,
|
|
1863
|
+
phase = "downloading",
|
|
1864
|
+
files = diffFiles,
|
|
1865
|
+
)
|
|
1866
|
+
if (e is HotUpdaterException) {
|
|
1867
|
+
throw e
|
|
1710
1868
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1869
|
+
// The bytes arrived; what failed is reading them back.
|
|
1870
|
+
throw HotUpdaterException.downloadFailed(
|
|
1871
|
+
e,
|
|
1872
|
+
UpdateFailure.ofUnexpectedError(e, UpdateFailureStage.DOWNLOAD),
|
|
1873
|
+
)
|
|
1874
|
+
} finally {
|
|
1875
|
+
if (changedAsset.fileCompression == "br") {
|
|
1876
|
+
assetDownloadResult.file.delete()
|
|
1719
1877
|
}
|
|
1878
|
+
}
|
|
1879
|
+
updateDiffProgressFile(
|
|
1880
|
+
files = diffFiles,
|
|
1881
|
+
assetPath = assetPath,
|
|
1882
|
+
status = "downloaded",
|
|
1883
|
+
progress = 1.0,
|
|
1884
|
+
)
|
|
1885
|
+
emitDiffProgress(
|
|
1886
|
+
progressCallback = progressCallback,
|
|
1887
|
+
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
1888
|
+
files = diffFiles,
|
|
1889
|
+
)
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return if (patchOutcome == PatchOutcome.FAILED) {
|
|
1893
|
+
FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE
|
|
1894
|
+
} else {
|
|
1895
|
+
FileDelivery.DOWNLOADED
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1720
1898
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1899
|
+
private fun pruneStagingDirectory(
|
|
1900
|
+
directory: File,
|
|
1901
|
+
assets: Set<String>,
|
|
1902
|
+
prefix: String = "",
|
|
1903
|
+
) {
|
|
1904
|
+
directory.listFiles()?.forEach { file ->
|
|
1905
|
+
val relativePath = prefix + file.name
|
|
1906
|
+
if (file.canonicalFile != File(directory.canonicalFile, file.name)) {
|
|
1907
|
+
if (!file.delete()) {
|
|
1908
|
+
throw HotUpdaterException.storageFailed("Cannot remove staging link: $relativePath")
|
|
1909
|
+
}
|
|
1910
|
+
} else if (file.isDirectory && assets.any { it.startsWith("$relativePath/") }) {
|
|
1911
|
+
pruneStagingDirectory(file, assets, "$relativePath/")
|
|
1912
|
+
} else if (!file.isFile || relativePath !in assets) {
|
|
1913
|
+
if (!file.deleteRecursively()) {
|
|
1914
|
+
throw HotUpdaterException.storageFailed("Cannot remove stale staging file: $relativePath")
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1724
1919
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1920
|
+
private fun selectArchive(
|
|
1921
|
+
manifest: ParsedBundleManifest,
|
|
1922
|
+
downloads: Map<String, ChangedAssetDescriptor>,
|
|
1923
|
+
archiveUrl: String?,
|
|
1924
|
+
): ParsedArchive? {
|
|
1925
|
+
if (archiveUrl.isNullOrBlank() || downloads.size < 2) return null
|
|
1926
|
+
val archive = manifest.archive ?: return null
|
|
1927
|
+
if (manifest.assets.values.any { it.byteSize == null }) return null
|
|
1928
|
+
|
|
1929
|
+
var individualBytes = 0L
|
|
1930
|
+
for ((assetPath, descriptor) in downloads) {
|
|
1931
|
+
val originalBytes = manifest.assets.getValue(assetPath).downloadByteSize ?: return null
|
|
1932
|
+
val expectedBytes =
|
|
1933
|
+
descriptor.patch?.let { patch ->
|
|
1934
|
+
val patchBytes = patch.byteSize?.takeIf { it in 0..MAX_SAFE_INTEGER } ?: return null
|
|
1935
|
+
minOf(originalBytes, patchBytes)
|
|
1936
|
+
} ?: originalBytes
|
|
1937
|
+
if (individualBytes > MAX_SAFE_INTEGER - expectedBytes) return null
|
|
1938
|
+
individualBytes += expectedBytes
|
|
1939
|
+
}
|
|
1940
|
+
var logicalBytes = 0L
|
|
1941
|
+
for (asset in manifest.assets.values) {
|
|
1942
|
+
val byteSize = asset.byteSize ?: return null
|
|
1943
|
+
if (logicalBytes > MAX_SAFE_INTEGER - byteSize) return null
|
|
1944
|
+
logicalBytes += byteSize
|
|
1945
|
+
}
|
|
1946
|
+
if (archive.tarByteSize < logicalBytes) return null
|
|
1947
|
+
val tarFramingBytes = archive.tarByteSize - logicalBytes
|
|
1948
|
+
if (individualBytes > MAX_SAFE_INTEGER - tarFramingBytes) return null
|
|
1949
|
+
val framedByteLimit = individualBytes + tarFramingBytes
|
|
1950
|
+
|
|
1951
|
+
if (archive.downloadByteSize <= individualBytes) return archive
|
|
1952
|
+
if (downloads.size != manifest.assets.size || downloads.values.any { it.patch != null }) return null
|
|
1953
|
+
return archive.takeIf { it.downloadByteSize <= framedByteLimit }
|
|
1954
|
+
}
|
|
1729
1955
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1956
|
+
private suspend fun tryInstallArchive(
|
|
1957
|
+
archiveUrl: String,
|
|
1958
|
+
archive: ParsedArchive,
|
|
1959
|
+
manifest: ParsedBundleManifest,
|
|
1960
|
+
tempDir: File,
|
|
1961
|
+
bundleStoreDir: File,
|
|
1962
|
+
tmpDir: File,
|
|
1963
|
+
bundleId: String,
|
|
1964
|
+
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1965
|
+
): MutableList<DiffProgressFileSnapshot>? {
|
|
1966
|
+
val archiveScratch = File(tempDir, "archive")
|
|
1967
|
+
val archiveFile = File(archiveScratch, "bundle.tar.br")
|
|
1968
|
+
val extractedDir = File(bundleStoreDir, "$bundleId.archive.tmp")
|
|
1969
|
+
val localBackupDir = File(bundleStoreDir, "$bundleId.local.tmp")
|
|
1970
|
+
archiveScratch.deleteRecursively()
|
|
1971
|
+
extractedDir.deleteRecursively()
|
|
1972
|
+
localBackupDir.deleteRecursively()
|
|
1973
|
+
archiveScratch.mkdirs()
|
|
1974
|
+
|
|
1975
|
+
val progressFiles =
|
|
1976
|
+
mutableListOf(
|
|
1977
|
+
DiffProgressFileSnapshot(
|
|
1978
|
+
path = "bundle.tar.br",
|
|
1979
|
+
downloadPath = "bundle.tar.br",
|
|
1980
|
+
status = "pending",
|
|
1981
|
+
progress = 0.0,
|
|
1982
|
+
order = 0,
|
|
1983
|
+
),
|
|
1984
|
+
)
|
|
1985
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1750
1986
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1987
|
+
return try {
|
|
1988
|
+
when (
|
|
1989
|
+
val result =
|
|
1990
|
+
downloadService.downloadFileOnce(
|
|
1991
|
+
URL(archiveUrl),
|
|
1992
|
+
archiveFile,
|
|
1993
|
+
) { progress ->
|
|
1994
|
+
updateDiffProgressFile(
|
|
1995
|
+
files = progressFiles,
|
|
1996
|
+
assetPath = "bundle.tar.br",
|
|
1997
|
+
status = "downloading",
|
|
1998
|
+
progress = progress.progress,
|
|
1999
|
+
downloadPath = "bundle.tar.br",
|
|
2000
|
+
downloadedBytes = progress.downloadedBytes,
|
|
2001
|
+
totalBytes = progress.totalBytes,
|
|
2002
|
+
)
|
|
2003
|
+
emitDiffProgress(progressCallback, "downloading", progressFiles)
|
|
1758
2004
|
}
|
|
2005
|
+
) {
|
|
2006
|
+
is DownloadResult.Error -> throw result.exception
|
|
2007
|
+
is DownloadResult.Success -> Unit
|
|
2008
|
+
}
|
|
1759
2009
|
|
|
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()
|
|
2010
|
+
if (archiveFile.length() != archive.downloadByteSize) {
|
|
2011
|
+
throw IllegalStateException("Archive compressed size mismatch")
|
|
2012
|
+
}
|
|
2013
|
+
if (!HashUtils.verifyHash(archiveFile, archive.downloadFileHash)) {
|
|
2014
|
+
throw IllegalStateException("Archive compressed hash mismatch")
|
|
2015
|
+
}
|
|
1778
2016
|
|
|
1779
|
-
|
|
1780
|
-
|
|
2017
|
+
TarBrArchiveExtractor.extract(
|
|
2018
|
+
archiveFile = archiveFile,
|
|
2019
|
+
destination = extractedDir,
|
|
2020
|
+
expectedTarByteSize = archive.tarByteSize,
|
|
2021
|
+
expectedFiles = manifest.assets.mapValues { it.value.byteSize!! },
|
|
2022
|
+
)
|
|
2023
|
+
manifest.assets.forEach { (assetPath, expectedAsset) ->
|
|
2024
|
+
val extractedFile =
|
|
2025
|
+
RelativePathResolver.resolveInside(extractedDir, assetPath)
|
|
2026
|
+
?: throw IllegalStateException("Archive output path is invalid")
|
|
2027
|
+
verifyManifestAssetFileOrThrow(
|
|
2028
|
+
extractedFile,
|
|
2029
|
+
expectedAsset,
|
|
2030
|
+
hashMismatch = UpdateFailure.install(UpdateFailureReason.EXTRACT),
|
|
2031
|
+
)
|
|
2032
|
+
}
|
|
1781
2033
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2034
|
+
if (!tmpDir.renameTo(localBackupDir)) {
|
|
2035
|
+
throw IllegalStateException("Cannot preserve locally prepared staging directory")
|
|
2036
|
+
}
|
|
2037
|
+
if (!extractedDir.renameTo(tmpDir)) {
|
|
2038
|
+
check(localBackupDir.renameTo(tmpDir)) { "Cannot restore locally prepared staging directory" }
|
|
2039
|
+
throw IllegalStateException("Cannot promote verified archive extraction")
|
|
1785
2040
|
}
|
|
2041
|
+
localBackupDir.deleteRecursively()
|
|
2042
|
+
|
|
2043
|
+
updateDiffProgressFile(
|
|
2044
|
+
files = progressFiles,
|
|
2045
|
+
assetPath = "bundle.tar.br",
|
|
2046
|
+
status = "downloaded",
|
|
2047
|
+
progress = 1.0,
|
|
2048
|
+
downloadedBytes = archive.downloadByteSize,
|
|
2049
|
+
totalBytes = archive.downloadByteSize,
|
|
2050
|
+
)
|
|
2051
|
+
emitDiffProgress(progressCallback, "finalizing", progressFiles)
|
|
2052
|
+
progressFiles
|
|
2053
|
+
} catch (error: Exception) {
|
|
2054
|
+
Log.w(TAG, "Archive optimization failed; using individual files: ${error.message}")
|
|
2055
|
+
val canUseIndividualPlan =
|
|
2056
|
+
tmpDir.exists() ||
|
|
2057
|
+
(localBackupDir.exists() && localBackupDir.renameTo(tmpDir))
|
|
2058
|
+
if (!canUseIndividualPlan) {
|
|
2059
|
+
// Only the staging directory swap can leave tmpDir missing.
|
|
2060
|
+
throw HotUpdaterException.unexpected(error, UpdateFailure.install(UpdateFailureReason.STORAGE))
|
|
2061
|
+
}
|
|
2062
|
+
extractedDir.deleteRecursively()
|
|
2063
|
+
null
|
|
2064
|
+
} finally {
|
|
2065
|
+
archiveScratch.deleteRecursively()
|
|
2066
|
+
extractedDir.deleteRecursively()
|
|
2067
|
+
if (tmpDir.exists()) localBackupDir.deleteRecursively()
|
|
1786
2068
|
}
|
|
1787
2069
|
}
|
|
1788
2070
|
|
|
1789
|
-
/**
|
|
1790
|
-
* Removes old bundles except for the specified bundle IDs, and any leftover .tmp directories
|
|
1791
|
-
*/
|
|
1792
2071
|
private suspend fun updateBundleFromManifest(
|
|
1793
2072
|
bundleId: String,
|
|
1794
2073
|
manifestUrl: String,
|
|
1795
2074
|
manifestFileHash: String,
|
|
1796
2075
|
changedAssets: Map<String, ChangedAssetDescriptor>,
|
|
2076
|
+
archiveUrl: String?,
|
|
1797
2077
|
bundleStoreDir: File,
|
|
1798
2078
|
finalBundleDir: File,
|
|
1799
2079
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
1800
|
-
) {
|
|
2080
|
+
): UpdateBundleResult {
|
|
1801
2081
|
val activeBundleDir = getActiveBundleDir()
|
|
1802
2082
|
val currentBundleId = getBundleId()
|
|
1803
|
-
val currentManifest =
|
|
1804
|
-
getActiveBundleMetadataSnapshot()?.manifest?.let(::parseBundleManifestFromMap)
|
|
1805
2083
|
val baseDir =
|
|
1806
2084
|
fileSystem.getInternalFilesDir() ?: bundleStoreDir.parentFile ?: bundleStoreDir
|
|
1807
2085
|
val tempDir = File(baseDir, "bundle-manifest-temp")
|
|
@@ -1812,345 +2090,284 @@ class BundleFileStorageService(
|
|
|
1812
2090
|
}
|
|
1813
2091
|
tempDir.mkdirs()
|
|
1814
2092
|
|
|
1815
|
-
|
|
2093
|
+
var diffFiles = mutableListOf<DiffProgressFileSnapshot>()
|
|
1816
2094
|
|
|
1817
2095
|
try {
|
|
1818
2096
|
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
|
-
)
|
|
2097
|
+
val targetManifest =
|
|
2098
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.MANIFEST) {
|
|
1930
2099
|
emitDiffProgress(
|
|
1931
2100
|
progressCallback = progressCallback,
|
|
1932
|
-
phase = "
|
|
2101
|
+
phase = "manifest",
|
|
1933
2102
|
files = diffFiles,
|
|
2103
|
+
manifestProgress = 0.0,
|
|
1934
2104
|
)
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (patched) {
|
|
1953
|
-
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
1954
|
-
updateDiffProgressFile(
|
|
1955
|
-
files = diffFiles,
|
|
1956
|
-
assetPath = assetPath,
|
|
1957
|
-
status = "downloaded",
|
|
1958
|
-
progress = 1.0,
|
|
1959
|
-
)
|
|
1960
|
-
emitDiffProgress(
|
|
1961
|
-
progressCallback = progressCallback,
|
|
1962
|
-
phase =
|
|
1963
|
-
if (diffFiles.all { it.status == "downloaded" }) {
|
|
1964
|
-
"finalizing"
|
|
1965
|
-
} else {
|
|
1966
|
-
"downloading"
|
|
1967
|
-
},
|
|
1968
|
-
files = diffFiles,
|
|
1969
|
-
)
|
|
1970
|
-
return@forEachIndexed
|
|
1971
|
-
}
|
|
2105
|
+
when (
|
|
2106
|
+
val manifestDownloadResult =
|
|
2107
|
+
downloadService.downloadFile(
|
|
2108
|
+
URL(manifestUrl),
|
|
2109
|
+
manifestFile,
|
|
2110
|
+
) { downloadProgress ->
|
|
2111
|
+
emitDiffProgress(
|
|
2112
|
+
progressCallback = progressCallback,
|
|
2113
|
+
phase = "manifest",
|
|
2114
|
+
files = diffFiles,
|
|
2115
|
+
manifestProgress = downloadProgress.progress,
|
|
2116
|
+
)
|
|
2117
|
+
}
|
|
2118
|
+
) {
|
|
2119
|
+
is DownloadResult.Error -> {
|
|
2120
|
+
throw downloadError(manifestDownloadResult.exception)
|
|
2121
|
+
}
|
|
1972
2122
|
|
|
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
|
-
)
|
|
2123
|
+
is DownloadResult.Success -> {
|
|
2124
|
+
Unit
|
|
1990
2125
|
}
|
|
2126
|
+
}
|
|
1991
2127
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
} else {
|
|
1997
|
-
targetFile
|
|
2128
|
+
try {
|
|
2129
|
+
SignatureVerifier.verifyBundle(context, manifestFile, manifestFileHash)
|
|
2130
|
+
} catch (e: SignatureVerificationException) {
|
|
2131
|
+
throw HotUpdaterException.signatureVerificationFailed(e)
|
|
1998
2132
|
}
|
|
1999
2133
|
|
|
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
|
-
)
|
|
2134
|
+
val manifest = parseBundleManifestFromFile(manifestFile) ?: throw HotUpdaterException.invalidBundle()
|
|
2135
|
+
if (manifest.bundleId != bundleId) {
|
|
2136
|
+
throw HotUpdaterException.invalidBundle()
|
|
2137
|
+
}
|
|
2138
|
+
// Reuse never bypasses the complete artifact contract.
|
|
2139
|
+
if (manifest.assets.keys != changedAssets.keys) throw HotUpdaterException.invalidBundle()
|
|
2140
|
+
manifest.assets.forEach { (path, asset) ->
|
|
2141
|
+
val descriptor = changedAssets.getValue(path)
|
|
2142
|
+
if (RelativePathResolver.normalizeRelativePath(path) != path ||
|
|
2143
|
+
path == "manifest.json" ||
|
|
2144
|
+
descriptor.fileUrl.isNullOrBlank() ||
|
|
2145
|
+
!descriptor.fileHash.equals(asset.fileHash, ignoreCase = true) ||
|
|
2146
|
+
RelativePathResolver.resolveInside(tmpDir, path) == null
|
|
2147
|
+
) {
|
|
2148
|
+
throw HotUpdaterException.invalidBundle()
|
|
2020
2149
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2150
|
+
}
|
|
2151
|
+
manifest
|
|
2152
|
+
}
|
|
2153
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
2154
|
+
if (!tmpDir.isDirectory && !tmpDir.mkdirs()) throw HotUpdaterException.directoryCreationFailed()
|
|
2155
|
+
pruneStagingDirectory(tmpDir, targetManifest.assets.keys)
|
|
2156
|
+
}
|
|
2157
|
+
val downloads =
|
|
2158
|
+
updateStep(UpdateFailureStage.INSTALL, UpdateFailureResource.FILE) {
|
|
2159
|
+
val targetEntries = targetManifest.assets.entries.sortedBy { it.key }
|
|
2160
|
+
val localDirectories = listOfNotNull(activeBundleDir, finalBundleDir).distinct().filter { it.isDirectory }
|
|
2161
|
+
val downloads = mutableMapOf<String, ChangedAssetDescriptor>()
|
|
2162
|
+
targetEntries.forEachIndexed { index, (assetPath, expectedAsset) ->
|
|
2163
|
+
progressCallback(
|
|
2164
|
+
UpdateProgressPayload(
|
|
2165
|
+
progress = 0.15 + 0.05 * index / targetEntries.size.coerceAtLeast(1),
|
|
2166
|
+
artifactType = "diff",
|
|
2167
|
+
details = DiffProgressDetails(totalFilesCount = 0, completedFilesCount = 0),
|
|
2168
|
+
),
|
|
2036
2169
|
)
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
incompleteEx.expectedSize,
|
|
2042
|
-
incompleteEx.actualSize,
|
|
2043
|
-
)
|
|
2170
|
+
val targetFile = RelativePathResolver.resolveInside(tmpDir, assetPath) ?: throw HotUpdaterException.invalidBundle()
|
|
2171
|
+
targetFile.parentFile?.mkdirs()
|
|
2172
|
+
if (targetFile.isFile && runCatching { verifyManifestAssetFileOrThrow(targetFile, expectedAsset) }.isSuccess) {
|
|
2173
|
+
return@forEachIndexed
|
|
2044
2174
|
}
|
|
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()
|
|
2175
|
+
if (targetFile.exists()) targetFile.delete()
|
|
2176
|
+
var reused = false
|
|
2177
|
+
for (directory in localDirectories) {
|
|
2178
|
+
val sourceFile = RelativePathResolver.resolveInside(directory, assetPath) ?: continue
|
|
2179
|
+
val sourceMatches =
|
|
2180
|
+
sourceFile.isFile && runCatching { verifyManifestAssetFileOrThrow(sourceFile, expectedAsset) }.isSuccess
|
|
2181
|
+
if (!sourceMatches) continue
|
|
2182
|
+
if (runCatching {
|
|
2183
|
+
copyBundleFile(sourceFile, targetFile)
|
|
2184
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2185
|
+
}.isSuccess
|
|
2186
|
+
) {
|
|
2187
|
+
reused = true
|
|
2188
|
+
break
|
|
2079
2189
|
}
|
|
2190
|
+
targetFile.delete()
|
|
2080
2191
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
)
|
|
2087
|
-
|
|
2192
|
+
if (reused) return@forEachIndexed
|
|
2193
|
+
if (builtInAssetResolver.copyIfMatches(assetPath, expectedAsset.fileHash, targetFile)) {
|
|
2194
|
+
verifyManifestAssetFileOrThrow(targetFile, expectedAsset)
|
|
2195
|
+
return@forEachIndexed
|
|
2196
|
+
}
|
|
2197
|
+
downloads[assetPath] = changedAssets.getValue(assetPath)
|
|
2198
|
+
}
|
|
2199
|
+
downloads
|
|
2200
|
+
}
|
|
2201
|
+
val selectedArchive = selectArchive(targetManifest, downloads, archiveUrl)
|
|
2202
|
+
val archiveProgress =
|
|
2203
|
+
if (selectedArchive != null) {
|
|
2204
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.ARCHIVE) {
|
|
2205
|
+
tryInstallArchive(
|
|
2206
|
+
archiveUrl = archiveUrl!!,
|
|
2207
|
+
archive = selectedArchive,
|
|
2208
|
+
manifest = targetManifest,
|
|
2209
|
+
tempDir = tempDir,
|
|
2210
|
+
bundleStoreDir = bundleStoreDir,
|
|
2211
|
+
tmpDir = tmpDir,
|
|
2212
|
+
bundleId = bundleId,
|
|
2088
2213
|
progressCallback = progressCallback,
|
|
2089
|
-
phase = if (diffFiles.all { it.status == "downloaded" }) "finalizing" else "downloading",
|
|
2090
|
-
files = diffFiles,
|
|
2091
2214
|
)
|
|
2092
2215
|
}
|
|
2216
|
+
} else {
|
|
2217
|
+
null
|
|
2093
2218
|
}
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
emitDiffProgress(
|
|
2097
|
-
progressCallback = progressCallback,
|
|
2098
|
-
phase = "finalizing",
|
|
2099
|
-
files = diffFiles,
|
|
2100
|
-
)
|
|
2101
2219
|
|
|
2102
|
-
|
|
2220
|
+
val fileDeliveries =
|
|
2221
|
+
if (archiveProgress != null) {
|
|
2222
|
+
diffFiles = archiveProgress
|
|
2223
|
+
emptyList()
|
|
2224
|
+
} else {
|
|
2225
|
+
diffFiles = createDiffProgressFiles(downloads)
|
|
2226
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2227
|
+
val progressLock = Any()
|
|
2228
|
+
coroutineScope {
|
|
2229
|
+
val permits = Semaphore(4)
|
|
2230
|
+
diffFiles
|
|
2231
|
+
.toList()
|
|
2232
|
+
.map { progressFile ->
|
|
2233
|
+
async {
|
|
2234
|
+
permits.withPermit {
|
|
2235
|
+
updateStep(UpdateFailureStage.DOWNLOAD, UpdateFailureResource.FILE) {
|
|
2236
|
+
val path = progressFile.path
|
|
2237
|
+
val workerTemp =
|
|
2238
|
+
File(tempDir, progressFile.order.toString()).apply { mkdirs() }
|
|
2239
|
+
downloadManifestAsset(
|
|
2240
|
+
assetPath = path,
|
|
2241
|
+
expectedAsset = targetManifest.assets.getValue(path),
|
|
2242
|
+
changedAsset = downloads.getValue(path),
|
|
2243
|
+
currentBundleId = currentBundleId,
|
|
2244
|
+
activeBundleDir = activeBundleDir,
|
|
2245
|
+
targetFile =
|
|
2246
|
+
RelativePathResolver.resolveInside(tmpDir, path)
|
|
2247
|
+
?: throw HotUpdaterException.invalidBundle(),
|
|
2248
|
+
tempDir = workerTemp,
|
|
2249
|
+
progressFile = progressFile,
|
|
2250
|
+
progressCallback = { payload ->
|
|
2251
|
+
synchronized(progressLock) {
|
|
2252
|
+
payload.details?.files?.firstOrNull()?.let { snapshot ->
|
|
2253
|
+
val index = diffFiles.indexOfFirst { it.path == path }
|
|
2254
|
+
diffFiles[index] = snapshot
|
|
2255
|
+
}
|
|
2256
|
+
emitDiffProgress(progressCallback, "downloading", diffFiles)
|
|
2257
|
+
}
|
|
2258
|
+
},
|
|
2259
|
+
)
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}.awaitAll()
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2103
2266
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2267
|
+
updateStep(UpdateFailureStage.INSTALL, resource = null) {
|
|
2268
|
+
emitDiffProgress(
|
|
2269
|
+
progressCallback = progressCallback,
|
|
2270
|
+
phase = "finalizing",
|
|
2271
|
+
files = diffFiles,
|
|
2272
|
+
)
|
|
2108
2273
|
|
|
2109
|
-
|
|
2110
|
-
finalBundleDir.deleteRecursively()
|
|
2111
|
-
}
|
|
2274
|
+
manifestFile.copyTo(File(tmpDir, "manifest.json"), overwrite = true)
|
|
2112
2275
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
throw HotUpdaterException.moveOperationFailed()
|
|
2118
|
-
}
|
|
2119
|
-
tmpDir.deleteRecursively()
|
|
2276
|
+
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
2277
|
+
if (extractedIndex == null) {
|
|
2278
|
+
// The manifest lists no platform bundle.
|
|
2279
|
+
throw HotUpdaterException.invalidBundle().withResource(UpdateFailureResource.MANIFEST)
|
|
2120
2280
|
}
|
|
2121
|
-
}
|
|
2122
2281
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2282
|
+
val backupDir = installBackupDir(bundleId)
|
|
2283
|
+
val updatedMetadata =
|
|
2284
|
+
synchronized(releaseStateLock) {
|
|
2285
|
+
if (backupDir.exists()) {
|
|
2286
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
val hadExistingFinal = finalBundleDir.exists()
|
|
2290
|
+
var promoted = false
|
|
2291
|
+
val previousBundleUrl = preferences.getItem("HotUpdaterBundleURL")
|
|
2292
|
+
try {
|
|
2293
|
+
if (hadExistingFinal && !directoryRenamer(finalBundleDir, backupDir)) {
|
|
2294
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2295
|
+
}
|
|
2296
|
+
if (!directoryRenamer(tmpDir, finalBundleDir)) {
|
|
2297
|
+
if (hadExistingFinal) {
|
|
2298
|
+
directoryRenamer(backupDir, finalBundleDir)
|
|
2299
|
+
}
|
|
2300
|
+
throw HotUpdaterException.moveOperationFailed()
|
|
2301
|
+
}
|
|
2302
|
+
promoted = true
|
|
2128
2303
|
|
|
2129
|
-
|
|
2304
|
+
val finalIndexFile =
|
|
2305
|
+
resolveBundleFile(finalBundleDir, bundleId)
|
|
2306
|
+
?: throw HotUpdaterException.invalidBundle()
|
|
2307
|
+
if (!isCompleteBundleDirectory(finalBundleDir, bundleId)) {
|
|
2308
|
+
throw HotUpdaterException.invalidBundle()
|
|
2309
|
+
}
|
|
2310
|
+
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
2130
2311
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2312
|
+
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
2313
|
+
val nextMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
2314
|
+
if (!setBundleURL(finalIndexFile.absolutePath)) {
|
|
2315
|
+
throw HotUpdaterException.storageFailed("Failed to persist bundle URL")
|
|
2316
|
+
}
|
|
2317
|
+
if (!saveMetadata(nextMetadata)) {
|
|
2318
|
+
throw HotUpdaterException.storageFailed("Failed to persist bundle metadata")
|
|
2319
|
+
}
|
|
2135
2320
|
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2321
|
+
if (backupDir.exists() && !backupDir.deleteRecursively()) {
|
|
2322
|
+
Log.w(TAG, "Failed to remove committed install backup: ${backupDir.absolutePath}")
|
|
2323
|
+
}
|
|
2324
|
+
nextMetadata
|
|
2325
|
+
} catch (e: Exception) {
|
|
2326
|
+
runCatching { preferences.setItem("HotUpdaterBundleURL", previousBundleUrl) }
|
|
2327
|
+
clearActiveBundleMetadataSnapshot()
|
|
2328
|
+
if (promoted && finalBundleDir.exists() && !finalBundleDir.deleteRecursively()) {
|
|
2329
|
+
Log.e(TAG, "Failed to remove uncommitted bundle: ${finalBundleDir.absolutePath}")
|
|
2330
|
+
}
|
|
2331
|
+
if (hadExistingFinal &&
|
|
2332
|
+
!finalBundleDir.exists() &&
|
|
2333
|
+
backupDir.exists() &&
|
|
2334
|
+
!directoryRenamer(backupDir, finalBundleDir)
|
|
2335
|
+
) {
|
|
2336
|
+
Log.e(TAG, "Failed to restore install backup: ${backupDir.absolutePath}")
|
|
2337
|
+
}
|
|
2338
|
+
throw e
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
tempDir.deleteRecursively()
|
|
2343
|
+
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
2344
|
+
progressCallback(
|
|
2345
|
+
UpdateProgressPayload(
|
|
2346
|
+
progress = 1.0,
|
|
2347
|
+
artifactType = "diff",
|
|
2348
|
+
details =
|
|
2349
|
+
DiffProgressDetails(
|
|
2350
|
+
totalFilesCount = diffFiles.size,
|
|
2351
|
+
completedFilesCount = diffFiles.count { it.status == "downloaded" },
|
|
2352
|
+
files = diffFiles.toList(),
|
|
2353
|
+
),
|
|
2354
|
+
),
|
|
2355
|
+
)
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
return UpdateBundleResult(
|
|
2359
|
+
delivery =
|
|
2360
|
+
when {
|
|
2361
|
+
archiveProgress != null -> BundleDelivery.ARCHIVE
|
|
2362
|
+
FileDelivery.PATCHED in fileDeliveries -> BundleDelivery.PATCH
|
|
2363
|
+
else -> BundleDelivery.MANIFEST
|
|
2364
|
+
},
|
|
2365
|
+
patchFallback = FileDelivery.DOWNLOADED_AFTER_PATCH_FAILURE in fileDeliveries,
|
|
2149
2366
|
)
|
|
2150
2367
|
} catch (e: Exception) {
|
|
2151
2368
|
tempDir.deleteRecursively()
|
|
2152
|
-
|
|
2153
|
-
throw e
|
|
2369
|
+
// A retry rehashes completed files and rejects partial bytes.
|
|
2370
|
+
throw classifyUpdateFailure(e, UpdateFailureStage.DOWNLOAD, resource = null)
|
|
2154
2371
|
}
|
|
2155
2372
|
}
|
|
2156
2373
|
|
|
@@ -2164,7 +2381,9 @@ class BundleFileStorageService(
|
|
|
2164
2381
|
val bundles =
|
|
2165
2382
|
bundleStoreDir
|
|
2166
2383
|
.listFiles { file ->
|
|
2167
|
-
file.isDirectory &&
|
|
2384
|
+
file.isDirectory &&
|
|
2385
|
+
!file.name.endsWith(".tmp") &&
|
|
2386
|
+
!file.name.endsWith(".install-backup")
|
|
2168
2387
|
}?.toList() ?: return
|
|
2169
2388
|
|
|
2170
2389
|
// Keep only the specified bundle IDs (filter out null values)
|
|
@@ -2287,6 +2506,7 @@ class BundleFileStorageService(
|
|
|
2287
2506
|
pendingUpdateStrategy = null,
|
|
2288
2507
|
pendingTransition = null,
|
|
2289
2508
|
verificationPending = false,
|
|
2509
|
+
launchInProgress = false,
|
|
2290
2510
|
updatedAt = System.currentTimeMillis(),
|
|
2291
2511
|
)
|
|
2292
2512
|
|