@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/proguard-rules.pro +17 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +109 -84
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +98 -74
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
- package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +13 -45
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +124 -69
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +13 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +16 -27
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/fetchUpdateResponse.js +54 -0
- package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
- package/lib/commonjs/httpClient.js +63 -94
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +71 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +78 -60
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +44 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +36 -87
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -26
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +117 -64
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +4 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +14 -26
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/fetchUpdateResponse.js +49 -0
- package/lib/module/fetchUpdateResponse.js.map +1 -0
- package/lib/module/httpClient.js +62 -93
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +58 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +71 -53
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +35 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +32 -82
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/wrap.js +43 -26
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +23 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/commonjs/httpClient.d.ts +4 -33
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +31 -22
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +23 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +16 -4
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +2 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +5 -5
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/module/httpClient.d.ts +4 -33
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +31 -22
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +4 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -7
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/appReady.ts +149 -0
- package/src/checkForUpdate.ts +151 -88
- package/src/clientPlugin.ts +18 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +14 -30
- package/src/fetchUpdateResponse.ts +54 -0
- package/src/httpClient.ts +88 -159
- package/src/index.ts +90 -24
- package/src/native.ts +106 -102
- package/src/pluginHost.ts +45 -0
- package/src/releaseCatalogCache.ts +43 -172
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +41 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +153 -0
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -7
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
- package/lib/commonjs/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -615
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -102
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -302
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -883
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -133
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -76
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -316
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -329
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -614
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -102
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -301
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -882
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -128
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -245
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -195
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -77
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -315
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -330
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -611
- package/src/httpClient.spec.ts +0 -132
- package/src/index.spec.ts +0 -337
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -945
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
- package/src/notifyAppReadyInsights.spec.ts +0 -243
- package/src/notifyAppReadyInsights.test-utils.ts +0 -31
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
- package/src/notifyAppReadyInsights.ts +0 -194
- package/src/releaseCatalogCache.spec.ts +0 -282
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -92
- package/src/store.spec.ts +0 -372
- package/src/wrap.spec.ts +0 -406
|
@@ -9,6 +9,7 @@ import android.os.Looper
|
|
|
9
9
|
import android.os.Process
|
|
10
10
|
import android.os.SystemClock
|
|
11
11
|
import android.util.Log
|
|
12
|
+
import androidx.core.util.AtomicFile
|
|
12
13
|
import com.facebook.react.ReactApplication
|
|
13
14
|
import com.facebook.react.bridge.JSExceptionHandler
|
|
14
15
|
import com.facebook.react.bridge.ReactContext
|
|
@@ -16,6 +17,7 @@ import com.facebook.react.bridge.ReactMarker
|
|
|
16
17
|
import com.facebook.react.bridge.ReactMarkerConstants
|
|
17
18
|
import org.json.JSONObject
|
|
18
19
|
import java.io.File
|
|
20
|
+
import java.io.FileOutputStream
|
|
19
21
|
import java.lang.reflect.Field
|
|
20
22
|
import kotlin.system.exitProcess
|
|
21
23
|
|
|
@@ -28,10 +30,7 @@ internal class HotUpdaterRecoveryManager(
|
|
|
28
30
|
private val crashMarkerFile = File(bundleStoreDir, CRASH_MARKER_FILENAME)
|
|
29
31
|
private val watchdogStateFile = File(bundleStoreDir, WATCHDOG_STATE_FILENAME)
|
|
30
32
|
|
|
31
|
-
private
|
|
32
|
-
private var shouldRollbackOnCrash = false
|
|
33
|
-
private var isMonitoring = false
|
|
34
|
-
private var recoveryRequested = false
|
|
33
|
+
private val launchState = HotUpdaterRecoveryState()
|
|
35
34
|
private var contentAppearedCallback: ((String?) -> Unit)? = null
|
|
36
35
|
private var recoveryRestartCallback: (() -> Boolean)? = null
|
|
37
36
|
|
|
@@ -39,10 +38,7 @@ internal class HotUpdaterRecoveryManager(
|
|
|
39
38
|
Runnable {
|
|
40
39
|
cancelRecoveryWatchdog()
|
|
41
40
|
Log.d(TAG, "Stopping crash monitoring for current launch")
|
|
42
|
-
|
|
43
|
-
recoveryRequested = false
|
|
44
|
-
shouldRollbackOnCrash = false
|
|
45
|
-
currentBundleId = null
|
|
41
|
+
launchState.stop()
|
|
46
42
|
contentAppearedCallback = null
|
|
47
43
|
recoveryRestartCallback = null
|
|
48
44
|
activeManager = null
|
|
@@ -52,7 +48,7 @@ internal class HotUpdaterRecoveryManager(
|
|
|
52
48
|
private val installJsExceptionHooksRunnable =
|
|
53
49
|
object : Runnable {
|
|
54
50
|
override fun run() {
|
|
55
|
-
if (
|
|
51
|
+
if (launchState.snapshot() == null) {
|
|
56
52
|
return
|
|
57
53
|
}
|
|
58
54
|
|
|
@@ -74,7 +70,7 @@ internal class HotUpdaterRecoveryManager(
|
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
return try {
|
|
77
|
-
val recovery = PendingCrashRecovery.
|
|
73
|
+
val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile) ?: return null
|
|
78
74
|
Log.d(
|
|
79
75
|
TAG,
|
|
80
76
|
"Consumed pending crash marker bundleId=${recovery.launchedBundleId} shouldRollback=${recovery.shouldRollback}",
|
|
@@ -96,11 +92,8 @@ internal class HotUpdaterRecoveryManager(
|
|
|
96
92
|
) {
|
|
97
93
|
crashMarkerFile.parentFile?.mkdirs()
|
|
98
94
|
contentAppearedCallback = onContentAppeared
|
|
99
|
-
currentBundleId = bundleId
|
|
100
|
-
shouldRollbackOnCrash = shouldRollback
|
|
101
|
-
isMonitoring = true
|
|
102
|
-
recoveryRequested = false
|
|
103
95
|
recoveryRestartCallback = onRecoveryRestartRequested
|
|
96
|
+
launchState.start(bundleId, shouldRollback)
|
|
104
97
|
activeManager = this
|
|
105
98
|
|
|
106
99
|
ensureExceptionHandlerInstalled()
|
|
@@ -122,17 +115,13 @@ internal class HotUpdaterRecoveryManager(
|
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
private fun handleContentAppeared() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
Log.d(TAG, "First content appeared for bundleId=$currentBundleId")
|
|
118
|
+
val launch = launchState.completeLaunch() ?: return
|
|
119
|
+
Log.d(TAG, "First content appeared for bundleId=${launch.bundleId}")
|
|
130
120
|
ReactMarker.removeListener(contentAppearedListener)
|
|
131
121
|
mainHandler.removeCallbacks(installJsExceptionHooksRunnable)
|
|
132
122
|
mainHandler.removeCallbacks(stopMonitoringRunnable)
|
|
133
|
-
contentAppearedCallback?.invoke(
|
|
134
|
-
|
|
135
|
-
updateNativeLaunchState(currentBundleId, false)
|
|
123
|
+
contentAppearedCallback?.invoke(launch.bundleId)
|
|
124
|
+
updateNativeLaunchState(launch.bundleId, false)
|
|
136
125
|
cancelRecoveryWatchdog()
|
|
137
126
|
mainHandler.postDelayed(stopMonitoringRunnable, MONITORING_GRACE_PERIOD_MS)
|
|
138
127
|
}
|
|
@@ -145,11 +134,10 @@ internal class HotUpdaterRecoveryManager(
|
|
|
145
134
|
previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
146
135
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
147
136
|
val manager = activeManager
|
|
148
|
-
manager?.writeCrashMarker()
|
|
149
|
-
|
|
150
137
|
if (manager?.requestAutomaticRecovery() == true) {
|
|
151
|
-
|
|
152
|
-
|
|
138
|
+
// The separate restart activity kills this process after it starts.
|
|
139
|
+
// A second fatal error must not kill it while that request is in flight.
|
|
140
|
+
return@setDefaultUncaughtExceptionHandler
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
previousExceptionHandler?.uncaughtException(thread, throwable)
|
|
@@ -161,20 +149,21 @@ internal class HotUpdaterRecoveryManager(
|
|
|
161
149
|
exceptionHandlerInstalled = true
|
|
162
150
|
}
|
|
163
151
|
|
|
164
|
-
private fun writeCrashMarker() {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
152
|
+
private fun writeCrashMarker(launch: HotUpdaterRecoveryState.Launch) {
|
|
153
|
+
val atomicFile = AtomicFile(crashMarkerFile)
|
|
154
|
+
var output: FileOutputStream? = null
|
|
169
155
|
try {
|
|
170
156
|
val payload =
|
|
171
157
|
JSONObject().apply {
|
|
172
|
-
put("bundleId",
|
|
173
|
-
put("shouldRollback",
|
|
158
|
+
put("bundleId", launch.bundleId ?: JSONObject.NULL)
|
|
159
|
+
put("shouldRollback", launch.shouldRollback)
|
|
174
160
|
}
|
|
175
161
|
crashMarkerFile.parentFile?.mkdirs()
|
|
176
|
-
|
|
162
|
+
output = atomicFile.startWrite()
|
|
163
|
+
output.write(payload.toString().toByteArray(Charsets.UTF_8))
|
|
164
|
+
atomicFile.finishWrite(output)
|
|
177
165
|
} catch (e: Exception) {
|
|
166
|
+
output?.let(atomicFile::failWrite)
|
|
178
167
|
Log.e(TAG, "Failed to write crash marker", e)
|
|
179
168
|
}
|
|
180
169
|
}
|
|
@@ -194,35 +183,28 @@ internal class HotUpdaterRecoveryManager(
|
|
|
194
183
|
cancelRecoveryWatchdogAlarm(appContext)
|
|
195
184
|
}
|
|
196
185
|
|
|
197
|
-
private fun requestAutomaticRecovery(): Boolean {
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
private fun requestAutomaticRecovery(exception: Exception? = null): Boolean {
|
|
187
|
+
val request = launchState.requestRecovery()
|
|
188
|
+
if (exception != null) {
|
|
189
|
+
Log.e(TAG, "Caught React startup exception for bundleId=${launchState.snapshot()?.bundleId}", exception)
|
|
200
190
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
recoveryRequested = true
|
|
191
|
+
if (request == null) {
|
|
192
|
+
launchState.snapshot()?.let(::writeCrashMarker)
|
|
193
|
+
return false
|
|
207
194
|
}
|
|
208
|
-
|
|
195
|
+
if (!request.shouldRestart) return true
|
|
196
|
+
writeCrashMarker(request.launch)
|
|
209
197
|
val started = recoveryRestartCallback?.invoke() == true
|
|
198
|
+
launchState.finishRecoveryRequest(request, started)
|
|
210
199
|
if (!started) {
|
|
211
|
-
synchronized(this) {
|
|
212
|
-
recoveryRequested = false
|
|
213
|
-
}
|
|
214
200
|
Log.w(TAG, "Failed to schedule automatic recovery restart")
|
|
215
201
|
} else {
|
|
216
|
-
Log.i(TAG, "Scheduled automatic recovery restart for bundleId=$
|
|
202
|
+
Log.i(TAG, "Scheduled automatic recovery restart for bundleId=${request.launch.bundleId}")
|
|
217
203
|
}
|
|
218
204
|
return started
|
|
219
205
|
}
|
|
220
206
|
|
|
221
|
-
private fun handleJavaScriptException(exception: Exception): Boolean
|
|
222
|
-
Log.e(TAG, "Caught React startup exception for bundleId=$currentBundleId", exception)
|
|
223
|
-
writeCrashMarker()
|
|
224
|
-
return requestAutomaticRecovery()
|
|
225
|
-
}
|
|
207
|
+
private fun handleJavaScriptException(exception: Exception): Boolean = requestAutomaticRecovery(exception)
|
|
226
208
|
|
|
227
209
|
private fun ensureNativeSignalHandlerInstalled() {
|
|
228
210
|
if (signalHandlerInstalled || !loadNativeLibrary()) {
|
|
@@ -467,7 +449,8 @@ internal class HotUpdaterRecoveryManager(
|
|
|
467
449
|
}
|
|
468
450
|
|
|
469
451
|
val crashMarkerFile = File(getBundleStoreDir(appContext), CRASH_MARKER_FILENAME)
|
|
470
|
-
|
|
452
|
+
val recovery = PendingCrashRecovery.loadFromFile(crashMarkerFile)
|
|
453
|
+
if (recovery?.shouldRollback == true && recovery.launchedBundleId != null) {
|
|
471
454
|
Log.i(TAG, "Recovery watchdog detected crash marker, relaunching app")
|
|
472
455
|
watchdogStateFile.delete()
|
|
473
456
|
cancelRecoveryWatchdogAlarm(appContext)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
internal class HotUpdaterRecoveryState {
|
|
4
|
+
data class Launch(
|
|
5
|
+
val bundleId: String?,
|
|
6
|
+
val shouldRollback: Boolean,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
data class RecoveryRequest(
|
|
10
|
+
val launch: Launch,
|
|
11
|
+
val shouldRestart: Boolean,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
private enum class RestartState { NONE, REQUESTED, SCHEDULED, FAILED }
|
|
15
|
+
|
|
16
|
+
private var launch: Launch? = null
|
|
17
|
+
private var restartState = RestartState.NONE
|
|
18
|
+
private var contentAppeared = false
|
|
19
|
+
|
|
20
|
+
@Synchronized
|
|
21
|
+
fun start(
|
|
22
|
+
bundleId: String?,
|
|
23
|
+
shouldRollback: Boolean,
|
|
24
|
+
) {
|
|
25
|
+
launch = Launch(bundleId, shouldRollback)
|
|
26
|
+
restartState = RestartState.NONE
|
|
27
|
+
contentAppeared = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Synchronized
|
|
31
|
+
fun snapshot(): Launch? = launch
|
|
32
|
+
|
|
33
|
+
@Synchronized
|
|
34
|
+
fun stop() {
|
|
35
|
+
launch = null
|
|
36
|
+
restartState = RestartState.NONE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Synchronized
|
|
40
|
+
fun completeLaunch(): Launch? {
|
|
41
|
+
val current = launch ?: return null
|
|
42
|
+
if (contentAppeared || restartState != RestartState.NONE) return null
|
|
43
|
+
contentAppeared = true
|
|
44
|
+
launch = current.copy(shouldRollback = false)
|
|
45
|
+
return current
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Synchronized
|
|
49
|
+
fun requestRecovery(): RecoveryRequest? {
|
|
50
|
+
val current = launch?.takeIf { it.shouldRollback } ?: return null
|
|
51
|
+
val shouldRestart = restartState == RestartState.NONE || restartState == RestartState.FAILED
|
|
52
|
+
if (shouldRestart) restartState = RestartState.REQUESTED
|
|
53
|
+
return RecoveryRequest(current, shouldRestart)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Synchronized
|
|
57
|
+
fun finishRecoveryRequest(
|
|
58
|
+
request: RecoveryRequest,
|
|
59
|
+
started: Boolean,
|
|
60
|
+
) {
|
|
61
|
+
if (launch !== request.launch) return
|
|
62
|
+
restartState = if (started) RestartState.SCHEDULED else RestartState.FAILED
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.util.UUID
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Owns the install id, a random id created once per app installation.
|
|
9
|
+
*
|
|
10
|
+
* The id lives outside the bundle store and outside device backups, so a
|
|
11
|
+
* device restored from a backup gets an id of its own.
|
|
12
|
+
*/
|
|
13
|
+
class InstallIdentityService(
|
|
14
|
+
private val identityFile: File,
|
|
15
|
+
) {
|
|
16
|
+
companion object {
|
|
17
|
+
/** The install id kept in `noBackupFilesDir/hot-updater/install-identity.json`. */
|
|
18
|
+
fun create(context: Context): InstallIdentityService =
|
|
19
|
+
InstallIdentityService(NoBackupStorage.file(context, InstallationIdentity.IDENTITY_FILENAME))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private var identity: InstallationIdentity? = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the install id, creating and persisting a random one when none
|
|
26
|
+
* is stored. The id stays stable for this process even if persisting fails.
|
|
27
|
+
*/
|
|
28
|
+
@Synchronized
|
|
29
|
+
fun getInstallId(): String {
|
|
30
|
+
identity?.let { return it.installId }
|
|
31
|
+
val current =
|
|
32
|
+
InstallationIdentity.loadFromFile(identityFile)
|
|
33
|
+
?: InstallationIdentity(installId = UUID.randomUUID().toString()).also { it.saveToFile(identityFile) }
|
|
34
|
+
identity = current
|
|
35
|
+
return current.installId
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.core.util.AtomicFile
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.io.FileNotFoundException
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The persistent string key-value store behind `getStorageItem` and
|
|
13
|
+
* `setStorageItem`, which client plugins use through JS. Keys are opaque
|
|
14
|
+
* here; JS scopes them per plugin.
|
|
15
|
+
*
|
|
16
|
+
* Values live as one JSON object in a file outside device backups. The file
|
|
17
|
+
* is read once, on first use, and rewritten atomically on every set. A
|
|
18
|
+
* missing, unreadable, or corrupt file reads as empty and is replaced by the
|
|
19
|
+
* next set. A set that cannot be persisted still applies for this process.
|
|
20
|
+
*/
|
|
21
|
+
class KeyValueStorageService(
|
|
22
|
+
private val file: File,
|
|
23
|
+
) {
|
|
24
|
+
companion object {
|
|
25
|
+
private const val TAG = "KeyValueStorage"
|
|
26
|
+
const val FILENAME = "storage.json"
|
|
27
|
+
|
|
28
|
+
/** The store kept in `noBackupFilesDir/hot-updater/storage.json`. */
|
|
29
|
+
fun create(context: Context): KeyValueStorageService = KeyValueStorageService(NoBackupStorage.file(context, FILENAME))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private var values: MutableMap<String, String>? = null
|
|
33
|
+
|
|
34
|
+
@Synchronized
|
|
35
|
+
fun getItem(key: String): String? = loadValues()[key]
|
|
36
|
+
|
|
37
|
+
/** Stores [value] under [key], or removes [key] when [value] is null. */
|
|
38
|
+
@Synchronized
|
|
39
|
+
fun setItem(
|
|
40
|
+
key: String,
|
|
41
|
+
value: String?,
|
|
42
|
+
) {
|
|
43
|
+
val values = loadValues()
|
|
44
|
+
if (value == null) {
|
|
45
|
+
values.remove(key)
|
|
46
|
+
} else {
|
|
47
|
+
values[key] = value
|
|
48
|
+
}
|
|
49
|
+
write(values)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun loadValues(): MutableMap<String, String> = values ?: read().also { values = it }
|
|
53
|
+
|
|
54
|
+
private fun read(): MutableMap<String, String> =
|
|
55
|
+
try {
|
|
56
|
+
val json =
|
|
57
|
+
AtomicFile(file).openRead().use { input ->
|
|
58
|
+
JSONObject(String(input.readBytes(), Charsets.UTF_8))
|
|
59
|
+
}
|
|
60
|
+
val stored = linkedMapOf<String, String>()
|
|
61
|
+
json.keys().forEach { key ->
|
|
62
|
+
(json.opt(key) as? String)?.let { stored[key] = it }
|
|
63
|
+
}
|
|
64
|
+
stored
|
|
65
|
+
} catch (_: FileNotFoundException) {
|
|
66
|
+
linkedMapOf()
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
// The message of a parse error can quote stored values, so it is not logged.
|
|
69
|
+
Log.w(TAG, "Ignoring unreadable storage file (${e.javaClass.simpleName})")
|
|
70
|
+
linkedMapOf()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun write(values: Map<String, String>) {
|
|
74
|
+
val atomicFile = AtomicFile(file)
|
|
75
|
+
var output: FileOutputStream? = null
|
|
76
|
+
try {
|
|
77
|
+
file.parentFile?.mkdirs()
|
|
78
|
+
output = atomicFile.startWrite()
|
|
79
|
+
output.write(JSONObject(values).toString().toByteArray(Charsets.UTF_8))
|
|
80
|
+
atomicFile.finishWrite(output)
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
output?.let(atomicFile::failWrite)
|
|
83
|
+
Log.w(TAG, "Failed to persist storage", e)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.net.ConnectivityManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
|
|
9
|
+
internal object NetworkState {
|
|
10
|
+
/**
|
|
11
|
+
* True when the device reports no active network. False when it has one,
|
|
12
|
+
* or when that cannot be told, as when the app lacks
|
|
13
|
+
* `ACCESS_NETWORK_STATE`.
|
|
14
|
+
*/
|
|
15
|
+
fun hasNoActiveNetwork(context: Context): Boolean =
|
|
16
|
+
try {
|
|
17
|
+
val permission = context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE)
|
|
18
|
+
val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
|
19
|
+
when {
|
|
20
|
+
permission != PackageManager.PERMISSION_GRANTED || connectivity == null -> false
|
|
21
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> connectivity.activeNetwork == null
|
|
22
|
+
else -> hasNoConnectedNetworkLegacy(connectivity)
|
|
23
|
+
}
|
|
24
|
+
} catch (_: Exception) {
|
|
25
|
+
false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Suppress("DEPRECATION")
|
|
29
|
+
private fun hasNoConnectedNetworkLegacy(connectivity: ConnectivityManager): Boolean =
|
|
30
|
+
connectivity.activeNetworkInfo?.isConnected != true
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SDK state that must never be restored onto another installation lives in
|
|
8
|
+
* `noBackupFilesDir/hot-updater`, which Android Auto Backup skips.
|
|
9
|
+
*/
|
|
10
|
+
internal object NoBackupStorage {
|
|
11
|
+
private const val DIRECTORY_NAME = "hot-updater"
|
|
12
|
+
|
|
13
|
+
fun file(
|
|
14
|
+
context: Context,
|
|
15
|
+
name: String,
|
|
16
|
+
): File = File(File(context.noBackupFilesDir, DIRECTORY_NAME), name)
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
+
import kotlinx.coroutines.CancellationException
|
|
4
5
|
import kotlinx.coroutines.Dispatchers
|
|
5
6
|
import kotlinx.coroutines.delay
|
|
6
7
|
import kotlinx.coroutines.withContext
|
|
@@ -15,10 +16,11 @@ import okio.Source
|
|
|
15
16
|
import okio.buffer
|
|
16
17
|
import java.io.File
|
|
17
18
|
import java.io.IOException
|
|
18
|
-
import java.net.
|
|
19
|
+
import java.net.MalformedURLException
|
|
19
20
|
import java.net.URL
|
|
20
|
-
import java.net.UnknownHostException
|
|
21
21
|
import java.util.concurrent.TimeUnit
|
|
22
|
+
import javax.net.ssl.SSLHandshakeException
|
|
23
|
+
import javax.net.ssl.SSLPeerUnverifiedException
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Exception for incomplete downloads with size information
|
|
@@ -28,6 +30,47 @@ class IncompleteDownloadException(
|
|
|
28
30
|
val actualSize: Long,
|
|
29
31
|
) : IOException("Download incomplete: received $actualSize bytes, expected $expectedSize bytes")
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The server answered with a non-2xx status. [originCode] is the storage
|
|
35
|
+
* origin's XML error `<Code>` (S3, R2, or GCS style) when the body names one.
|
|
36
|
+
*/
|
|
37
|
+
class HttpStatusException(
|
|
38
|
+
val statusCode: Int,
|
|
39
|
+
statusMessage: String,
|
|
40
|
+
val originCode: String? = null,
|
|
41
|
+
) : Exception("HTTP error $statusCode: $statusMessage")
|
|
42
|
+
|
|
43
|
+
/** How much of an error body is read to find a storage origin's error code. */
|
|
44
|
+
internal const val MAX_ORIGIN_ERROR_BODY_BYTES = 4 * 1024L
|
|
45
|
+
|
|
46
|
+
private val ORIGIN_ERROR_CODE = Regex("<Error(?:\\s[^>]*)?>(?:(?!</Error>).)*?<Code>([^<]*)</Code>", RegexOption.DOT_MATCHES_ALL)
|
|
47
|
+
private val ORIGIN_CODE_VALUE = Regex("[A-Za-z0-9._-]{1,64}")
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads the `<Code>` of a storage origin's XML error, as in
|
|
51
|
+
* `<Error><Code>AccessDenied</Code>...`, from at most the first 4 KB of
|
|
52
|
+
* [body]. Only a 1 to 64 character code of letters, digits, `.`, `_`, and `-`
|
|
53
|
+
* is kept, so no key, resource, or message text is ever returned.
|
|
54
|
+
*/
|
|
55
|
+
internal fun readOriginErrorCode(body: ResponseBody?): String? =
|
|
56
|
+
try {
|
|
57
|
+
body?.source()?.let { source ->
|
|
58
|
+
source.request(MAX_ORIGIN_ERROR_BODY_BYTES)
|
|
59
|
+
val prefix = source.buffer.readByteArray(minOf(source.buffer.size, MAX_ORIGIN_ERROR_BODY_BYTES))
|
|
60
|
+
parseOriginErrorCode(String(prefix, Charsets.UTF_8))
|
|
61
|
+
}
|
|
62
|
+
} catch (_: Exception) {
|
|
63
|
+
null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
internal fun parseOriginErrorCode(body: String): String? =
|
|
67
|
+
ORIGIN_ERROR_CODE
|
|
68
|
+
.find(body)
|
|
69
|
+
?.groupValues
|
|
70
|
+
?.get(1)
|
|
71
|
+
?.trim()
|
|
72
|
+
?.takeIf { ORIGIN_CODE_VALUE.matches(it) }
|
|
73
|
+
|
|
31
74
|
/**
|
|
32
75
|
* Result wrapper for download operations
|
|
33
76
|
*/
|
|
@@ -65,6 +108,13 @@ interface DownloadService {
|
|
|
65
108
|
fileSizeCallback: ((Long) -> Unit)? = null,
|
|
66
109
|
progressCallback: (DownloadProgress) -> Unit,
|
|
67
110
|
): DownloadResult
|
|
111
|
+
|
|
112
|
+
suspend fun downloadFileOnce(
|
|
113
|
+
fileUrl: URL,
|
|
114
|
+
destination: File,
|
|
115
|
+
fileSizeCallback: ((Long) -> Unit)? = null,
|
|
116
|
+
progressCallback: (DownloadProgress) -> Unit,
|
|
117
|
+
): DownloadResult = downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
68
118
|
}
|
|
69
119
|
|
|
70
120
|
/**
|
|
@@ -127,13 +177,33 @@ private class ProgressResponseBody(
|
|
|
127
177
|
}
|
|
128
178
|
}
|
|
129
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Whether a failed download may succeed if tried again: a network failure or
|
|
182
|
+
* timeout, a body that ended early, or a 408, 429, or 5xx answer. Another 4xx,
|
|
183
|
+
* a TLS certificate failure, a canceled call, and a local storage failure
|
|
184
|
+
* would fail the same way again.
|
|
185
|
+
*/
|
|
186
|
+
internal fun isRetryableDownloadError(error: Exception): Boolean =
|
|
187
|
+
when (error) {
|
|
188
|
+
is HttpStatusException -> error.statusCode == 408 || error.statusCode == 429 || error.statusCode >= 500
|
|
189
|
+
is LocalStorageException -> false
|
|
190
|
+
is SSLHandshakeException, is SSLPeerUnverifiedException -> false
|
|
191
|
+
is MalformedURLException -> false
|
|
192
|
+
is IOException -> error.message != "Canceled"
|
|
193
|
+
else -> false
|
|
194
|
+
}
|
|
195
|
+
|
|
130
196
|
/**
|
|
131
197
|
* OkHttp-based implementation of DownloadService with resume support
|
|
132
198
|
*/
|
|
133
|
-
class OkHttpDownloadService
|
|
199
|
+
class OkHttpDownloadService internal constructor(
|
|
200
|
+
private val initialRetryDelayMs: Long,
|
|
201
|
+
) : DownloadService {
|
|
202
|
+
constructor() : this(INITIAL_RETRY_DELAY_MS)
|
|
203
|
+
|
|
134
204
|
companion object {
|
|
135
205
|
private const val TAG = "OkHttpDownloadService"
|
|
136
|
-
|
|
206
|
+
internal const val MAX_ATTEMPTS = 3
|
|
137
207
|
private const val INITIAL_RETRY_DELAY_MS = 1000L
|
|
138
208
|
private const val TIMEOUT_SECONDS = 30L
|
|
139
209
|
}
|
|
@@ -146,6 +216,12 @@ class OkHttpDownloadService : DownloadService {
|
|
|
146
216
|
.writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
|
147
217
|
.build()
|
|
148
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Downloads [fileUrl], trying again after a retryable failure (see
|
|
221
|
+
* [isRetryableDownloadError]) up to [MAX_ATTEMPTS] attempts in all, 1 and
|
|
222
|
+
* then 2 seconds apart. The last attempt's result is returned, so a
|
|
223
|
+
* failure keeps what classified it.
|
|
224
|
+
*/
|
|
149
225
|
override suspend fun downloadFile(
|
|
150
226
|
fileUrl: URL,
|
|
151
227
|
destination: File,
|
|
@@ -153,38 +229,51 @@ class OkHttpDownloadService : DownloadService {
|
|
|
153
229
|
progressCallback: (DownloadProgress) -> Unit,
|
|
154
230
|
): DownloadResult =
|
|
155
231
|
withContext(Dispatchers.IO) {
|
|
156
|
-
var attempt =
|
|
157
|
-
var
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (attempt < MAX_RETRIES && isRetryableException(e)) {
|
|
172
|
-
val delayMs = INITIAL_RETRY_DELAY_MS * (1 shl (attempt - 1))
|
|
173
|
-
Log.d(
|
|
174
|
-
TAG,
|
|
175
|
-
"Download failed (attempt $attempt/$MAX_RETRIES): ${e.message}. Retrying in ${delayMs}ms...",
|
|
176
|
-
)
|
|
177
|
-
delay(delayMs)
|
|
178
|
-
} else {
|
|
179
|
-
Log.d(TAG, "Download failed: ${e.message}")
|
|
180
|
-
break
|
|
181
|
-
}
|
|
182
|
-
}
|
|
232
|
+
var attempt = 1
|
|
233
|
+
var result = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
234
|
+
while (
|
|
235
|
+
result is DownloadResult.Error &&
|
|
236
|
+
attempt < MAX_ATTEMPTS &&
|
|
237
|
+
isRetryableDownloadError(result.exception)
|
|
238
|
+
) {
|
|
239
|
+
val delayMs = initialRetryDelayMs * (1 shl (attempt - 1))
|
|
240
|
+
Log.d(
|
|
241
|
+
TAG,
|
|
242
|
+
"Download failed (attempt $attempt/$MAX_ATTEMPTS): ${result.exception.message}. Retrying in ${delayMs}ms...",
|
|
243
|
+
)
|
|
244
|
+
delay(delayMs)
|
|
245
|
+
attempt++
|
|
246
|
+
result = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
183
247
|
}
|
|
248
|
+
if (result is DownloadResult.Error) {
|
|
249
|
+
Log.d(TAG, "Download failed (attempt $attempt/$MAX_ATTEMPTS): ${result.exception.message}")
|
|
250
|
+
}
|
|
251
|
+
result
|
|
252
|
+
}
|
|
184
253
|
|
|
185
|
-
|
|
254
|
+
/** One attempt; an exception a callback threw, such as the disk space check's, is its error. */
|
|
255
|
+
private suspend fun attemptOrError(
|
|
256
|
+
fileUrl: URL,
|
|
257
|
+
destination: File,
|
|
258
|
+
fileSizeCallback: ((Long) -> Unit)?,
|
|
259
|
+
progressCallback: (DownloadProgress) -> Unit,
|
|
260
|
+
): DownloadResult =
|
|
261
|
+
try {
|
|
262
|
+
attemptDownload(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
263
|
+
} catch (error: CancellationException) {
|
|
264
|
+
throw error
|
|
265
|
+
} catch (error: Exception) {
|
|
266
|
+
DownloadResult.Error(error)
|
|
186
267
|
}
|
|
187
268
|
|
|
269
|
+
/** One attempt with no retry, for a download with a fallback, such as the archive's per-file downloads. */
|
|
270
|
+
override suspend fun downloadFileOnce(
|
|
271
|
+
fileUrl: URL,
|
|
272
|
+
destination: File,
|
|
273
|
+
fileSizeCallback: ((Long) -> Unit)?,
|
|
274
|
+
progressCallback: (DownloadProgress) -> Unit,
|
|
275
|
+
): DownloadResult = attemptOrError(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
276
|
+
|
|
188
277
|
private suspend fun attemptDownload(
|
|
189
278
|
fileUrl: URL,
|
|
190
279
|
destination: File,
|
|
@@ -212,10 +301,10 @@ class OkHttpDownloadService : DownloadService {
|
|
|
212
301
|
}
|
|
213
302
|
|
|
214
303
|
if (!response.isSuccessful) {
|
|
215
|
-
val
|
|
216
|
-
Log.d(TAG,
|
|
304
|
+
val error = HttpStatusException(response.code, response.message, readOriginErrorCode(response.body))
|
|
305
|
+
Log.d(TAG, "HTTP error ${response.code}: ${response.message}")
|
|
217
306
|
response.close()
|
|
218
|
-
return@withContext DownloadResult.Error(
|
|
307
|
+
return@withContext DownloadResult.Error(error)
|
|
219
308
|
}
|
|
220
309
|
|
|
221
310
|
val body = response.body
|
|
@@ -242,9 +331,10 @@ class OkHttpDownloadService : DownloadService {
|
|
|
242
331
|
progressCallback.invoke(progress)
|
|
243
332
|
}
|
|
244
333
|
|
|
245
|
-
// Write to file
|
|
334
|
+
// Write to file. A local write failure surfaces as LocalStorageException,
|
|
335
|
+
// so it is not mistaken for a network failure.
|
|
246
336
|
progressBody.source().use { source ->
|
|
247
|
-
|
|
337
|
+
LocalStorageOutputStream.open(destination).use { output ->
|
|
248
338
|
val buffer = ByteArray(8 * 1024)
|
|
249
339
|
var bytesRead: Int
|
|
250
340
|
|
|
@@ -291,17 +381,4 @@ class OkHttpDownloadService : DownloadService {
|
|
|
291
381
|
DownloadResult.Error(e)
|
|
292
382
|
}
|
|
293
383
|
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Check if exception is retryable
|
|
297
|
-
*/
|
|
298
|
-
private fun isRetryableException(e: Exception): Boolean =
|
|
299
|
-
when (e) {
|
|
300
|
-
is SocketTimeoutException,
|
|
301
|
-
is UnknownHostException,
|
|
302
|
-
is IOException,
|
|
303
|
-
-> true
|
|
304
|
-
|
|
305
|
-
else -> false
|
|
306
|
-
}
|
|
307
384
|
}
|