@hot-updater/react-native 1.0.0-rc.33 → 1.0.0-rc.35
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 +0 -9
- 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/BundleFileStorageService.kt +119 -16
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +35 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +14 -10
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +38 -138
- package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
- package/android/src/newarch/ReactIntegrationManager.kt +6 -32
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +93 -15
- package/ios/HotUpdater/Internal/BundleMetadata.swift +53 -1
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +39 -0
- package/lib/commonjs/appReady.js +3 -12
- package/lib/commonjs/appReady.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +0 -4
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/native.js +0 -4
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/appReady.js +2 -10
- package/lib/module/appReady.js.map +1 -1
- package/lib/module/checkForUpdate.js +1 -5
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/native.js +0 -4
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +0 -2
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +0 -2
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +0 -7
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +0 -2
- package/lib/typescript/module/appReady.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +0 -2
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +0 -7
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/appReady.ts +0 -8
- package/src/checkForUpdate.ts +1 -7
- package/src/native.ts +0 -8
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +0 -7
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +0 -89
package/android/build.gradle
CHANGED
|
@@ -20,8 +20,12 @@ def reactNativeArchitectures() {
|
|
|
20
20
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// The New Architecture is the default. React Native 0.82 and later run nothing
|
|
24
|
+
// else and set newArchEnabled to true on every library project, whatever the
|
|
25
|
+
// app's gradle.properties says, so only an older React Native app that sets
|
|
26
|
+
// newArchEnabled=false builds the old architecture.
|
|
23
27
|
def isNewArchitectureEnabled() {
|
|
24
|
-
return
|
|
28
|
+
return !project.hasProperty("newArchEnabled") || project.property("newArchEnabled").toString().toBoolean()
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
apply plugin: "com.android.library"
|
|
@@ -115,7 +119,11 @@ android {
|
|
|
115
119
|
"src/newarch",
|
|
116
120
|
// Codegen specs
|
|
117
121
|
"generated/java",
|
|
118
|
-
"generated/jni"
|
|
122
|
+
"generated/jni",
|
|
123
|
+
// React Native 0.81 and older can run the New Architecture on the
|
|
124
|
+
// bridge. Later versions have none, so their builds never compile
|
|
125
|
+
// ReactNativeHost.
|
|
126
|
+
project.ext.reactNativeMinorVersion >= 82 ? "src/bridgeless" : "src/bridge"
|
|
119
127
|
]
|
|
120
128
|
}
|
|
121
129
|
}
|
|
@@ -146,6 +154,9 @@ dependencies {
|
|
|
146
154
|
}
|
|
147
155
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
148
156
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
|
157
|
+
// ProcessLifecycleOwner, for an activity that started before Hot Updater was created.
|
|
158
|
+
// React Native's AppCompat already brings it at runtime.
|
|
159
|
+
implementation "androidx.lifecycle:lifecycle-process:2.6.1"
|
|
149
160
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
|
150
161
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
151
162
|
implementation "org.apache.commons:commons-compress:1.27.1"
|
|
@@ -6,15 +6,6 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
# New Architecture
|
|
9
|
-
# Crash recovery discovers these accessors through reflection.
|
|
10
|
-
-keepclassmembers class * implements com.facebook.react.ReactApplication {
|
|
11
|
-
public *** getReactHost();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
-keepclassmembers class * implements com.facebook.react.ReactHost {
|
|
15
|
-
public *** getCurrentReactContext();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
# Keep fields accessed via reflection in ReactHost
|
|
19
10
|
# Support both Java (mReactHostDelegate) and Kotlin (reactHostDelegate) field names
|
|
20
11
|
-keepclassmembers class com.facebook.react.runtime.ReactHostImpl {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactApplication
|
|
4
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Native 0.81 and older can run the New Architecture on the bridge, which
|
|
8
|
+
* only ReactNativeHost reaches. Only builds for those versions compile this file.
|
|
9
|
+
*/
|
|
10
|
+
internal object BridgeReactInstance {
|
|
11
|
+
fun setJSBundle(
|
|
12
|
+
application: ReactApplication,
|
|
13
|
+
bundleLoader: JSBundleLoader?,
|
|
14
|
+
) {
|
|
15
|
+
@Suppress("DEPRECATION")
|
|
16
|
+
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
17
|
+
val bundleLoaderField = instanceManager::class.java.getDeclaredField("mBundleLoader")
|
|
18
|
+
bundleLoaderField.isAccessible = true
|
|
19
|
+
bundleLoaderField.set(instanceManager, bundleLoader)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fun reload(application: ReactApplication) {
|
|
23
|
+
@Suppress("DEPRECATION")
|
|
24
|
+
val reactNativeHost = application.reactNativeHost
|
|
25
|
+
try {
|
|
26
|
+
reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
|
|
27
|
+
} catch (e: Exception) {
|
|
28
|
+
val currentActivity = reactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
|
|
29
|
+
if (currentActivity == null) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
currentActivity.runOnUiThread {
|
|
34
|
+
currentActivity.recreate()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.ReactApplication
|
|
5
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React Native 0.82 and later have no bridge, so an app without a ReactHost has
|
|
9
|
+
* no instance to update. Builds for those versions compile this file instead of
|
|
10
|
+
* the bridge one, and never reference ReactNativeHost.
|
|
11
|
+
*/
|
|
12
|
+
@Suppress("UNUSED_PARAMETER")
|
|
13
|
+
internal object BridgeReactInstance {
|
|
14
|
+
fun setJSBundle(
|
|
15
|
+
application: ReactApplication,
|
|
16
|
+
bundleLoader: JSBundleLoader?,
|
|
17
|
+
) {
|
|
18
|
+
Log.d("HotUpdater", "No ReactHost to set the JS bundle on")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun reload(application: ReactApplication) {
|
|
22
|
+
Log.d("HotUpdater", "No ReactHost to reload")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import androidx.lifecycle.Lifecycle
|
|
7
|
+
import androidx.lifecycle.ProcessLifecycleOwner
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs an action once this process has a started activity.
|
|
11
|
+
*
|
|
12
|
+
* A headless JS task (HeadlessJsTaskService, which background push libraries
|
|
13
|
+
* start) loads the bundle with no activity and never renders. Waiting for an
|
|
14
|
+
* activity keeps that launch from counting as one that ended before its first
|
|
15
|
+
* render.
|
|
16
|
+
*
|
|
17
|
+
* Android does not replay lifecycle callbacks to a late registration. An
|
|
18
|
+
* activity that started before this gate existed, as when a brownfield app adds
|
|
19
|
+
* React Native to an activity already on screen, is read from
|
|
20
|
+
* [processHasStartedActivity] instead.
|
|
21
|
+
*/
|
|
22
|
+
internal class ActivityStartGate(
|
|
23
|
+
private val application: Application?,
|
|
24
|
+
private val processHasStartedActivity: () -> Boolean = ::hasStartedActivity,
|
|
25
|
+
) : Application.ActivityLifecycleCallbacks {
|
|
26
|
+
private var startedActivities = 0
|
|
27
|
+
private var pendingAction: (() -> Unit)? = null
|
|
28
|
+
|
|
29
|
+
init {
|
|
30
|
+
application?.registerActivityLifecycleCallbacks(this)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runs [action] now when an activity is started, otherwise at the next
|
|
35
|
+
* activity start. It replaces an action still waiting; null cancels it.
|
|
36
|
+
* Without an application to observe, it runs [action] now.
|
|
37
|
+
*/
|
|
38
|
+
fun runWhenActivityStarted(action: (() -> Unit)?) {
|
|
39
|
+
val runNow =
|
|
40
|
+
synchronized(this) {
|
|
41
|
+
val waits =
|
|
42
|
+
application != null &&
|
|
43
|
+
startedActivities == 0 &&
|
|
44
|
+
!processHasStartedActivity()
|
|
45
|
+
pendingAction = action.takeIf { waits }
|
|
46
|
+
action.takeUnless { waits }
|
|
47
|
+
}
|
|
48
|
+
runNow?.invoke()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun onActivityStarted(activity: Activity) {
|
|
52
|
+
val action =
|
|
53
|
+
synchronized(this) {
|
|
54
|
+
startedActivities += 1
|
|
55
|
+
pendingAction.also { pendingAction = null }
|
|
56
|
+
}
|
|
57
|
+
action?.invoke()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onActivityStopped(activity: Activity) {
|
|
61
|
+
synchronized(this) {
|
|
62
|
+
// An activity started before this gate existed was never counted.
|
|
63
|
+
if (startedActivities > 0) startedActivities -= 1
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override fun onActivityCreated(
|
|
68
|
+
activity: Activity,
|
|
69
|
+
savedInstanceState: Bundle?,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
override fun onActivityResumed(activity: Activity) {}
|
|
73
|
+
|
|
74
|
+
override fun onActivityPaused(activity: Activity) {}
|
|
75
|
+
|
|
76
|
+
override fun onActivitySaveInstanceState(
|
|
77
|
+
activity: Activity,
|
|
78
|
+
outState: Bundle,
|
|
79
|
+
) {}
|
|
80
|
+
|
|
81
|
+
override fun onActivityDestroyed(activity: Activity) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ProcessLifecycleOwner observes activities from process start, through the
|
|
85
|
+
// androidx.startup initializer that lifecycle-process registers. It is STARTED
|
|
86
|
+
// while an activity of this process is started, and stays at INITIALIZED when
|
|
87
|
+
// an app removes that initializer, so the gate then waits for the next start.
|
|
88
|
+
private fun hasStartedActivity(): Boolean =
|
|
89
|
+
runCatching {
|
|
90
|
+
ProcessLifecycleOwner
|
|
91
|
+
.get()
|
|
92
|
+
.lifecycle.currentState
|
|
93
|
+
.isAtLeast(Lifecycle.State.STARTED)
|
|
94
|
+
}.getOrDefault(false)
|
|
@@ -151,6 +151,12 @@ interface BundleStorageService {
|
|
|
151
151
|
|
|
152
152
|
suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = false
|
|
153
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
|
+
|
|
154
160
|
/**
|
|
155
161
|
* Marks the current launch as successful after the first content appeared.
|
|
156
162
|
*/
|
|
@@ -162,7 +168,8 @@ interface BundleStorageService {
|
|
|
162
168
|
fun notifyAppReady(): Map<String, Any?>
|
|
163
169
|
|
|
164
170
|
/**
|
|
165
|
-
* 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])
|
|
166
173
|
* @return CrashedHistory containing crashed bundles
|
|
167
174
|
*/
|
|
168
175
|
fun getCrashHistory(): CrashedHistory
|
|
@@ -592,6 +599,16 @@ class BundleFileStorageService(
|
|
|
592
599
|
private var hasPreparedLaunch = false
|
|
593
600
|
private var currentLaunchReport: LaunchReport? = null
|
|
594
601
|
|
|
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
|
|
611
|
+
|
|
595
612
|
@Volatile
|
|
596
613
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
|
|
597
614
|
private val activeBundleMetadataLock = Any()
|
|
@@ -1092,7 +1109,14 @@ class BundleFileStorageService(
|
|
|
1092
1109
|
)
|
|
1093
1110
|
}
|
|
1094
1111
|
|
|
1095
|
-
|
|
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 {
|
|
1096
1120
|
val metadata = loadMetadataOrNull() ?: return false
|
|
1097
1121
|
if (metadata.stagingBundleId != stagingBundleId) {
|
|
1098
1122
|
return false
|
|
@@ -1100,9 +1124,7 @@ class BundleFileStorageService(
|
|
|
1100
1124
|
|
|
1101
1125
|
Log.w(TAG, "Rolling back crashed staging bundle: $stagingBundleId")
|
|
1102
1126
|
|
|
1103
|
-
|
|
1104
|
-
crashedHistory.addEntry(stagingBundleId)
|
|
1105
|
-
saveCrashedHistory(crashedHistory)
|
|
1127
|
+
recordFailedLaunch(stagingBundleId, unfinishedLaunch)
|
|
1106
1128
|
|
|
1107
1129
|
val fallbackBundleId =
|
|
1108
1130
|
metadata.stableBundleId?.takeIf { candidate ->
|
|
@@ -1369,18 +1391,98 @@ class BundleFileStorageService(
|
|
|
1369
1391
|
|
|
1370
1392
|
private fun saveCrashedHistory(history: CrashedHistory): Boolean = history.saveToFile(getCrashedHistoryFile())
|
|
1371
1393
|
|
|
1372
|
-
private fun isBundleInCrashedHistory(bundleId: String): Boolean =
|
|
1394
|
+
private fun isBundleInCrashedHistory(bundleId: String): Boolean = getCrashHistory().contains(bundleId)
|
|
1373
1395
|
|
|
1374
|
-
|
|
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
|
+
}
|
|
1375
1412
|
|
|
1376
1413
|
override fun clearCrashHistory(): Boolean {
|
|
1377
|
-
|
|
1378
|
-
|
|
1414
|
+
synchronized(crashedHistoryLock) {
|
|
1415
|
+
saveCrashedHistory(CrashedHistory())
|
|
1416
|
+
retryHeldBundleId = null
|
|
1417
|
+
}
|
|
1379
1418
|
Log.d(TAG, "Cleared crash history")
|
|
1380
1419
|
return true
|
|
1381
1420
|
}
|
|
1382
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
|
+
|
|
1383
1484
|
override fun markLaunchCompleted(currentBundleId: String?) {
|
|
1485
|
+
readyInterruptedLaunchRetry(currentBundleId)
|
|
1384
1486
|
val metadata = loadMetadataOrNull() ?: return
|
|
1385
1487
|
val stagingBundleId = metadata.stagingBundleId ?: return
|
|
1386
1488
|
if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
|
|
@@ -1418,7 +1520,11 @@ class BundleFileStorageService(
|
|
|
1418
1520
|
val report = loadLaunchReport()
|
|
1419
1521
|
if (report == null) {
|
|
1420
1522
|
val metadata = loadMetadataOrNull()
|
|
1421
|
-
|
|
1523
|
+
val launchedBundleId = pendingLaunchBundleId
|
|
1524
|
+
if (launchedBundleId != null &&
|
|
1525
|
+
metadata?.verificationPending == true &&
|
|
1526
|
+
metadata.stagingBundleId == launchedBundleId
|
|
1527
|
+
) {
|
|
1422
1528
|
return mapOf("status" to "PENDING")
|
|
1423
1529
|
}
|
|
1424
1530
|
return mapOf("status" to "UNCHANGED")
|
|
@@ -1483,17 +1589,14 @@ class BundleFileStorageService(
|
|
|
1483
1589
|
if (!hasPreparedLaunch) {
|
|
1484
1590
|
val metadata = loadMetadataOrNull()
|
|
1485
1591
|
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1486
|
-
metadata.stagingBundleId?.let { rollbackPendingBundle(it) }
|
|
1592
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
|
|
1487
1593
|
}
|
|
1488
1594
|
}
|
|
1489
1595
|
hasPreparedLaunch = true
|
|
1490
1596
|
|
|
1597
|
+
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
1491
1598
|
val selection = selectLaunch()
|
|
1492
|
-
|
|
1493
|
-
loadMetadataOrNull()?.let { metadata ->
|
|
1494
|
-
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1599
|
+
pendingLaunchBundleId = selection.launchedBundleId.takeIf { selection.shouldRollbackOnCrash }
|
|
1497
1600
|
Log.d(
|
|
1498
1601
|
TAG,
|
|
1499
1602
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -296,12 +296,44 @@ data class CrashedBundleEntry(
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/**
|
|
300
|
+
* A bundle whose launch ended before first content without a crash, waiting
|
|
301
|
+
* for its one retry. Such a launch cannot tell a hang from a user leaving
|
|
302
|
+
* early, so the bundle is offered again instead of entering crash history.
|
|
303
|
+
* A second unfinished launch of it adds it there.
|
|
304
|
+
*
|
|
305
|
+
* [retryReady] turns true once a later launch shows content. The retry
|
|
306
|
+
* starts with the next process, so the session that recovered never reloads
|
|
307
|
+
* into the same bundle.
|
|
308
|
+
*/
|
|
309
|
+
data class InterruptedLaunch(
|
|
310
|
+
val bundleId: String,
|
|
311
|
+
val retryReady: Boolean = false,
|
|
312
|
+
) {
|
|
313
|
+
companion object {
|
|
314
|
+
fun fromJson(json: JSONObject): InterruptedLaunch? =
|
|
315
|
+
json.optNullableString("bundleId")?.let { bundleId ->
|
|
316
|
+
InterruptedLaunch(
|
|
317
|
+
bundleId = bundleId,
|
|
318
|
+
retryReady = json.optBoolean("retryReady", false),
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fun toJson(): JSONObject =
|
|
324
|
+
JSONObject().apply {
|
|
325
|
+
put("bundleId", bundleId)
|
|
326
|
+
put("retryReady", retryReady)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
299
330
|
/**
|
|
300
331
|
* History of crashed bundles
|
|
301
332
|
*/
|
|
302
333
|
data class CrashedHistory(
|
|
303
334
|
val bundles: MutableList<CrashedBundleEntry> = mutableListOf(),
|
|
304
335
|
val maxHistorySize: Int = DEFAULT_MAX_HISTORY_SIZE,
|
|
336
|
+
var interruptedLaunch: InterruptedLaunch? = null,
|
|
305
337
|
) {
|
|
306
338
|
companion object {
|
|
307
339
|
private const val TAG = "CrashedHistory"
|
|
@@ -317,6 +349,7 @@ data class CrashedHistory(
|
|
|
317
349
|
return CrashedHistory(
|
|
318
350
|
bundles = bundles,
|
|
319
351
|
maxHistorySize = json.optInt("maxHistorySize", DEFAULT_MAX_HISTORY_SIZE),
|
|
352
|
+
interruptedLaunch = json.optJSONObject("interruptedLaunch")?.let(InterruptedLaunch::fromJson),
|
|
320
353
|
)
|
|
321
354
|
}
|
|
322
355
|
|
|
@@ -342,6 +375,7 @@ data class CrashedHistory(
|
|
|
342
375
|
bundles.forEach { bundlesArray.put(it.toJson()) }
|
|
343
376
|
put("bundles", bundlesArray)
|
|
344
377
|
put("maxHistorySize", maxHistorySize)
|
|
378
|
+
interruptedLaunch?.let { put("interruptedLaunch", it.toJson()) }
|
|
345
379
|
}
|
|
346
380
|
|
|
347
381
|
fun saveToFile(file: File): Boolean =
|
|
@@ -389,6 +423,7 @@ data class CrashedHistory(
|
|
|
389
423
|
|
|
390
424
|
fun clear() {
|
|
391
425
|
bundles.clear()
|
|
426
|
+
interruptedLaunch = null
|
|
392
427
|
}
|
|
393
428
|
}
|
|
394
429
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityOptions
|
|
4
|
+
import android.app.Application
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
6
7
|
import android.os.Process
|
|
@@ -20,6 +21,7 @@ class HotUpdaterImpl {
|
|
|
20
21
|
private val releaseCatalogCache: ReleaseCatalogCacheService
|
|
21
22
|
private val installIdentity: InstallIdentityService
|
|
22
23
|
private val keyValueStorage: KeyValueStorageService
|
|
24
|
+
private val activityStartGate: ActivityStartGate
|
|
23
25
|
private var currentLaunchSelection: LaunchSelection? = null
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -33,6 +35,7 @@ class HotUpdaterImpl {
|
|
|
33
35
|
this.context = context.applicationContext
|
|
34
36
|
this.bundleStorage = bundleStorage
|
|
35
37
|
this.preferences = preferences
|
|
38
|
+
this.activityStartGate = ActivityStartGate(this.context as? Application)
|
|
36
39
|
this.recoveryManager = HotUpdaterRecoveryManager(this.context)
|
|
37
40
|
this.releaseCatalogCache =
|
|
38
41
|
ReleaseCatalogCacheService(
|
|
@@ -40,7 +43,6 @@ class HotUpdaterImpl {
|
|
|
40
43
|
)
|
|
41
44
|
this.installIdentity = InstallIdentityService.create(this.context)
|
|
42
45
|
this.keyValueStorage = KeyValueStorageService.create(this.context)
|
|
43
|
-
PreviousProcessExit.initialize(this.context)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -379,15 +381,7 @@ class HotUpdaterImpl {
|
|
|
379
381
|
}
|
|
380
382
|
}
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
* The launch report for this process, with `previousProcessExit` when
|
|
384
|
-
* Android reports why the previous main process exited.
|
|
385
|
-
*/
|
|
386
|
-
fun notifyAppReady(): Map<String, Any?> {
|
|
387
|
-
val report = bundleStorage.notifyAppReady()
|
|
388
|
-
val previousProcessExit = PreviousProcessExit.get() ?: return report
|
|
389
|
-
return report + ("previousProcessExit" to previousProcessExit)
|
|
390
|
-
}
|
|
384
|
+
fun notifyAppReady(): Map<String, Any?> = bundleStorage.notifyAppReady()
|
|
391
385
|
|
|
392
386
|
fun acceptReleaseCatalog(
|
|
393
387
|
catalogId: String,
|
|
@@ -517,6 +511,16 @@ class HotUpdaterImpl {
|
|
|
517
511
|
|
|
518
512
|
val pendingRecovery = recoveryManager.consumePendingCrashRecovery()
|
|
519
513
|
val selection = bundleStorage.prepareLaunch(pendingRecovery)
|
|
514
|
+
// A headless JS task, such as a background push, loads the bundle without
|
|
515
|
+
// an activity and never renders. Only a launch with an activity may leave
|
|
516
|
+
// an unfinished launch for the next process to roll back.
|
|
517
|
+
activityStartGate.runWhenActivityStarted(
|
|
518
|
+
if (selection.shouldRollbackOnCrash) {
|
|
519
|
+
{ bundleStorage.markLaunchStarted(selection.launchedBundleId) }
|
|
520
|
+
} else {
|
|
521
|
+
null
|
|
522
|
+
},
|
|
523
|
+
)
|
|
520
524
|
recoveryManager.startMonitoring(
|
|
521
525
|
bundleId = selection.launchedBundleId,
|
|
522
526
|
shouldRollback = selection.shouldRollbackOnCrash,
|