@hot-updater/react-native 0.8.0 → 0.10.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.
@@ -11,6 +11,7 @@ buildscript {
11
11
  classpath "com.android.tools.build:gradle:7.2.1"
12
12
  // noinspection DifferentKotlinGradleVersion
13
13
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
+
14
15
  }
15
16
  }
16
17
 
@@ -122,6 +123,9 @@ dependencies {
122
123
  implementation 'com.facebook.react:react-native:+'
123
124
  }
124
125
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
126
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
127
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
128
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
125
129
  }
126
130
 
127
131
  if (isNewArchitectureEnabled()) {
@@ -12,6 +12,8 @@ import com.facebook.react.bridge.NativeModule
12
12
  import com.facebook.react.bridge.ReactApplicationContext
13
13
  import com.facebook.react.uimanager.ReactShadowNode
14
14
  import com.facebook.react.uimanager.ViewManager
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.withContext
15
17
  import java.io.File
16
18
  import java.net.HttpURLConnection
17
19
  import java.net.URL
@@ -28,9 +30,9 @@ class HotUpdater : ReactPackage {
28
30
  context: Context,
29
31
  basePath: String,
30
32
  ): String {
31
- val documentsDir = context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
33
+ val documentsDir =
34
+ context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
32
35
  val separator = if (basePath.startsWith("/")) "" else "/"
33
-
34
36
  return "$documentsDir$separator$basePath"
35
37
  }
36
38
 
@@ -65,12 +67,11 @@ class HotUpdater : ReactPackage {
65
67
  }
66
68
 
67
69
  val reactIntegrationManager = ReactIntegrationManager(context)
68
-
69
70
  val activity: Activity? = getCurrentActivity(context)
70
- val reactApplication: ReactApplication = reactIntegrationManager.getReactApplication(activity?.application)
71
- val bundleURL = getJSBundleFile(context)
72
-
73
- reactIntegrationManager.setJSBundle(reactApplication, bundleURL)
71
+ val reactApplication: ReactApplication =
72
+ reactIntegrationManager.getReactApplication(activity?.application)
73
+ val newBundleURL = getJSBundleFile(context)
74
+ reactIntegrationManager.setJSBundle(reactApplication, newBundleURL)
74
75
  }
75
76
 
76
77
  private fun extractZipFileAtPath(
@@ -106,19 +107,17 @@ class HotUpdater : ReactPackage {
106
107
 
107
108
  fun reload(context: Context) {
108
109
  val reactIntegrationManager = ReactIntegrationManager(context)
109
-
110
110
  val activity: Activity? = getCurrentActivity(context)
111
- val reactApplication: ReactApplication = reactIntegrationManager.getReactApplication(activity?.application)
111
+ val reactApplication: ReactApplication =
112
+ reactIntegrationManager.getReactApplication(activity?.application)
112
113
  val bundleURL = getJSBundleFile(context)
113
-
114
114
  reactIntegrationManager.setJSBundle(reactApplication, bundleURL)
115
-
116
115
  Handler(Looper.getMainLooper()).post {
117
116
  reactIntegrationManager.reload(reactApplication)
118
117
  }
119
118
  }
120
119
 
121
- public fun getJSBundleFile(context: Context): String {
120
+ fun getJSBundleFile(context: Context): String {
122
121
  val sharedPreferences =
123
122
  context.getSharedPreferences("HotUpdaterPrefs", Context.MODE_PRIVATE)
124
123
  val urlString = sharedPreferences.getString("HotUpdaterBundleURL", null)
@@ -135,7 +134,7 @@ class HotUpdater : ReactPackage {
135
134
  return urlString
136
135
  }
137
136
 
138
- fun updateBundle(
137
+ suspend fun updateBundle(
139
138
  context: Context,
140
139
  bundleId: String,
141
140
  zipUrl: String?,
@@ -148,66 +147,81 @@ class HotUpdater : ReactPackage {
148
147
  }
149
148
 
150
149
  val downloadUrl = URL(zipUrl)
151
-
152
150
  val basePath = stripPrefixFromPath(bundleId, downloadUrl.path)
153
151
  val path = convertFileSystemPathFromBasePath(context, basePath)
154
152
 
155
- var connection: HttpURLConnection? = null
156
- try {
157
- connection = downloadUrl.openConnection() as HttpURLConnection
158
- connection.connect()
159
-
160
- val totalSize = connection.contentLength
161
- if (totalSize <= 0) {
162
- Log.d("HotUpdater", "Invalid content length: $totalSize")
163
- return false
164
- }
165
-
166
- val file = File(path)
167
- file.parentFile?.mkdirs()
153
+ val isSuccess =
154
+ withContext(Dispatchers.IO) {
155
+ val conn =
156
+ try {
157
+ downloadUrl.openConnection() as HttpURLConnection
158
+ } catch (e: Exception) {
159
+ Log.d("HotUpdater", "Failed to open connection: ${e.message}")
160
+ return@withContext false
161
+ }
168
162
 
169
- connection.inputStream.use { input ->
170
- file.outputStream().use { output ->
171
- val buffer = ByteArray(8 * 1024)
172
- var bytesRead: Int
173
- var totalRead = 0L
163
+ try {
164
+ conn.connect()
165
+ val totalSize = conn.contentLength
166
+ if (totalSize <= 0) {
167
+ Log.d("HotUpdater", "Invalid content length: $totalSize")
168
+ return@withContext false
169
+ }
174
170
 
175
- while (input.read(buffer).also { bytesRead = it } != -1) {
176
- output.write(buffer, 0, bytesRead)
177
- totalRead += bytesRead
178
- val progress = (totalRead.toDouble() / totalSize)
179
- progressCallback.invoke(progress)
171
+ val file = File(path)
172
+ file.parentFile?.mkdirs()
173
+
174
+ conn.inputStream.use { input ->
175
+ file.outputStream().use { output ->
176
+ val buffer = ByteArray(8 * 1024)
177
+ var bytesRead: Int
178
+ var totalRead = 0L
179
+ var lastProgressTime = System.currentTimeMillis()
180
+
181
+ while (input.read(buffer).also { bytesRead = it } != -1) {
182
+ output.write(buffer, 0, bytesRead)
183
+ totalRead += bytesRead
184
+ val currentTime = System.currentTimeMillis()
185
+ if (currentTime - lastProgressTime >= 100) { // Check every 100ms
186
+ val progress = totalRead.toDouble() / totalSize
187
+ progressCallback.invoke(progress)
188
+ lastProgressTime = currentTime
189
+ }
190
+ }
191
+ // Send final progress (100%) after download completes
192
+ progressCallback.invoke(1.0)
193
+ }
180
194
  }
195
+ } catch (e: Exception) {
196
+ Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
197
+ return@withContext false
198
+ } finally {
199
+ conn.disconnect()
181
200
  }
182
- }
183
- } catch (e: Exception) {
184
- Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
185
- return false
186
- } finally {
187
- connection?.disconnect()
188
- }
189
201
 
190
- val extractedPath = File(path).parentFile?.path ?: return false
202
+ val extractedPath = File(path).parentFile?.path ?: return@withContext false
191
203
 
192
- if (!extractZipFileAtPath(path, extractedPath)) {
193
- Log.d("HotUpdater", "Failed to extract zip file.")
194
- return false
195
- }
204
+ if (!extractZipFileAtPath(path, extractedPath)) {
205
+ Log.d("HotUpdater", "Failed to extract zip file.")
206
+ return@withContext false
207
+ }
196
208
 
197
- val extractedDirectory = File(extractedPath)
198
- val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
209
+ val extractedDirectory = File(extractedPath)
210
+ val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
199
211
 
200
- if (indexFile != null) {
201
- val bundlePath = indexFile.path
202
- Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
203
- setBundleURL(context, bundlePath)
204
- } else {
205
- Log.d("HotUpdater", "index.android.bundle not found.")
206
- return false
207
- }
212
+ if (indexFile != null) {
213
+ val bundlePath = indexFile.path
214
+ Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
215
+ setBundleURL(context, bundlePath)
216
+ } else {
217
+ Log.d("HotUpdater", "index.android.bundle not found.")
218
+ return@withContext false
219
+ }
208
220
 
209
- Log.d("HotUpdater", "Downloaded and extracted file successfully.")
210
- return true
221
+ Log.d("HotUpdater", "Downloaded and extracted file successfully.")
222
+ true
223
+ }
224
+ return isSuccess
211
225
  }
212
226
  }
213
227
  }
@@ -1,10 +1,13 @@
1
1
  package com.hotupdater
2
2
 
3
+ import androidx.fragment.app.FragmentActivity
4
+ import androidx.lifecycle.lifecycleScope
3
5
  import com.facebook.react.bridge.Promise
4
6
  import com.facebook.react.bridge.ReactApplicationContext
5
7
  import com.facebook.react.bridge.ReactMethod
6
8
  import com.facebook.react.bridge.WritableNativeMap
7
9
  import com.facebook.react.modules.core.DeviceEventManagerModule
10
+ import kotlinx.coroutines.launch
8
11
 
9
12
  class HotUpdaterModule internal constructor(
10
13
  context: ReactApplicationContext,
@@ -29,18 +32,26 @@ class HotUpdaterModule internal constructor(
29
32
  zipUrl: String?,
30
33
  promise: Promise,
31
34
  ) {
32
- val isSuccess =
33
- HotUpdater.updateBundle(mReactApplicationContext, bundleId, zipUrl) { progress ->
34
- val params =
35
- WritableNativeMap().apply {
36
- putDouble("progress", progress)
37
- }
38
-
39
- this.mReactApplicationContext
40
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
41
- .emit("onProgress", params)
42
- }
43
- promise.resolve(isSuccess)
35
+ // Use lifecycleScope when currentActivity is FragmentActivity
36
+ (currentActivity as? FragmentActivity)?.lifecycleScope?.launch {
37
+ val isSuccess =
38
+ HotUpdater.updateBundle(
39
+ mReactApplicationContext,
40
+ bundleId,
41
+ zipUrl,
42
+ ) { progress ->
43
+ val params =
44
+ WritableNativeMap().apply {
45
+ putDouble("progress", progress)
46
+ }
47
+
48
+ this@HotUpdaterModule
49
+ .mReactApplicationContext
50
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
51
+ .emit("onProgress", params)
52
+ }
53
+ promise.resolve(isSuccess)
54
+ }
44
55
  }
45
56
 
46
57
  override fun addListener(eventName: String?) {
@@ -1,10 +1,13 @@
1
1
  package com.hotupdater
2
2
 
3
+ import androidx.fragment.app.FragmentActivity
4
+ import androidx.lifecycle.lifecycleScope
3
5
  import com.facebook.react.bridge.Promise
4
6
  import com.facebook.react.bridge.ReactApplicationContext
5
7
  import com.facebook.react.bridge.ReactMethod
6
8
  import com.facebook.react.bridge.WritableNativeMap
7
9
  import com.facebook.react.modules.core.DeviceEventManagerModule
10
+ import kotlinx.coroutines.launch
8
11
 
9
12
  class HotUpdaterModule internal constructor(
10
13
  context: ReactApplicationContext,
@@ -29,18 +32,26 @@ class HotUpdaterModule internal constructor(
29
32
  zipUrl: String?,
30
33
  promise: Promise,
31
34
  ) {
32
- val isSuccess =
33
- HotUpdater.updateBundle(mReactApplicationContext, bundleId, zipUrl) { progress ->
34
- val params =
35
- WritableNativeMap().apply {
36
- putDouble("progress", progress)
37
- }
38
-
39
- this.mReactApplicationContext
40
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
41
- .emit("onProgress", params)
42
- }
43
- promise.resolve(isSuccess)
35
+ // Use lifecycleScope when currentActivity is FragmentActivity
36
+ (currentActivity as? FragmentActivity)?.lifecycleScope?.launch {
37
+ val isSuccess =
38
+ HotUpdater.updateBundle(
39
+ mReactApplicationContext,
40
+ bundleId,
41
+ zipUrl,
42
+ ) { progress ->
43
+ val params =
44
+ WritableNativeMap().apply {
45
+ putDouble("progress", progress)
46
+ }
47
+
48
+ this@HotUpdaterModule
49
+ .mReactApplicationContext
50
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
51
+ .emit("onProgress", params)
52
+ }
53
+ promise.resolve(isSuccess)
54
+ }
44
55
  }
45
56
 
46
57
  companion object {
@@ -9,6 +9,7 @@
9
9
  @interface HotUpdater : RCTEventEmitter <RCTBridgeModule>
10
10
  #endif // RCT_NEW_ARCH_ENABLED
11
11
 
12
+ @property (nonatomic, assign) NSTimeInterval lastUpdateTime;
12
13
  + (NSURL *)bundleURL;
13
14
 
14
15
  @end
@@ -1,11 +1,21 @@
1
1
  #import "HotUpdater.h"
2
2
  #import <React/RCTReloadCommand.h>
3
3
  #import <SSZipArchive/SSZipArchive.h>
4
+ #import <Foundation/NSURLSession.h>
4
5
 
5
6
  @implementation HotUpdater {
6
7
  bool hasListeners;
7
8
  }
8
9
 
10
+
11
+ - (instancetype)init {
12
+ self = [super init];
13
+ if (self) {
14
+ _lastUpdateTime = 0;
15
+ }
16
+ return self;
17
+ }
18
+
9
19
  RCT_EXPORT_MODULE();
10
20
 
11
21
  #pragma mark - Bundle URL Management
@@ -153,22 +163,44 @@ RCT_EXPORT_MODULE();
153
163
  }
154
164
  }];
155
165
 
156
-
166
+
157
167
  // Add observer for progress updates
158
168
  [downloadTask addObserver:self
159
- forKeyPath:@"countOfBytesReceived"
160
- options:NSKeyValueObservingOptionNew
161
- context:nil];
169
+ forKeyPath:@"countOfBytesReceived"
170
+ options:NSKeyValueObservingOptionNew
171
+ context:nil];
162
172
  [downloadTask addObserver:self
163
- forKeyPath:@"countOfBytesExpectedToReceive"
164
- options:NSKeyValueObservingOptionNew
165
- context:nil];
166
-
173
+ forKeyPath:@"countOfBytesExpectedToReceive"
174
+ options:NSKeyValueObservingOptionNew
175
+ context:nil];
176
+
177
+ __block HotUpdater *weakSelf = self;
178
+ [[NSNotificationCenter defaultCenter] addObserverForName:@"NSURLSessionDownloadTaskDidFinishDownloading"
179
+ object:downloadTask
180
+ queue:[NSOperationQueue mainQueue]
181
+ usingBlock:^(NSNotification * _Nonnull note) {
182
+ [weakSelf removeObserversForTask:downloadTask];
183
+ }];
167
184
  [downloadTask resume];
185
+
168
186
  }
169
187
 
170
188
  #pragma mark - Progress Updates
171
189
 
190
+
191
+ - (void)removeObserversForTask:(NSURLSessionDownloadTask *)task {
192
+ @try {
193
+ if ([task observationInfo]) {
194
+ [task removeObserver:self forKeyPath:@"countOfBytesReceived"];
195
+ [task removeObserver:self forKeyPath:@"countOfBytesExpectedToReceive"];
196
+ NSLog(@"KVO observers removed successfully for task: %@", task);
197
+ }
198
+ } @catch (NSException *exception) {
199
+ NSLog(@"Failed to remove observers: %@", exception);
200
+ }
201
+ }
202
+
203
+
172
204
  - (void)observeValueForKeyPath:(NSString *)keyPath
173
205
  ofObject:(id)object
174
206
  change:(NSDictionary<NSKeyValueChangeKey, id> *)change
@@ -179,8 +211,16 @@ RCT_EXPORT_MODULE();
179
211
  if (task.countOfBytesExpectedToReceive > 0) {
180
212
  double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
181
213
 
182
- // Send progress to React Native
183
- [self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
214
+ // Get current timestamp
215
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // Convert to milliseconds
216
+
217
+ // Send event only if 100ms has passed OR progress is 100%
218
+ if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
219
+ self.lastUpdateTime = currentTime; // Update last event timestamp
220
+
221
+ // Send progress to React Native
222
+ [self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
223
+ }
184
224
  }
185
225
  }
186
226
  }
@@ -212,6 +252,7 @@ RCT_EXPORT_MODULE();
212
252
  RCT_EXPORT_METHOD(reload) {
213
253
  NSLog(@"HotUpdater requested a reload");
214
254
  dispatch_async(dispatch_get_main_queue(), ^{
255
+ [super.bridge setValue:[HotUpdater bundleURL] forKey:@"bundleURL"];
215
256
  RCTTriggerReloadCommandListeners(@"HotUpdater requested a reload");
216
257
  });
217
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -78,8 +78,8 @@
78
78
  "react-native-builder-bob": "^0.33.1"
79
79
  },
80
80
  "dependencies": {
81
- "@hot-updater/js": "0.8.0",
82
- "@hot-updater/core": "0.8.0"
81
+ "@hot-updater/js": "0.10.0",
82
+ "@hot-updater/core": "0.10.0"
83
83
  },
84
84
  "scripts": {
85
85
  "build": "rslib build",