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.PREVIOUS_PATH
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 oldPath = sharedPrefs.getString(PREVIOUS_PATH)
29
- if (oldPath != "") {
30
- val isDeleted = utils.deleteOldBundleIfneeded(PATH)
31
- if (isDeleted) {
32
- sharedPrefs.putString(PATH, oldPath)
33
- sharedPrefs.putString(PREVIOUS_PATH, "")
34
- } else {
35
- sharedPrefs.putString(PATH, "")
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
- } else {
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
- utils.deleteOldBundleIfneeded(bundle.path)
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(path: String?, extension: String?, version: Int?, maxVersions: Int?, metadata: String?): Boolean {
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(path: String?, extension: String?, version: Double?, maxVersions: Double?, metadata: String?, promise: Promise) {
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 context: Context? = reactApplicationContext.currentActivity
293
- ProcessPhoenix.triggerRebirth(context)
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
- fileWriterExecutor.execute {
531
- try {
532
- // Decode base64 to bytes
533
- val bytes = Base64.decode(base64Content, Base64.DEFAULT)
540
+ }
534
541
 
535
- // Ensure parent directory exists
536
- val file = File(path)
537
- val parentDir = file.parentFile
538
- if (parentDir != null && !parentDir.exists()) {
539
- parentDir.mkdirs()
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
- // Write file on background thread
543
- FileOutputStream(file).use { fos ->
544
- fos.write(bytes)
545
- fos.flush()
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
- // Resolve on UI thread (React Native requirement)
549
- UiThreadUtil.runOnUiThread {
550
- promise.resolve(true)
551
- }
552
- } catch (e: IOException) {
553
- UiThreadUtil.runOnUiThread {
554
- promise.reject("WRITE_ERROR", "Failed to write file: ${e.message}", e)
555
- }
556
- } catch (e: Exception) {
557
- UiThreadUtil.runOnUiThread {
558
- promise.reject("WRITE_ERROR", "Unexpected error: ${e.message}", e)
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 {
@@ -39,16 +39,24 @@ RCT_EXPORT_MODULE()
39
39
  void OTASignalHandler(int sig) {
40
40
  if (isBeginning) {
41
41
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
42
- NSString *oldPath = [defaults stringForKey:@"OLD_PATH"];
43
- if (oldPath) {
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:oldPath forKey:@"PATH"];
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
- NSString *oldPath = [defaults stringForKey:@"OLD_PATH"];
62
- if (oldPath) {
63
- BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
64
- if (isDeleted) {
65
- [defaults setObject:oldPath forKey:@"PATH"];
66
- [defaults removeObjectForKey:@"OLD_PATH"];
67
- } else {
68
- [defaults removeObjectForKey:@"OLD_PATH"];
69
- [defaults removeObjectForKey:@"PATH"];
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
- [defaults removeObjectForKey:@"PATH"];
82
+ [defaults removeObjectForKey:@"PATH"];
73
83
  }
74
- [defaults synchronize];
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"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ota-hot-update",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "Hot update for react native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",