@hot-updater/react-native 0.9.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 {
@@ -252,6 +252,7 @@ RCT_EXPORT_MODULE();
252
252
  RCT_EXPORT_METHOD(reload) {
253
253
  NSLog(@"HotUpdater requested a reload");
254
254
  dispatch_async(dispatch_get_main_queue(), ^{
255
+ [super.bridge setValue:[HotUpdater bundleURL] forKey:@"bundleURL"];
255
256
  RCTTriggerReloadCommandListeners(@"HotUpdater requested a reload");
256
257
  });
257
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.9.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.9.0",
82
- "@hot-updater/core": "0.9.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",