@vexor-push/react-native-code-push 0.2.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/LICENSE +20 -0
- package/README.md +61 -0
- package/android/build.gradle +129 -0
- package/android/generated/java/com/vexorpush/codepush/NativeVexorCodePushSpec.java +91 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNVexorCodePushSpec-generated.cpp +110 -0
- package/android/generated/jni/RNVexorCodePushSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.h +17 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.cpp +19 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.h +18 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI-generated.cpp +129 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI.h +184 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.h +23 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.h +19 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/vexorpush/codepush/CrashHandler.kt +78 -0
- package/android/src/main/java/com/vexorpush/codepush/SharedPrefs.kt +60 -0
- package/android/src/main/java/com/vexorpush/codepush/Utils.kt +105 -0
- package/android/src/main/java/com/vexorpush/codepush/VexorCodePush.kt +100 -0
- package/android/src/main/java/com/vexorpush/codepush/VexorCodePushModule.kt +698 -0
- package/android/src/newarch/VexorCodePushSpec.kt +7 -0
- package/android/src/oldarch/VexorCodePushSpec.kt +24 -0
- package/app.plugin.js +1 -0
- package/current-version.js +27 -0
- package/deployment-state.js +33 -0
- package/index.d.ts +101 -0
- package/index.js +535 -0
- package/ios/VexorCodePush.h +13 -0
- package/ios/VexorCodePush.mm +949 -0
- package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
- package/ios/generated/RNVexorCodePushSpec/EventEmitters.h +17 -0
- package/ios/generated/RNVexorCodePushSpec/Props.cpp +19 -0
- package/ios/generated/RNVexorCodePushSpec/Props.h +18 -0
- package/ios/generated/RNVexorCodePushSpec/RCTComponentViewHelpers.h +18 -0
- package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec-generated.mm +130 -0
- package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec.h +109 -0
- package/ios/generated/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/RNVexorCodePushSpec/ShadowNodes.h +23 -0
- package/ios/generated/RNVexorCodePushSpec/States.cpp +16 -0
- package/ios/generated/RNVexorCodePushSpec/States.h +19 -0
- package/ios/generated/RNVexorCodePushSpecJSI-generated.cpp +129 -0
- package/ios/generated/RNVexorCodePushSpecJSI.h +184 -0
- package/package.json +51 -0
- package/plugin/src/index.ts +92 -0
- package/plugin/tsconfig.json +9 -0
- package/react-native-code-push.podspec +48 -0
- package/react-native.config.js +13 -0
- package/src/NativeVexorCodePush.ts +21 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
package com.vexorpush.codepush
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.jakewharton.processphoenix.ProcessPhoenix
|
|
8
|
+
import com.vexorpush.codepush.VexorCodePush.Companion.getVersionCode
|
|
9
|
+
import com.vexorpush.codepush.Common
|
|
10
|
+
import com.vexorpush.codepush.Common.CURRENT_VERSION_CODE
|
|
11
|
+
import com.vexorpush.codepush.Common.PATH
|
|
12
|
+
import com.vexorpush.codepush.Common.PREVIOUS_PATH
|
|
13
|
+
import com.vexorpush.codepush.Common.VERSION
|
|
14
|
+
import com.vexorpush.codepush.Common.PREVIOUS_VERSION
|
|
15
|
+
import com.vexorpush.codepush.Common.METADATA
|
|
16
|
+
import com.vexorpush.codepush.Common.BUNDLE_HISTORY
|
|
17
|
+
import com.vexorpush.codepush.SharedPrefs
|
|
18
|
+
import kotlinx.coroutines.CoroutineScope
|
|
19
|
+
import kotlinx.coroutines.Dispatchers
|
|
20
|
+
import kotlinx.coroutines.SupervisorJob
|
|
21
|
+
import kotlinx.coroutines.cancel
|
|
22
|
+
import kotlinx.coroutines.launch
|
|
23
|
+
import kotlinx.coroutines.withContext
|
|
24
|
+
import android.util.Base64
|
|
25
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
26
|
+
import java.util.concurrent.Executors
|
|
27
|
+
import java.io.File
|
|
28
|
+
import java.io.FileOutputStream
|
|
29
|
+
import java.io.IOException
|
|
30
|
+
import java.net.HttpURLConnection
|
|
31
|
+
import java.net.URL
|
|
32
|
+
import android.util.Log
|
|
33
|
+
import org.json.JSONArray
|
|
34
|
+
import org.json.JSONObject
|
|
35
|
+
|
|
36
|
+
private const val DEBUG_TAG = "VEXOR-CODEPUSH-DEBUG"
|
|
37
|
+
|
|
38
|
+
private fun shortValue(value: String?): String {
|
|
39
|
+
if (value.isNullOrBlank()) return value ?: ""
|
|
40
|
+
return if (value.length > 120) "...${value.takeLast(120)}" else value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
data class BundleVersion(
|
|
44
|
+
val version: Int,
|
|
45
|
+
val path: String,
|
|
46
|
+
val timestamp: Long,
|
|
47
|
+
val metadata: String? = null
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class VexorCodePushModule internal constructor(context: ReactApplicationContext) :
|
|
51
|
+
VexorCodePushSpec(context) {
|
|
52
|
+
private val utils: Utils = Utils(context)
|
|
53
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
54
|
+
private val fileWriterExecutor = Executors.newSingleThreadExecutor()
|
|
55
|
+
|
|
56
|
+
override fun getName(): String {
|
|
57
|
+
return NAME
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun invalidate() {
|
|
61
|
+
super.invalidate()
|
|
62
|
+
scope.cancel()
|
|
63
|
+
fileWriterExecutor.shutdown()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun loadBundleHistory(): List<BundleVersion> {
|
|
67
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
68
|
+
val historyJson = sharedPrefs.getString(BUNDLE_HISTORY)
|
|
69
|
+
|
|
70
|
+
// If history exists, load it
|
|
71
|
+
if (!historyJson.isNullOrEmpty()) {
|
|
72
|
+
return try {
|
|
73
|
+
val jsonArray = JSONArray(historyJson)
|
|
74
|
+
(0 until jsonArray.length()).map { i ->
|
|
75
|
+
val obj = jsonArray.getJSONObject(i)
|
|
76
|
+
BundleVersion(
|
|
77
|
+
version = obj.getInt("version"),
|
|
78
|
+
path = obj.getString("path"),
|
|
79
|
+
timestamp = obj.getLong("timestamp"),
|
|
80
|
+
metadata = if (obj.has("metadata") && !obj.isNull("metadata")) obj.getString("metadata") else null
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
} catch (e: Exception) {
|
|
84
|
+
emptyList()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Bootstrap bundle history from the current and previous bundle pointers
|
|
89
|
+
val currentPath = sharedPrefs.getString(PATH)
|
|
90
|
+
val currentVersion = sharedPrefs.getString(VERSION)
|
|
91
|
+
val previousPath = sharedPrefs.getString(PREVIOUS_PATH)
|
|
92
|
+
val previousVersion = sharedPrefs.getString(PREVIOUS_VERSION)
|
|
93
|
+
|
|
94
|
+
if (currentPath.isNullOrEmpty()) {
|
|
95
|
+
return emptyList()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Migrate current bundle
|
|
99
|
+
val migratedHistory = mutableListOf<BundleVersion>()
|
|
100
|
+
|
|
101
|
+
// Add current bundle if has version
|
|
102
|
+
if (!currentVersion.isNullOrEmpty()) {
|
|
103
|
+
try {
|
|
104
|
+
val version = currentVersion.toInt()
|
|
105
|
+
val bundleFile = File(currentPath)
|
|
106
|
+
if (bundleFile.exists()) {
|
|
107
|
+
migratedHistory.add(
|
|
108
|
+
BundleVersion(
|
|
109
|
+
version = version,
|
|
110
|
+
path = currentPath,
|
|
111
|
+
timestamp = bundleFile.lastModified(), // Use file modification time
|
|
112
|
+
metadata = null
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
// Version is not a number, skip
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add previous bundle if exists
|
|
122
|
+
if (!previousPath.isNullOrEmpty() && !previousVersion.isNullOrEmpty()) {
|
|
123
|
+
try {
|
|
124
|
+
val version = previousVersion.toInt()
|
|
125
|
+
val bundleFile = File(previousPath)
|
|
126
|
+
if (bundleFile.exists()) {
|
|
127
|
+
migratedHistory.add(
|
|
128
|
+
BundleVersion(
|
|
129
|
+
version = version,
|
|
130
|
+
path = previousPath,
|
|
131
|
+
timestamp = bundleFile.lastModified(),
|
|
132
|
+
metadata = null
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
} catch (e: Exception) {
|
|
137
|
+
// Version is not a number, skip
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Save migrated history if any
|
|
142
|
+
if (migratedHistory.isNotEmpty()) {
|
|
143
|
+
saveBundleHistory(migratedHistory.sortedByDescending { it.version })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return migratedHistory.sortedByDescending { it.version }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun saveBundleHistory(history: List<BundleVersion>) {
|
|
150
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
151
|
+
val jsonArray = JSONArray()
|
|
152
|
+
history.forEach { bundle ->
|
|
153
|
+
val obj = JSONObject()
|
|
154
|
+
obj.put("version", bundle.version)
|
|
155
|
+
obj.put("path", bundle.path)
|
|
156
|
+
obj.put("timestamp", bundle.timestamp)
|
|
157
|
+
if (bundle.metadata != null) {
|
|
158
|
+
obj.put("metadata", bundle.metadata)
|
|
159
|
+
} else {
|
|
160
|
+
obj.put("metadata", JSONObject.NULL)
|
|
161
|
+
}
|
|
162
|
+
jsonArray.put(obj)
|
|
163
|
+
}
|
|
164
|
+
sharedPrefs.putString(BUNDLE_HISTORY, jsonArray.toString())
|
|
165
|
+
Log.d(DEBUG_TAG, "saveBundleHistory versions=${history.map { it.version }}")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun extractFolderName(path: String): String {
|
|
169
|
+
val file = File(path)
|
|
170
|
+
return file.parentFile?.name ?: ""
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun saveBundleVersion(
|
|
174
|
+
newPath: String,
|
|
175
|
+
version: Int,
|
|
176
|
+
maxVersions: Int,
|
|
177
|
+
metadata: String?
|
|
178
|
+
) {
|
|
179
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
180
|
+
val history = loadBundleHistory()
|
|
181
|
+
|
|
182
|
+
// Add new version
|
|
183
|
+
val newBundle = BundleVersion(
|
|
184
|
+
version = version,
|
|
185
|
+
path = newPath,
|
|
186
|
+
timestamp = System.currentTimeMillis(),
|
|
187
|
+
metadata = metadata
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Combine and sort by version descending
|
|
191
|
+
val updatedHistory = (listOf(newBundle) + history)
|
|
192
|
+
.sortedByDescending { it.version }
|
|
193
|
+
.distinctBy { it.version } // Remove duplicates by version
|
|
194
|
+
|
|
195
|
+
// Keep only maxVersions most recent
|
|
196
|
+
val finalHistory = updatedHistory.take(maxVersions)
|
|
197
|
+
|
|
198
|
+
// Delete old versions beyond limit
|
|
199
|
+
val versionsToKeep = finalHistory.map { it.version }.toSet()
|
|
200
|
+
updatedHistory.forEach { bundle ->
|
|
201
|
+
if (bundle.version !in versionsToKeep) {
|
|
202
|
+
val bundleFile = File(bundle.path)
|
|
203
|
+
val parentDir = bundleFile.parentFile
|
|
204
|
+
if (parentDir != null && parentDir.exists() && parentDir.isDirectory) {
|
|
205
|
+
utils.deleteDirectory(parentDir)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Save updated history
|
|
211
|
+
saveBundleHistory(finalHistory)
|
|
212
|
+
|
|
213
|
+
// Set current path
|
|
214
|
+
sharedPrefs.putString(PATH, newPath)
|
|
215
|
+
sharedPrefs.putString(VERSION, version.toString())
|
|
216
|
+
Log.d(
|
|
217
|
+
DEBUG_TAG,
|
|
218
|
+
"saveBundleVersion version=$version path=${shortValue(newPath)} maxVersions=$maxVersions kept=${finalHistory.map { it.version }}"
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private fun processBundleFile(
|
|
223
|
+
path: String?,
|
|
224
|
+
extension: String?,
|
|
225
|
+
version: Int?,
|
|
226
|
+
maxVersions: Int?,
|
|
227
|
+
metadata: String?
|
|
228
|
+
): Boolean {
|
|
229
|
+
if (path != null) {
|
|
230
|
+
val file = File(path)
|
|
231
|
+
Log.d(
|
|
232
|
+
DEBUG_TAG,
|
|
233
|
+
"processBundleFile input path=${shortValue(path)} extension=$extension version=$version exists=${file.exists()} isFile=${file.isFile}"
|
|
234
|
+
)
|
|
235
|
+
if (file.exists() && file.isFile) {
|
|
236
|
+
val fileUnzip = utils.extractZipFile(file, extension ?: ".bundle", version)
|
|
237
|
+
Log.d(DEBUG_TAG, "processBundleFile extracted path=${shortValue(fileUnzip)} version=$version")
|
|
238
|
+
if (fileUnzip != null) {
|
|
239
|
+
file.delete()
|
|
240
|
+
utils.deleteOldBundleIfneeded(null)
|
|
241
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
242
|
+
val oldPath = sharedPrefs.getString(PATH)
|
|
243
|
+
|
|
244
|
+
// If version is provided, save to history system
|
|
245
|
+
if (version != null) {
|
|
246
|
+
val maxVersionsToKeep = maxVersions ?: Common.DEFAULT_MAX_BUNDLE_VERSIONS
|
|
247
|
+
saveBundleVersion(fileUnzip, version, maxVersionsToKeep, metadata)
|
|
248
|
+
} else {
|
|
249
|
+
// No version (e.g., Git update) - just set path, no history
|
|
250
|
+
sharedPrefs.putString(PATH, fileUnzip)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
sharedPrefs.putString(
|
|
254
|
+
CURRENT_VERSION_CODE,
|
|
255
|
+
reactApplicationContext.getVersionCode()
|
|
256
|
+
)
|
|
257
|
+
Log.d(
|
|
258
|
+
DEBUG_TAG,
|
|
259
|
+
"processBundleFile saved path=${shortValue(fileUnzip)} version=${sharedPrefs.getString(VERSION)} appVersion=${reactApplicationContext.getVersionCode()}"
|
|
260
|
+
)
|
|
261
|
+
return true
|
|
262
|
+
} else {
|
|
263
|
+
file.delete()
|
|
264
|
+
throw Exception("File unzip failed or path is invalid: $file")
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
throw Exception("File not exist: $file")
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
throw Exception("Invalid path: $path")
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@ReactMethod
|
|
275
|
+
override fun downloadAndInstallBundle(
|
|
276
|
+
url: String?,
|
|
277
|
+
headersJson: String?,
|
|
278
|
+
extension: String?,
|
|
279
|
+
version: Double?,
|
|
280
|
+
maxVersions: Double?,
|
|
281
|
+
metadata: String?,
|
|
282
|
+
promise: Promise
|
|
283
|
+
) {
|
|
284
|
+
if (url.isNullOrBlank()) {
|
|
285
|
+
promise.reject("INVALID_URL", "Download URL is required", null)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
scope.launch {
|
|
290
|
+
try {
|
|
291
|
+
val bundleFile = downloadBundleToCache(url, headersJson)
|
|
292
|
+
val result = processBundleFile(
|
|
293
|
+
bundleFile.absolutePath,
|
|
294
|
+
extension,
|
|
295
|
+
version?.toInt(),
|
|
296
|
+
maxVersions?.toInt(),
|
|
297
|
+
metadata,
|
|
298
|
+
)
|
|
299
|
+
withContext(Dispatchers.Main) {
|
|
300
|
+
promise.resolve(result)
|
|
301
|
+
}
|
|
302
|
+
} catch (e: Exception) {
|
|
303
|
+
withContext(Dispatchers.Main) {
|
|
304
|
+
Log.e(DEBUG_TAG, "downloadAndInstallBundle error=${e.message}", e)
|
|
305
|
+
promise.reject("DOWNLOAD_INSTALL_ERROR", e)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun downloadBundleToCache(url: String, headersJson: String?): File {
|
|
312
|
+
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
|
313
|
+
connectTimeout = 30000
|
|
314
|
+
readTimeout = 120000
|
|
315
|
+
requestMethod = "GET"
|
|
316
|
+
instanceFollowRedirects = true
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!headersJson.isNullOrBlank()) {
|
|
320
|
+
val headers = JSONObject(headersJson)
|
|
321
|
+
headers.keys().forEach { key ->
|
|
322
|
+
val value = headers.optString(key, "")
|
|
323
|
+
if (key.isNotBlank() && value.isNotBlank()) {
|
|
324
|
+
connection.setRequestProperty(key, value)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
val statusCode = connection.responseCode
|
|
330
|
+
if (statusCode !in 200..299) {
|
|
331
|
+
connection.disconnect()
|
|
332
|
+
throw IOException("Download failed with HTTP status $statusCode")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
val outputDir = File(reactApplicationContext.cacheDir, "vexor-code-push")
|
|
336
|
+
if (!outputDir.exists()) outputDir.mkdirs()
|
|
337
|
+
val outputFile = File(outputDir, "${System.currentTimeMillis()}-update.zip")
|
|
338
|
+
|
|
339
|
+
connection.inputStream.use { input ->
|
|
340
|
+
FileOutputStream(outputFile).use { output ->
|
|
341
|
+
input.copyTo(output)
|
|
342
|
+
output.flush()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
connection.disconnect()
|
|
346
|
+
return outputFile
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@ReactMethod
|
|
350
|
+
override fun setupBundlePath(
|
|
351
|
+
path: String?,
|
|
352
|
+
extension: String?,
|
|
353
|
+
version: Double?,
|
|
354
|
+
maxVersions: Double?,
|
|
355
|
+
metadata: String?,
|
|
356
|
+
promise: Promise
|
|
357
|
+
) {
|
|
358
|
+
Log.d(
|
|
359
|
+
DEBUG_TAG,
|
|
360
|
+
"setupBundlePath start path=${shortValue(path)} extension=$extension version=$version maxVersions=$maxVersions"
|
|
361
|
+
)
|
|
362
|
+
scope.launch {
|
|
363
|
+
try {
|
|
364
|
+
val versionInt = version?.toInt()
|
|
365
|
+
val maxVersionsInt = maxVersions?.toInt()
|
|
366
|
+
val result = processBundleFile(path, extension, versionInt, maxVersionsInt, metadata)
|
|
367
|
+
withContext(Dispatchers.Main) {
|
|
368
|
+
Log.d(DEBUG_TAG, "setupBundlePath result=$result version=$versionInt")
|
|
369
|
+
promise.resolve(result)
|
|
370
|
+
}
|
|
371
|
+
} catch (e: Exception) {
|
|
372
|
+
withContext(Dispatchers.Main) {
|
|
373
|
+
Log.e(DEBUG_TAG, "setupBundlePath error=${e.message}", e)
|
|
374
|
+
promise.reject("SET_ERROR", e)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@ReactMethod
|
|
381
|
+
override fun deleteBundle(i: Double, promise: Promise) {
|
|
382
|
+
scope.launch {
|
|
383
|
+
try {
|
|
384
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
385
|
+
val currentPath = sharedPrefs.getString(PATH)
|
|
386
|
+
|
|
387
|
+
// Delete current bundle from file system
|
|
388
|
+
val isDeleted = utils.deleteOldBundleIfneeded(PATH)
|
|
389
|
+
|
|
390
|
+
// Remove current bundle from history if exists
|
|
391
|
+
if (currentPath != null && currentPath.isNotEmpty()) {
|
|
392
|
+
val history = loadBundleHistory()
|
|
393
|
+
val updatedHistory = history.filter { it.path != currentPath }
|
|
394
|
+
saveBundleHistory(updatedHistory)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Clear paths and version (no longer clear PREVIOUS_PATH, use history instead)
|
|
398
|
+
sharedPrefs.putString(PATH, "")
|
|
399
|
+
sharedPrefs.putString(VERSION, "0")
|
|
400
|
+
|
|
401
|
+
withContext(Dispatchers.Main) {
|
|
402
|
+
promise.resolve(isDeleted)
|
|
403
|
+
}
|
|
404
|
+
} catch (e: Exception) {
|
|
405
|
+
withContext(Dispatchers.Main) {
|
|
406
|
+
promise.reject("DELETE_BUNDLE_ERROR", e)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@ReactMethod
|
|
413
|
+
override fun restart() {
|
|
414
|
+
val activity = reactApplicationContext.currentActivity
|
|
415
|
+
val context: Context = activity ?: reactApplicationContext
|
|
416
|
+
UiThreadUtil.runOnUiThread {
|
|
417
|
+
Log.d(DEBUG_TAG, "restart triggerRebirth")
|
|
418
|
+
ProcessPhoenix.triggerRebirth(context)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@ReactMethod
|
|
423
|
+
override fun getCurrentVersion(a: Double, promise: Promise) {
|
|
424
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
425
|
+
val version = sharedPrefs.getString(VERSION)
|
|
426
|
+
Log.d(DEBUG_TAG, "getCurrentVersion version=$version")
|
|
427
|
+
if (version != "") {
|
|
428
|
+
promise.resolve(version)
|
|
429
|
+
} else {
|
|
430
|
+
promise.resolve("0")
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@ReactMethod
|
|
435
|
+
override fun setCurrentVersion(version: String?, promise: Promise) {
|
|
436
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
437
|
+
// No longer save PREVIOUS_VERSION, use history instead
|
|
438
|
+
sharedPrefs.putString(VERSION, version)
|
|
439
|
+
Log.d(DEBUG_TAG, "setCurrentVersion version=$version")
|
|
440
|
+
promise.resolve(true)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@ReactMethod
|
|
444
|
+
override fun getUpdateMetadata(a: Double, promise: Promise) {
|
|
445
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
446
|
+
val metadata = sharedPrefs.getString(METADATA)
|
|
447
|
+
if (metadata != "") {
|
|
448
|
+
promise.resolve(metadata);
|
|
449
|
+
} else {
|
|
450
|
+
promise.resolve(null);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@ReactMethod
|
|
455
|
+
override fun setUpdateMetadata(metadata: String?, promise: Promise) {
|
|
456
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
457
|
+
sharedPrefs.putString(METADATA, metadata)
|
|
458
|
+
promise.resolve(true)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@ReactMethod
|
|
463
|
+
override fun setExactBundlePath(path: String?, promise: Promise) {
|
|
464
|
+
val file = File(path)
|
|
465
|
+
if (file.exists() && file.isFile) {
|
|
466
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
467
|
+
sharedPrefs.putString(PATH, path)
|
|
468
|
+
sharedPrefs.putString(
|
|
469
|
+
CURRENT_VERSION_CODE,
|
|
470
|
+
reactApplicationContext.getVersionCode()
|
|
471
|
+
)
|
|
472
|
+
promise.resolve(true)
|
|
473
|
+
} else {
|
|
474
|
+
promise.resolve(false)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@ReactMethod
|
|
479
|
+
override fun rollbackToPreviousBundle(a: Double, promise: Promise) {
|
|
480
|
+
scope.launch {
|
|
481
|
+
try {
|
|
482
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
483
|
+
val currentPath = sharedPrefs.getString(PATH)
|
|
484
|
+
|
|
485
|
+
// Use history to find previous version (closest to current)
|
|
486
|
+
val history = loadBundleHistory()
|
|
487
|
+
if (history.isNotEmpty() && currentPath != null && currentPath.isNotEmpty()) {
|
|
488
|
+
// Find current bundle in history
|
|
489
|
+
val currentBundle = history.find { it.path == currentPath }
|
|
490
|
+
if (currentBundle != null) {
|
|
491
|
+
// Find previous version (version < current, max version = closest to current)
|
|
492
|
+
val previousBundle = history
|
|
493
|
+
.filter { it.version < currentBundle.version }
|
|
494
|
+
.maxByOrNull { it.version }
|
|
495
|
+
|
|
496
|
+
if (previousBundle != null && File(previousBundle.path).exists()) {
|
|
497
|
+
// Rollback to previous bundle from history
|
|
498
|
+
val isDeleted = utils.deleteOldBundleIfneeded(PATH)
|
|
499
|
+
if (isDeleted) {
|
|
500
|
+
sharedPrefs.putString(PATH, previousBundle.path)
|
|
501
|
+
sharedPrefs.putString(VERSION, previousBundle.version.toString())
|
|
502
|
+
|
|
503
|
+
withContext(Dispatchers.Main) {
|
|
504
|
+
promise.resolve(true)
|
|
505
|
+
}
|
|
506
|
+
return@launch
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
withContext(Dispatchers.Main) {
|
|
513
|
+
promise.resolve(false)
|
|
514
|
+
}
|
|
515
|
+
} catch (e: Exception) {
|
|
516
|
+
withContext(Dispatchers.Main) {
|
|
517
|
+
promise.reject("ROLLBACK_ERROR", e)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@ReactMethod
|
|
524
|
+
override fun getBundleList(a: Double, promise: Promise) {
|
|
525
|
+
scope.launch {
|
|
526
|
+
try {
|
|
527
|
+
val history = loadBundleHistory()
|
|
528
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
529
|
+
val activePath = sharedPrefs.getString(PATH)
|
|
530
|
+
|
|
531
|
+
val bundleList = history.map { bundle ->
|
|
532
|
+
val folderName = extractFolderName(bundle.path)
|
|
533
|
+
val bundleObj = JSONObject()
|
|
534
|
+
bundleObj.put("id", folderName)
|
|
535
|
+
bundleObj.put("version", bundle.version)
|
|
536
|
+
bundleObj.put("date", bundle.timestamp)
|
|
537
|
+
bundleObj.put("path", bundle.path)
|
|
538
|
+
bundleObj.put("isActive", bundle.path == activePath)
|
|
539
|
+
if (bundle.metadata != null) {
|
|
540
|
+
try {
|
|
541
|
+
// Try to parse as JSON, if fails use as string
|
|
542
|
+
val metadataJson = JSONObject(bundle.metadata)
|
|
543
|
+
bundleObj.put("metadata", metadataJson)
|
|
544
|
+
} catch (e: Exception) {
|
|
545
|
+
bundleObj.put("metadata", bundle.metadata)
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
bundleObj.put("metadata", JSONObject.NULL)
|
|
549
|
+
}
|
|
550
|
+
bundleObj
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
val jsonArray = JSONArray()
|
|
554
|
+
bundleList.forEach { jsonArray.put(it) }
|
|
555
|
+
|
|
556
|
+
withContext(Dispatchers.Main) {
|
|
557
|
+
promise.resolve(jsonArray.toString())
|
|
558
|
+
}
|
|
559
|
+
} catch (e: Exception) {
|
|
560
|
+
withContext(Dispatchers.Main) {
|
|
561
|
+
promise.reject("GET_BUNDLE_LIST_ERROR", e)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@ReactMethod
|
|
568
|
+
override fun deleteBundleById(id: String, promise: Promise) {
|
|
569
|
+
scope.launch {
|
|
570
|
+
try {
|
|
571
|
+
val history = loadBundleHistory()
|
|
572
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
573
|
+
val activePath = sharedPrefs.getString(PATH)
|
|
574
|
+
|
|
575
|
+
val bundleToDelete = history.find { extractFolderName(it.path) == id }
|
|
576
|
+
if (bundleToDelete == null) {
|
|
577
|
+
withContext(Dispatchers.Main) {
|
|
578
|
+
promise.resolve(false)
|
|
579
|
+
}
|
|
580
|
+
return@launch
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// If deleting active bundle, rollback to oldest remaining bundle or clear
|
|
584
|
+
if (bundleToDelete.path == activePath) {
|
|
585
|
+
val remainingBundles = history.filter { it.path != bundleToDelete.path }
|
|
586
|
+
if (remainingBundles.isNotEmpty()) {
|
|
587
|
+
val oldestBundle = remainingBundles.minByOrNull { it.version }
|
|
588
|
+
if (oldestBundle != null) {
|
|
589
|
+
sharedPrefs.putString(PATH, oldestBundle.path)
|
|
590
|
+
sharedPrefs.putString(VERSION, oldestBundle.version.toString())
|
|
591
|
+
} else {
|
|
592
|
+
sharedPrefs.putString(PATH, "")
|
|
593
|
+
sharedPrefs.putString(VERSION, "")
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
sharedPrefs.putString(PATH, "")
|
|
597
|
+
sharedPrefs.putString(VERSION, "")
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Delete bundle folder
|
|
602
|
+
val isDeleted = utils.deleteOldBundleIfneeded(bundleToDelete.path)
|
|
603
|
+
|
|
604
|
+
// Remove from history
|
|
605
|
+
val updatedHistory = history.filter { it.path != bundleToDelete.path }
|
|
606
|
+
saveBundleHistory(updatedHistory)
|
|
607
|
+
|
|
608
|
+
withContext(Dispatchers.Main) {
|
|
609
|
+
promise.resolve(isDeleted)
|
|
610
|
+
}
|
|
611
|
+
} catch (e: Exception) {
|
|
612
|
+
withContext(Dispatchers.Main) {
|
|
613
|
+
promise.reject("DELETE_BUNDLE_ERROR", e)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@ReactMethod
|
|
620
|
+
override fun clearAllBundles(a: Double, promise: Promise) {
|
|
621
|
+
scope.launch {
|
|
622
|
+
try {
|
|
623
|
+
val history = loadBundleHistory()
|
|
624
|
+
val sharedPrefs = SharedPrefs(reactApplicationContext)
|
|
625
|
+
|
|
626
|
+
// Delete all bundle folders
|
|
627
|
+
history.forEach { bundle ->
|
|
628
|
+
utils.deleteOldBundleIfneeded(bundle.path)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Clear history
|
|
632
|
+
saveBundleHistory(emptyList())
|
|
633
|
+
|
|
634
|
+
// Clear current path and version
|
|
635
|
+
sharedPrefs.putString(PATH, "")
|
|
636
|
+
sharedPrefs.putString(VERSION, "")
|
|
637
|
+
|
|
638
|
+
withContext(Dispatchers.Main) {
|
|
639
|
+
promise.resolve(true)
|
|
640
|
+
}
|
|
641
|
+
} catch (e: Exception) {
|
|
642
|
+
withContext(Dispatchers.Main) {
|
|
643
|
+
promise.reject("CLEAR_ALL_BUNDLES_ERROR", e)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
override fun writeFile(
|
|
651
|
+
path: String?,
|
|
652
|
+
base64Content: String?,
|
|
653
|
+
encoding: String?,
|
|
654
|
+
promise: Promise
|
|
655
|
+
) {
|
|
656
|
+
if (path == null || base64Content == null) {
|
|
657
|
+
promise.reject("INVALID_ARG", "Path and base64Content are required", null)
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
fileWriterExecutor.execute {
|
|
662
|
+
try {
|
|
663
|
+
// Decode base64 to bytes
|
|
664
|
+
val bytes = Base64.decode(base64Content, Base64.DEFAULT)
|
|
665
|
+
|
|
666
|
+
// Ensure parent directory exists
|
|
667
|
+
val file = File(path)
|
|
668
|
+
val parentDir = file.parentFile
|
|
669
|
+
if (parentDir != null && !parentDir.exists()) {
|
|
670
|
+
parentDir.mkdirs()
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Write file on background thread
|
|
674
|
+
FileOutputStream(file).use { fos ->
|
|
675
|
+
fos.write(bytes)
|
|
676
|
+
fos.flush()
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Resolve on UI thread (React Native requirement)
|
|
680
|
+
UiThreadUtil.runOnUiThread {
|
|
681
|
+
promise.resolve(true)
|
|
682
|
+
}
|
|
683
|
+
} catch (e: IOException) {
|
|
684
|
+
UiThreadUtil.runOnUiThread {
|
|
685
|
+
promise.reject("WRITE_ERROR", "Failed to write file: ${e.message}", e)
|
|
686
|
+
}
|
|
687
|
+
} catch (e: Exception) {
|
|
688
|
+
UiThreadUtil.runOnUiThread {
|
|
689
|
+
promise.reject("WRITE_ERROR", "Unexpected error: ${e.message}", e)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
companion object {
|
|
696
|
+
const val NAME = "VexorCodePush"
|
|
697
|
+
}
|
|
698
|
+
}
|