@hot-updater/react-native 0.27.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- package/src/wrap.tsx +8 -12
|
@@ -14,8 +14,6 @@ data class BundleMetadata(
|
|
|
14
14
|
val stableBundleId: String? = null,
|
|
15
15
|
val stagingBundleId: String? = null,
|
|
16
16
|
val verificationPending: Boolean = false,
|
|
17
|
-
val verificationAttemptedAt: Long? = null,
|
|
18
|
-
val stagingExecutionCount: Int? = null,
|
|
19
17
|
val updatedAt: Long = System.currentTimeMillis(),
|
|
20
18
|
) {
|
|
21
19
|
companion object {
|
|
@@ -45,18 +43,6 @@ data class BundleMetadata(
|
|
|
45
43
|
null
|
|
46
44
|
},
|
|
47
45
|
verificationPending = json.optBoolean("verificationPending", false),
|
|
48
|
-
verificationAttemptedAt =
|
|
49
|
-
if (json.has("verificationAttemptedAt") && !json.isNull("verificationAttemptedAt")) {
|
|
50
|
-
json.getLong("verificationAttemptedAt")
|
|
51
|
-
} else {
|
|
52
|
-
null
|
|
53
|
-
},
|
|
54
|
-
stagingExecutionCount =
|
|
55
|
-
if (json.has("stagingExecutionCount") && !json.isNull("stagingExecutionCount")) {
|
|
56
|
-
json.getInt("stagingExecutionCount")
|
|
57
|
-
} else {
|
|
58
|
-
null
|
|
59
|
-
},
|
|
60
46
|
updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
|
|
61
47
|
)
|
|
62
48
|
|
|
@@ -100,8 +86,6 @@ data class BundleMetadata(
|
|
|
100
86
|
put("stableBundleId", stableBundleId ?: JSONObject.NULL)
|
|
101
87
|
put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
|
|
102
88
|
put("verificationPending", verificationPending)
|
|
103
|
-
put("verificationAttemptedAt", verificationAttemptedAt ?: JSONObject.NULL)
|
|
104
|
-
put("stagingExecutionCount", stagingExecutionCount ?: JSONObject.NULL)
|
|
105
89
|
put("updatedAt", updatedAt)
|
|
106
90
|
}
|
|
107
91
|
|
|
@@ -237,3 +221,76 @@ data class CrashedHistory(
|
|
|
237
221
|
bundles.clear()
|
|
238
222
|
}
|
|
239
223
|
}
|
|
224
|
+
|
|
225
|
+
data class PendingCrashRecovery(
|
|
226
|
+
val launchedBundleId: String?,
|
|
227
|
+
val shouldRollback: Boolean,
|
|
228
|
+
) {
|
|
229
|
+
companion object {
|
|
230
|
+
fun fromJson(json: JSONObject): PendingCrashRecovery =
|
|
231
|
+
PendingCrashRecovery(
|
|
232
|
+
launchedBundleId =
|
|
233
|
+
if (json.has("bundleId") && !json.isNull("bundleId")) {
|
|
234
|
+
json.getString("bundleId").takeIf { it.isNotEmpty() }
|
|
235
|
+
} else {
|
|
236
|
+
null
|
|
237
|
+
},
|
|
238
|
+
shouldRollback = json.optBoolean("shouldRollback", false),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
data class LaunchSelection(
|
|
244
|
+
val bundleUrl: String,
|
|
245
|
+
val launchedBundleId: String?,
|
|
246
|
+
val shouldRollbackOnCrash: Boolean,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
data class LaunchReport(
|
|
250
|
+
val status: String = "STABLE",
|
|
251
|
+
val crashedBundleId: String? = null,
|
|
252
|
+
) {
|
|
253
|
+
companion object {
|
|
254
|
+
private const val TAG = "LaunchReport"
|
|
255
|
+
const val LAUNCH_REPORT_FILENAME = "launch-report.json"
|
|
256
|
+
|
|
257
|
+
fun fromJson(json: JSONObject): LaunchReport =
|
|
258
|
+
LaunchReport(
|
|
259
|
+
status = json.optString("status", "STABLE"),
|
|
260
|
+
crashedBundleId =
|
|
261
|
+
if (json.has("crashedBundleId") && !json.isNull("crashedBundleId")) {
|
|
262
|
+
json.getString("crashedBundleId").takeIf { it.isNotEmpty() }
|
|
263
|
+
} else {
|
|
264
|
+
null
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
fun loadFromFile(file: File): LaunchReport? =
|
|
269
|
+
try {
|
|
270
|
+
if (!file.exists()) {
|
|
271
|
+
null
|
|
272
|
+
} else {
|
|
273
|
+
fromJson(JSONObject(file.readText()))
|
|
274
|
+
}
|
|
275
|
+
} catch (e: Exception) {
|
|
276
|
+
Log.e(TAG, "Failed to load launch report", e)
|
|
277
|
+
null
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fun toJson(): JSONObject =
|
|
282
|
+
JSONObject().apply {
|
|
283
|
+
put("status", status)
|
|
284
|
+
put("crashedBundleId", crashedBundleId ?: JSONObject.NULL)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fun saveToFile(file: File): Boolean =
|
|
288
|
+
try {
|
|
289
|
+
file.parentFile?.mkdirs()
|
|
290
|
+
file.writeText(toJson().toString(2))
|
|
291
|
+
true
|
|
292
|
+
} catch (e: Exception) {
|
|
293
|
+
Log.e(TAG, "Failed to save launch report", e)
|
|
294
|
+
false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import android.provider.Settings
|
|
6
|
+
import java.util.UUID
|
|
7
|
+
|
|
8
|
+
class CohortService(
|
|
9
|
+
private val context: Context,
|
|
10
|
+
) {
|
|
11
|
+
private val prefs: SharedPreferences =
|
|
12
|
+
context.getSharedPreferences("HotUpdaterCohort", Context.MODE_PRIVATE)
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
// Keep the legacy key so existing custom cohorts continue to work.
|
|
16
|
+
private const val COHORT_KEY = "custom_cohort"
|
|
17
|
+
private const val FALLBACK_IDENTIFIER_KEY = "fallback_identifier"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private fun hashString(value: String): Int {
|
|
21
|
+
var hash = 0
|
|
22
|
+
for (char in value) {
|
|
23
|
+
hash = (hash shl 5) - hash + char.code
|
|
24
|
+
}
|
|
25
|
+
return hash
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private fun defaultNumericCohort(identifier: String): String {
|
|
29
|
+
val hash = hashString(identifier)
|
|
30
|
+
val normalized = ((hash % 1000) + 1000) % 1000
|
|
31
|
+
return (normalized + 1).toString()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun fallbackIdentifier(): String {
|
|
35
|
+
val fallback = prefs.getString(FALLBACK_IDENTIFIER_KEY, null)
|
|
36
|
+
if (!fallback.isNullOrEmpty()) {
|
|
37
|
+
return fallback
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
val generated = UUID.randomUUID().toString()
|
|
41
|
+
prefs.edit().putString(FALLBACK_IDENTIFIER_KEY, generated).apply()
|
|
42
|
+
return generated
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun setCohort(cohort: String) {
|
|
46
|
+
if (cohort.isEmpty()) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
prefs.edit().putString(COHORT_KEY, cohort).apply()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun getCohort(): String {
|
|
53
|
+
val cohort = prefs.getString(COHORT_KEY, null)
|
|
54
|
+
if (!cohort.isNullOrEmpty()) {
|
|
55
|
+
return cohort
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
val androidId =
|
|
59
|
+
Settings.Secure.getString(
|
|
60
|
+
context.contentResolver,
|
|
61
|
+
Settings.Secure.ANDROID_ID,
|
|
62
|
+
)
|
|
63
|
+
val initialCohort =
|
|
64
|
+
if (!androidId.isNullOrEmpty()) {
|
|
65
|
+
defaultNumericCohort(androidId)
|
|
66
|
+
} else {
|
|
67
|
+
defaultNumericCohort(fallbackIdentifier())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
prefs.edit().putString(COHORT_KEY, initialCohort).apply()
|
|
71
|
+
return initialCohort
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -12,14 +12,14 @@ class DecompressService {
|
|
|
12
12
|
private const val TAG = "DecompressService"
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
private val
|
|
15
|
+
// Strategies with reliable file signatures that can be validated cheaply.
|
|
16
|
+
// TAR.BR is attempted only as the final fallback because Brotli has no reliable magic bytes.
|
|
17
|
+
private val signatureStrategies =
|
|
18
18
|
listOf(
|
|
19
19
|
ZipDecompressionStrategy(),
|
|
20
20
|
TarGzDecompressionStrategy(),
|
|
21
|
-
TarBrDecompressionStrategy(),
|
|
22
21
|
)
|
|
22
|
+
private val tarBrStrategy = TarBrDecompressionStrategy()
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Extracts a compressed file to the destination directory.
|
|
@@ -39,45 +39,51 @@ class DecompressService {
|
|
|
39
39
|
val fileName = file.name
|
|
40
40
|
val fileSize = if (file.exists()) file.length() else 0L
|
|
41
41
|
|
|
42
|
-
// Try each strategy
|
|
43
|
-
for (strategy in
|
|
42
|
+
// Try each signature-based strategy first.
|
|
43
|
+
for (strategy in signatureStrategies) {
|
|
44
44
|
if (strategy.isValid(filePath)) {
|
|
45
45
|
Log.d(TAG, "Using strategy for $fileName")
|
|
46
46
|
return strategy.decompress(filePath, destinationPath, progressCallback)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
val errorMessage =
|
|
52
|
-
"""
|
|
53
|
-
Failed to decompress file: $fileName ($fileSize bytes)
|
|
50
|
+
Log.d(TAG, "No ZIP/TAR.GZ signature matched for $fileName, trying TAR.BR fallback")
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- GZIP compressed TAR archives (.tar.gz)
|
|
60
|
-
- Brotli compressed TAR archives (.tar.br)
|
|
61
|
-
|
|
62
|
-
Please verify the file is not corrupted and matches one of the supported formats.
|
|
63
|
-
""".trimIndent()
|
|
52
|
+
if (tarBrStrategy.decompress(filePath, destinationPath, progressCallback)) {
|
|
53
|
+
Log.d(TAG, "Using TAR.BR fallback for $fileName")
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
64
56
|
|
|
57
|
+
val errorMessage = createInvalidArchiveMessage(fileName, fileSize)
|
|
65
58
|
Log.e(TAG, errorMessage)
|
|
66
59
|
return false
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
/**
|
|
70
|
-
* Validates if a file
|
|
63
|
+
* Validates if a file matches one of the signature-based archive formats.
|
|
71
64
|
* @param filePath Path to the file to validate
|
|
72
65
|
* @return true if the file is a valid compressed archive
|
|
73
66
|
*/
|
|
74
67
|
fun isValidZipFile(filePath: String): Boolean {
|
|
75
|
-
for (strategy in
|
|
68
|
+
for (strategy in signatureStrategies) {
|
|
76
69
|
if (strategy.isValid(filePath)) {
|
|
77
70
|
return true
|
|
78
71
|
}
|
|
79
72
|
}
|
|
80
|
-
Log.d(TAG, "No
|
|
73
|
+
Log.d(TAG, "No ZIP/TAR.GZ signature matched for file: $filePath. TAR.BR is handled during extraction fallback.")
|
|
81
74
|
return false
|
|
82
75
|
}
|
|
76
|
+
|
|
77
|
+
private fun createInvalidArchiveMessage(
|
|
78
|
+
fileName: String,
|
|
79
|
+
fileSize: Long,
|
|
80
|
+
): String =
|
|
81
|
+
"""
|
|
82
|
+
The downloaded bundle file is not a valid compressed archive: $fileName ($fileSize bytes)
|
|
83
|
+
|
|
84
|
+
Supported formats:
|
|
85
|
+
- ZIP archives (.zip)
|
|
86
|
+
- GZIP compressed TAR archives (.tar.gz)
|
|
87
|
+
- Brotli compressed TAR archives (.tar.br)
|
|
88
|
+
""".trimIndent()
|
|
83
89
|
}
|
|
@@ -48,7 +48,7 @@ class HotUpdaterException(
|
|
|
48
48
|
fun extractionFormatError(cause: Throwable? = null) =
|
|
49
49
|
HotUpdaterException(
|
|
50
50
|
"EXTRACTION_FORMAT_ERROR",
|
|
51
|
-
"
|
|
51
|
+
"The downloaded bundle file is not a valid compressed archive",
|
|
52
52
|
cause,
|
|
53
53
|
)
|
|
54
54
|
|
|
@@ -15,6 +15,8 @@ class HotUpdaterImpl {
|
|
|
15
15
|
private val context: Context
|
|
16
16
|
private val bundleStorage: BundleStorageService
|
|
17
17
|
private val preferences: PreferencesService
|
|
18
|
+
private val recoveryManager: HotUpdaterRecoveryManager
|
|
19
|
+
private var currentLaunchSelection: LaunchSelection? = null
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Primary constructor with dependency injection (for testing)
|
|
@@ -27,6 +29,7 @@ class HotUpdaterImpl {
|
|
|
27
29
|
this.context = context.applicationContext
|
|
28
30
|
this.bundleStorage = bundleStorage
|
|
29
31
|
this.preferences = preferences
|
|
32
|
+
this.recoveryManager = HotUpdaterRecoveryManager(this.context)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -265,7 +268,7 @@ class HotUpdaterImpl {
|
|
|
265
268
|
* Gets the path to the bundle file
|
|
266
269
|
* @return The path to the bundle file
|
|
267
270
|
*/
|
|
268
|
-
fun getJSBundleFile(): String =
|
|
271
|
+
fun getJSBundleFile(): String = prepareLaunchIfNeeded().bundleUrl
|
|
269
272
|
|
|
270
273
|
/**
|
|
271
274
|
* Updates the bundle from the specified URL
|
|
@@ -299,6 +302,7 @@ class HotUpdaterImpl {
|
|
|
299
302
|
*/
|
|
300
303
|
suspend fun reload(reactContext: Context) {
|
|
301
304
|
try {
|
|
305
|
+
currentLaunchSelection = null
|
|
302
306
|
withContext(Dispatchers.Main) {
|
|
303
307
|
performReactReload(reactContext)
|
|
304
308
|
}
|
|
@@ -309,6 +313,7 @@ class HotUpdaterImpl {
|
|
|
309
313
|
|
|
310
314
|
suspend fun reloadProcess(reactContext: Context) {
|
|
311
315
|
try {
|
|
316
|
+
currentLaunchSelection = null
|
|
312
317
|
withContext(Dispatchers.Main) {
|
|
313
318
|
if (!restartApplication(reactContext)) {
|
|
314
319
|
Log.w(TAG, "Falling back to in-process reload because process restart could not be started")
|
|
@@ -330,22 +335,24 @@ class HotUpdaterImpl {
|
|
|
330
335
|
|
|
331
336
|
// Use a cold restart in release builds so bundle application does not depend on RN reload timing.
|
|
332
337
|
private fun restartApplication(reactContext: Context): Boolean {
|
|
338
|
+
val applicationContext = reactContext.applicationContext
|
|
333
339
|
val currentActivity =
|
|
334
340
|
(reactContext as? com.facebook.react.bridge.ReactApplicationContext)?.currentActivity
|
|
335
|
-
if (currentActivity == null) {
|
|
336
|
-
Log.w(TAG, "Cannot restart app: current activity unavailable")
|
|
337
|
-
return false
|
|
338
|
-
}
|
|
339
341
|
|
|
340
342
|
return try {
|
|
341
343
|
val restartIntent =
|
|
342
|
-
Intent(
|
|
344
|
+
Intent(applicationContext, HotUpdaterRestartActivity::class.java).apply {
|
|
345
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
343
346
|
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
344
|
-
putExtra(HotUpdaterRestartActivity.EXTRA_PACKAGE_NAME,
|
|
347
|
+
putExtra(HotUpdaterRestartActivity.EXTRA_PACKAGE_NAME, applicationContext.packageName)
|
|
345
348
|
putExtra(HotUpdaterRestartActivity.EXTRA_TARGET_PID, Process.myPid())
|
|
346
349
|
}
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
if (currentActivity != null) {
|
|
351
|
+
val options = ActivityOptions.makeCustomAnimation(currentActivity, 0, 0)
|
|
352
|
+
currentActivity.startActivity(restartIntent, options.toBundle())
|
|
353
|
+
} else {
|
|
354
|
+
applicationContext.startActivity(restartIntent)
|
|
355
|
+
}
|
|
349
356
|
|
|
350
357
|
Log.i(TAG, "Started restart trampoline to apply update bundle")
|
|
351
358
|
return true
|
|
@@ -356,12 +363,11 @@ class HotUpdaterImpl {
|
|
|
356
363
|
}
|
|
357
364
|
|
|
358
365
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* @param bundleId The ID of the currently running bundle
|
|
366
|
+
* Returns the launch report for the current process.
|
|
367
|
+
* Startup success and rollback are finalized before JS reads it.
|
|
362
368
|
* @return Map containing status and optional crashedBundleId
|
|
363
369
|
*/
|
|
364
|
-
fun notifyAppReady(
|
|
370
|
+
fun notifyAppReady(): Map<String, Any?> = bundleStorage.notifyAppReady()
|
|
365
371
|
|
|
366
372
|
/**
|
|
367
373
|
* Gets the crashed bundle history.
|
|
@@ -383,11 +389,43 @@ class HotUpdaterImpl {
|
|
|
383
389
|
*/
|
|
384
390
|
fun getBaseURL(): String = bundleStorage.getBaseURL()
|
|
385
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Gets the current active bundle ID from bundle storage.
|
|
394
|
+
* Reads manifest.json first and falls back to the legacy BUNDLE_ID file.
|
|
395
|
+
* Built-in bundle fallback is handled in JS.
|
|
396
|
+
*/
|
|
397
|
+
fun getBundleId(): String? = bundleStorage.getBundleId()
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Gets the current manifest from bundle storage.
|
|
401
|
+
*/
|
|
402
|
+
fun getManifest(): Map<String, Any?> = bundleStorage.getManifest()
|
|
403
|
+
|
|
386
404
|
suspend fun resetChannel(): Boolean {
|
|
387
405
|
val success = bundleStorage.resetChannel()
|
|
388
406
|
if (success) {
|
|
389
407
|
preferences.setItem(CHANNEL_STORAGE_KEY, null)
|
|
408
|
+
currentLaunchSelection = null
|
|
390
409
|
}
|
|
391
410
|
return success
|
|
392
411
|
}
|
|
412
|
+
|
|
413
|
+
private fun prepareLaunchIfNeeded(): LaunchSelection {
|
|
414
|
+
currentLaunchSelection?.let { return it }
|
|
415
|
+
|
|
416
|
+
val pendingRecovery = recoveryManager.consumePendingCrashRecovery()
|
|
417
|
+
val selection = bundleStorage.prepareLaunch(pendingRecovery)
|
|
418
|
+
recoveryManager.startMonitoring(
|
|
419
|
+
bundleId = selection.launchedBundleId,
|
|
420
|
+
shouldRollback = selection.shouldRollbackOnCrash,
|
|
421
|
+
onContentAppeared = { launchedBundleId ->
|
|
422
|
+
bundleStorage.markLaunchCompleted(launchedBundleId)
|
|
423
|
+
},
|
|
424
|
+
onRecoveryRestartRequested = {
|
|
425
|
+
restartApplication(context)
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
currentLaunchSelection = selection
|
|
429
|
+
return selection
|
|
430
|
+
}
|
|
393
431
|
}
|