@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.
- package/android/build.gradle +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +75 -61
- package/android/src/newarch/HotUpdaterModule.kt +23 -12
- package/android/src/oldarch/HotUpdaterModule.kt +23 -12
- package/ios/HotUpdater/HotUpdater.h +1 -0
- package/ios/HotUpdater/HotUpdater.mm +51 -10
- package/package.json +3 -3
package/android/build.gradle
CHANGED
|
@@ -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 =
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
reactIntegrationManager.setJSBundle(reactApplication,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
val
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
202
|
+
val extractedPath = File(path).parentFile?.path ?: return@withContext false
|
|
191
203
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
if (!extractZipFileAtPath(path, extractedPath)) {
|
|
205
|
+
Log.d("HotUpdater", "Failed to extract zip file.")
|
|
206
|
+
return@withContext false
|
|
207
|
+
}
|
|
196
208
|
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
val extractedDirectory = File(extractedPath)
|
|
210
|
+
val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
|
|
199
211
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
forKeyPath:@"countOfBytesReceived"
|
|
170
|
+
options:NSKeyValueObservingOptionNew
|
|
171
|
+
context:nil];
|
|
162
172
|
[downloadTask addObserver:self
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
//
|
|
183
|
-
[
|
|
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.
|
|
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.
|
|
82
|
-
"@hot-updater/core": "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",
|