react-native-ota-hot-update 2.4.1 → 2.4.3
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.
|
@@ -4,12 +4,15 @@ import android.content.Context
|
|
|
4
4
|
import android.widget.Toast
|
|
5
5
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
|
6
6
|
import com.rnhotupdate.Common.PATH
|
|
7
|
-
import com.rnhotupdate.Common.
|
|
7
|
+
import com.rnhotupdate.Common.VERSION
|
|
8
|
+
import com.rnhotupdate.Common.BUNDLE_HISTORY
|
|
8
9
|
import com.rnhotupdate.SharedPrefs
|
|
9
10
|
import kotlinx.coroutines.Dispatchers
|
|
10
11
|
import kotlinx.coroutines.GlobalScope
|
|
11
12
|
import kotlinx.coroutines.delay
|
|
12
13
|
import kotlinx.coroutines.launch
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import java.io.File
|
|
13
16
|
|
|
14
17
|
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
|
|
15
18
|
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
@@ -23,20 +26,45 @@ class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandl
|
|
|
23
26
|
}
|
|
24
27
|
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
|
25
28
|
if (beginning) {
|
|
26
|
-
//begin remove and using previous bundle
|
|
27
29
|
val sharedPrefs = SharedPrefs(context)
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
val currentPath = sharedPrefs.getString(PATH)
|
|
31
|
+
|
|
32
|
+
// Try to rollback using history system
|
|
33
|
+
val historyJson = sharedPrefs.getString(BUNDLE_HISTORY)
|
|
34
|
+
var rolledBack = false
|
|
35
|
+
|
|
36
|
+
if (!historyJson.isNullOrEmpty() && !currentPath.isNullOrEmpty()) {
|
|
37
|
+
try {
|
|
38
|
+
val jsonArray = JSONArray(historyJson)
|
|
39
|
+
val history = (0 until jsonArray.length()).map { i ->
|
|
40
|
+
val obj = jsonArray.getJSONObject(i)
|
|
41
|
+
Pair(obj.getInt("version"), obj.getString("path"))
|
|
42
|
+
}.sortedByDescending { it.first }
|
|
43
|
+
|
|
44
|
+
val currentBundle = history.find { it.second == currentPath }
|
|
45
|
+
if (currentBundle != null) {
|
|
46
|
+
val previousBundle = history
|
|
47
|
+
.filter { it.first < currentBundle.first }
|
|
48
|
+
.maxByOrNull { it.first }
|
|
49
|
+
|
|
50
|
+
if (previousBundle != null && File(previousBundle.second).exists()) {
|
|
51
|
+
val isDeleted = utils.deleteOldBundleIfneeded(PATH)
|
|
52
|
+
if (isDeleted) {
|
|
53
|
+
sharedPrefs.putString(PATH, previousBundle.second)
|
|
54
|
+
sharedPrefs.putString(VERSION, previousBundle.first.toString())
|
|
55
|
+
rolledBack = true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
// ignore, fall through to clear path
|
|
36
61
|
}
|
|
37
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!rolledBack) {
|
|
38
65
|
sharedPrefs.putString(PATH, "")
|
|
39
66
|
}
|
|
67
|
+
|
|
40
68
|
val errorMessage = throwable.message ?: "Unknown error occurred"
|
|
41
69
|
Toast.makeText(context, "Update failed: $errorMessage", Toast.LENGTH_LONG).show()
|
|
42
70
|
GlobalScope.launch(Dispatchers.IO) {
|
|
@@ -48,4 +76,3 @@ class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandl
|
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
|
-
|
|
@@ -14,7 +14,6 @@ import com.rnhotupdate.Common.VERSION
|
|
|
14
14
|
import com.rnhotupdate.Common.PREVIOUS_VERSION
|
|
15
15
|
import com.rnhotupdate.Common.METADATA
|
|
16
16
|
import com.rnhotupdate.Common.BUNDLE_HISTORY
|
|
17
|
-
import com.rnhotupdate.Common.DEFAULT_MAX_BUNDLE_VERSIONS
|
|
18
17
|
import com.rnhotupdate.SharedPrefs
|
|
19
18
|
import kotlinx.coroutines.CoroutineScope
|
|
20
19
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -189,7 +188,11 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
|
|
|
189
188
|
val versionsToKeep = finalHistory.map { it.version }.toSet()
|
|
190
189
|
updatedHistory.forEach { bundle ->
|
|
191
190
|
if (bundle.version !in versionsToKeep) {
|
|
192
|
-
|
|
191
|
+
val bundleFile = File(bundle.path)
|
|
192
|
+
val parentDir = bundleFile.parentFile
|
|
193
|
+
if (parentDir != null && parentDir.exists() && parentDir.isDirectory) {
|
|
194
|
+
utils.deleteDirectory(parentDir)
|
|
195
|
+
}
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -201,7 +204,13 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
|
|
|
201
204
|
sharedPrefs.putString(VERSION, version.toString())
|
|
202
205
|
}
|
|
203
206
|
|
|
204
|
-
private fun processBundleFile(
|
|
207
|
+
private fun processBundleFile(
|
|
208
|
+
path: String?,
|
|
209
|
+
extension: String?,
|
|
210
|
+
version: Int?,
|
|
211
|
+
maxVersions: Int?,
|
|
212
|
+
metadata: String?
|
|
213
|
+
): Boolean {
|
|
205
214
|
if (path != null) {
|
|
206
215
|
val file = File(path)
|
|
207
216
|
if (file.exists() && file.isFile) {
|
|
@@ -237,8 +246,16 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
|
|
|
237
246
|
throw Exception("Invalid path: $path")
|
|
238
247
|
}
|
|
239
248
|
}
|
|
249
|
+
|
|
240
250
|
@ReactMethod
|
|
241
|
-
override fun setupBundlePath(
|
|
251
|
+
override fun setupBundlePath(
|
|
252
|
+
path: String?,
|
|
253
|
+
extension: String?,
|
|
254
|
+
version: Double?,
|
|
255
|
+
maxVersions: Double?,
|
|
256
|
+
metadata: String?,
|
|
257
|
+
promise: Promise
|
|
258
|
+
) {
|
|
242
259
|
scope.launch {
|
|
243
260
|
try {
|
|
244
261
|
val versionInt = version?.toInt()
|
|
@@ -289,8 +306,11 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
|
|
|
289
306
|
|
|
290
307
|
@ReactMethod
|
|
291
308
|
override fun restart() {
|
|
292
|
-
val
|
|
293
|
-
|
|
309
|
+
val activity = reactApplicationContext.currentActivity
|
|
310
|
+
val context: Context = activity ?: reactApplicationContext
|
|
311
|
+
UiThreadUtil.runOnUiThread {
|
|
312
|
+
ProcessPhoenix.triggerRebirth(context)
|
|
313
|
+
}
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
@ReactMethod
|
|
@@ -516,49 +536,52 @@ class OtaHotUpdateModule internal constructor(context: ReactApplicationContext)
|
|
|
516
536
|
}
|
|
517
537
|
}
|
|
518
538
|
}
|
|
519
|
-
/**
|
|
520
|
-
* Write file with base64 content on native thread
|
|
521
|
-
* This runs on a background thread, not blocking JS thread
|
|
522
|
-
*/
|
|
523
|
-
@ReactMethod
|
|
524
|
-
override fun writeFile(path: String?, base64Content: String?, encoding: String?, promise: Promise) {
|
|
525
|
-
if (path == null || base64Content == null) {
|
|
526
|
-
promise.reject("INVALID_ARG", "Path and base64Content are required", null)
|
|
527
|
-
return
|
|
528
|
-
}
|
|
529
539
|
|
|
530
|
-
|
|
531
|
-
try {
|
|
532
|
-
// Decode base64 to bytes
|
|
533
|
-
val bytes = Base64.decode(base64Content, Base64.DEFAULT)
|
|
540
|
+
}
|
|
534
541
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
override fun writeFile(
|
|
543
|
+
path: String?,
|
|
544
|
+
base64Content: String?,
|
|
545
|
+
encoding: String?,
|
|
546
|
+
promise: Promise
|
|
547
|
+
) {
|
|
548
|
+
if (path == null || base64Content == null) {
|
|
549
|
+
promise.reject("INVALID_ARG", "Path and base64Content are required", null)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
541
552
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
553
|
+
fileWriterExecutor.execute {
|
|
554
|
+
try {
|
|
555
|
+
// Decode base64 to bytes
|
|
556
|
+
val bytes = Base64.decode(base64Content, Base64.DEFAULT)
|
|
557
|
+
|
|
558
|
+
// Ensure parent directory exists
|
|
559
|
+
val file = File(path)
|
|
560
|
+
val parentDir = file.parentFile
|
|
561
|
+
if (parentDir != null && !parentDir.exists()) {
|
|
562
|
+
parentDir.mkdirs()
|
|
563
|
+
}
|
|
547
564
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
565
|
+
// Write file on background thread
|
|
566
|
+
FileOutputStream(file).use { fos ->
|
|
567
|
+
fos.write(bytes)
|
|
568
|
+
fos.flush()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Resolve on UI thread (React Native requirement)
|
|
572
|
+
UiThreadUtil.runOnUiThread {
|
|
573
|
+
promise.resolve(true)
|
|
574
|
+
}
|
|
575
|
+
} catch (e: IOException) {
|
|
576
|
+
UiThreadUtil.runOnUiThread {
|
|
577
|
+
promise.reject("WRITE_ERROR", "Failed to write file: ${e.message}", e)
|
|
578
|
+
}
|
|
579
|
+
} catch (e: Exception) {
|
|
580
|
+
UiThreadUtil.runOnUiThread {
|
|
581
|
+
promise.reject("WRITE_ERROR", "Unexpected error: ${e.message}", e)
|
|
561
582
|
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
562
585
|
}
|
|
563
586
|
|
|
564
587
|
companion object {
|
package/ios/OtaHotUpdate.mm
CHANGED
|
@@ -39,16 +39,24 @@ RCT_EXPORT_MODULE()
|
|
|
39
39
|
void OTASignalHandler(int sig) {
|
|
40
40
|
if (isBeginning) {
|
|
41
41
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Use PREVIOUS_BUNDLE_PATH (simple key, written by saveBundleVersion before each update)
|
|
43
|
+
NSString *previousPath = [defaults stringForKey:@"PREVIOUS_BUNDLE_PATH"];
|
|
44
|
+
if (previousPath && previousPath.length > 0) {
|
|
44
45
|
BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
|
|
45
46
|
if (isDeleted) {
|
|
46
|
-
[defaults setObject:
|
|
47
|
+
[defaults setObject:previousPath forKey:@"PATH"];
|
|
48
|
+
NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_BUNDLE_VERSION"];
|
|
49
|
+
if (previousVersion) {
|
|
50
|
+
[defaults setObject:previousVersion forKey:@"VERSION"];
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
[defaults removeObjectForKey:@"PATH"];
|
|
47
54
|
}
|
|
48
|
-
[defaults removeObjectForKey:@"OLD_PATH"];
|
|
49
55
|
} else {
|
|
50
56
|
[defaults removeObjectForKey:@"PATH"];
|
|
51
57
|
}
|
|
58
|
+
[defaults removeObjectForKey:@"PREVIOUS_BUNDLE_PATH"];
|
|
59
|
+
[defaults removeObjectForKey:@"PREVIOUS_BUNDLE_VERSION"];
|
|
52
60
|
[defaults synchronize];
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -58,20 +66,24 @@ void OTASignalHandler(int sig) {
|
|
|
58
66
|
void OTAExceptionHandler(NSException *exception) {
|
|
59
67
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
60
68
|
if (isBeginning) {
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
NSString *previousPath = [defaults stringForKey:@"PREVIOUS_BUNDLE_PATH"];
|
|
70
|
+
if (previousPath && previousPath.length > 0) {
|
|
71
|
+
BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
|
|
72
|
+
if (isDeleted) {
|
|
73
|
+
[defaults setObject:previousPath forKey:@"PATH"];
|
|
74
|
+
NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_BUNDLE_VERSION"];
|
|
75
|
+
if (previousVersion) {
|
|
76
|
+
[defaults setObject:previousVersion forKey:@"VERSION"];
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
[defaults removeObjectForKey:@"PATH"];
|
|
80
|
+
}
|
|
71
81
|
} else {
|
|
72
|
-
|
|
82
|
+
[defaults removeObjectForKey:@"PATH"];
|
|
73
83
|
}
|
|
74
|
-
|
|
84
|
+
[defaults removeObjectForKey:@"PREVIOUS_BUNDLE_PATH"];
|
|
85
|
+
[defaults removeObjectForKey:@"PREVIOUS_BUNDLE_VERSION"];
|
|
86
|
+
[defaults synchronize];
|
|
75
87
|
} else if (previousHandler) {
|
|
76
88
|
previousHandler(exception);
|
|
77
89
|
}
|
|
@@ -300,12 +312,10 @@ RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extensi
|
|
|
300
312
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
301
313
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
302
314
|
if ([OtaHotUpdate isFilePathValid:path]) {
|
|
303
|
-
[OtaHotUpdate removeBundleIfNeeded:nil];
|
|
304
315
|
//Unzip file
|
|
305
316
|
NSString *extractedFilePath = [self unzipFileAtPath:path extension:(extension != nil) ? extension : @".jsbundle" version:version];
|
|
306
317
|
if (extractedFilePath) {
|
|
307
318
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
308
|
-
NSString *oldPath = [defaults stringForKey:@"PATH"];
|
|
309
319
|
|
|
310
320
|
// If version is provided, save to history system
|
|
311
321
|
if (version != nil) {
|
|
@@ -628,6 +638,16 @@ RCT_EXPORT_METHOD(setExactBundlePath:(NSString *)path
|
|
|
628
638
|
// Save updated history
|
|
629
639
|
[self saveBundleHistory:finalHistory];
|
|
630
640
|
|
|
641
|
+
// Before updating current path, save it as fallback for crash handler
|
|
642
|
+
NSString *currentPath = [defaults stringForKey:@"PATH"];
|
|
643
|
+
NSString *currentVersion = [defaults stringForKey:@"VERSION"];
|
|
644
|
+
if (currentPath && currentPath.length > 0) {
|
|
645
|
+
[defaults setObject:currentPath forKey:@"PREVIOUS_BUNDLE_PATH"];
|
|
646
|
+
if (currentVersion) {
|
|
647
|
+
[defaults setObject:currentVersion forKey:@"PREVIOUS_BUNDLE_VERSION"];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
631
651
|
// Set current path and version
|
|
632
652
|
[defaults setObject:path forKey:@"PATH"];
|
|
633
653
|
[defaults setObject:[NSString stringWithFormat:@"%ld", (long)version] forKey:@"VERSION"];
|