@hot-updater/react-native 0.17.0 → 0.18.1
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/HotUpdater.podspec +7 -11
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +200 -0
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +104 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +62 -305
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +49 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +176 -0
- package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +98 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +69 -0
- package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +52 -0
- package/android/src/newarch/HotUpdaterModule.kt +31 -34
- package/android/src/oldarch/HotUpdaterModule.kt +32 -34
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -9
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +593 -0
- package/ios/HotUpdater/Internal/FileManagerService.swift +97 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +241 -0
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +24 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +143 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +6 -0
- package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +25 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +101 -0
- package/ios/HotUpdater/Internal/VersionedPreferencesService.swift +82 -0
- package/ios/HotUpdater/Public/HotUpdater.h +29 -0
- package/lib/commonjs/checkForUpdate.js +70 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -0
- package/lib/commonjs/error.js +14 -0
- package/lib/commonjs/error.js.map +1 -0
- package/lib/commonjs/fetchUpdateInfo.js +74 -0
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -0
- package/lib/commonjs/hooks/useEventCallback.js +17 -0
- package/lib/commonjs/hooks/useEventCallback.js.map +1 -0
- package/lib/commonjs/index.js +234 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native.js +132 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/runUpdateProcess.js +69 -0
- package/lib/commonjs/runUpdateProcess.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js +9 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -0
- package/lib/commonjs/store.js +48 -0
- package/lib/commonjs/store.js.map +1 -0
- package/lib/commonjs/wrap.js +98 -0
- package/lib/commonjs/wrap.js.map +1 -0
- package/lib/module/checkForUpdate.js +64 -0
- package/lib/module/checkForUpdate.js.map +1 -0
- package/lib/module/error.js +9 -0
- package/lib/module/error.js.map +1 -0
- package/lib/module/fetchUpdateInfo.js +69 -0
- package/lib/module/fetchUpdateInfo.js.map +1 -0
- package/lib/module/hooks/useEventCallback.js +13 -0
- package/lib/module/hooks/useEventCallback.js.map +1 -0
- package/lib/module/index.js +211 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native.js +119 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/runUpdateProcess.js +64 -0
- package/lib/module/runUpdateProcess.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js +5 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -0
- package/lib/module/store.js +42 -0
- package/lib/module/store.js.map +1 -0
- package/lib/module/wrap.js +94 -0
- package/lib/module/wrap.js.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +22 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/error.d.ts +1 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -0
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +4 -0
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/hooks/useEventCallback.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/useEventCallback.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/index.d.ts +38 -12
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native.d.ts +64 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/{dist → lib/typescript/commonjs}/runUpdateProcess.d.ts +1 -0
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/specs/NativeHotUpdater.d.ts +8 -9
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/store.d.ts +1 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -0
- package/{dist → lib/typescript/commonjs}/wrap.d.ts +3 -2
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +22 -0
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -0
- package/lib/typescript/module/error.d.ts +4 -0
- package/lib/typescript/module/error.d.ts.map +1 -0
- package/lib/typescript/module/fetchUpdateInfo.d.ts +4 -0
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useEventCallback.d.ts +5 -0
- package/lib/typescript/module/hooks/useEventCallback.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +202 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native.d.ts +64 -0
- package/lib/typescript/module/native.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/runUpdateProcess.d.ts +49 -0
- package/lib/typescript/module/runUpdateProcess.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -0
- package/lib/typescript/module/store.d.ts +11 -0
- package/lib/typescript/module/store.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts +51 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -0
- package/package.json +59 -30
- package/src/checkForUpdate.ts +59 -9
- package/src/fetchUpdateInfo.ts +40 -12
- package/src/index.ts +37 -11
- package/src/native.ts +87 -41
- package/src/runUpdateProcess.ts +2 -2
- package/src/specs/NativeHotUpdater.ts +8 -10
- package/src/wrap.tsx +9 -13
- package/android/generated/java/com/hotupdater/NativeHotUpdaterSpec.java +0 -93
- package/android/generated/jni/CMakeLists.txt +0 -36
- package/android/generated/jni/HotUpdaterSpec-generated.cpp +0 -68
- package/android/generated/jni/HotUpdaterSpec.h +0 -31
- package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI-generated.cpp +0 -70
- package/android/generated/jni/react/renderer/components/HotUpdaterSpec/HotUpdaterSpecJSI.h +0 -121
- package/android/src/main/java/com/hotupdater/HotUpdaterPrefs.kt +0 -42
- package/dist/checkForUpdate.d.ts +0 -12
- package/dist/fetchUpdateInfo.d.ts +0 -3
- package/dist/index.js +0 -341
- package/dist/index.mjs +0 -301
- package/dist/native.d.ts +0 -41
- package/ios/HotUpdater/HotUpdater.h +0 -15
- package/ios/HotUpdater/HotUpdater.mm +0 -468
- package/ios/HotUpdater/HotUpdater.modulemap +0 -6
- package/ios/HotUpdater/HotUpdaterPrefs.h +0 -9
- package/ios/HotUpdater/HotUpdaterPrefs.mm +0 -45
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec-generated.mm +0 -81
- package/ios/generated/HotUpdaterSpec/HotUpdaterSpec.h +0 -112
- package/ios/generated/HotUpdaterSpecJSI-generated.cpp +0 -70
- package/ios/generated/HotUpdaterSpecJSI.h +0 -121
- package/react-native.config.js +0 -12
- package/src/global.d.ts +0 -3
|
@@ -2,23 +2,16 @@ package com.hotupdater
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.os.Handler
|
|
6
|
-
import android.os.Looper
|
|
7
|
-
import android.util.Log
|
|
8
5
|
import android.view.View
|
|
9
|
-
import com.facebook.react.ReactApplication
|
|
10
6
|
import com.facebook.react.ReactPackage
|
|
11
7
|
import com.facebook.react.bridge.NativeModule
|
|
12
8
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
9
|
import com.facebook.react.uimanager.ReactShadowNode
|
|
14
10
|
import com.facebook.react.uimanager.ViewManager
|
|
15
|
-
import kotlinx.coroutines.Dispatchers
|
|
16
|
-
import kotlinx.coroutines.withContext
|
|
17
|
-
import java.io.File
|
|
18
|
-
import java.net.HttpURLConnection
|
|
19
|
-
import java.net.URL
|
|
20
|
-
import java.util.zip.ZipFile
|
|
21
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Main React Native package for HotUpdater
|
|
14
|
+
*/
|
|
22
15
|
class HotUpdater : ReactPackage {
|
|
23
16
|
override fun createViewManagers(context: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
|
|
24
17
|
|
|
@@ -26,309 +19,73 @@ class HotUpdater : ReactPackage {
|
|
|
26
19
|
listOf(HotUpdaterModule(context)).toMutableList()
|
|
27
20
|
|
|
28
21
|
companion object {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Gets the app version
|
|
24
|
+
* @param context Application context
|
|
25
|
+
* @return App version name or null if not available
|
|
26
|
+
*/
|
|
27
|
+
fun getAppVersion(context: Context): String? = HotUpdaterFactory.getInstance(context).getAppVersion()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generates a bundle ID based on build timestamp
|
|
31
|
+
* @param context Application context
|
|
32
|
+
* @return The minimum bundle ID string
|
|
33
|
+
*/
|
|
34
|
+
fun getMinBundleId(context: Context): String = HotUpdaterFactory.getInstance(context).getMinBundleId()
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets the current update channel
|
|
38
|
+
* @param context Application context
|
|
39
|
+
* @return The channel name or null if not set
|
|
40
|
+
*/
|
|
41
|
+
fun getChannel(context: Context): String? = HotUpdaterFactory.getInstance(context).getChannel()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the path to the bundle file
|
|
45
|
+
* @param context Application context
|
|
46
|
+
* @return The path to the bundle file
|
|
47
|
+
*/
|
|
48
|
+
fun getJSBundleFile(context: Context): String = HotUpdaterFactory.getInstance(context).getJSBundleFile()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Updates the bundle from the specified URL
|
|
52
|
+
* @param context Application context
|
|
53
|
+
* @param bundleId ID of the bundle to update
|
|
54
|
+
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
55
|
+
* @param progressCallback Callback for download progress updates
|
|
56
|
+
* @return true if the update was successful
|
|
57
|
+
*/
|
|
58
|
+
suspend fun updateBundle(
|
|
53
59
|
context: Context,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
updaterPrefs.setItem("HotUpdaterBundleURL", bundleURL)
|
|
58
|
-
|
|
59
|
-
if (bundleURL == null) {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
val reactIntegrationManager = ReactIntegrationManager(context)
|
|
64
|
-
val activity: Activity? = getCurrentActivity(context)
|
|
65
|
-
val reactApplication: ReactApplication =
|
|
66
|
-
reactIntegrationManager.getReactApplication(activity?.application)
|
|
67
|
-
val newBundleURL = getJSBundleFile(context)
|
|
68
|
-
reactIntegrationManager.setJSBundle(reactApplication, newBundleURL)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private fun extractZipFileAtPath(
|
|
72
|
-
filePath: String,
|
|
73
|
-
destinationPath: String,
|
|
60
|
+
bundleId: String,
|
|
61
|
+
fileUrl: String?,
|
|
62
|
+
progressCallback: (Double) -> Unit,
|
|
74
63
|
): Boolean =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
true
|
|
90
|
-
} catch (e: Exception) {
|
|
91
|
-
Log.d("HotUpdater", "Failed to unzip file: ${e.message}")
|
|
92
|
-
false
|
|
93
|
-
}
|
|
64
|
+
HotUpdaterFactory.getInstance(context).updateBundle(
|
|
65
|
+
bundleId,
|
|
66
|
+
fileUrl,
|
|
67
|
+
progressCallback,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reloads the React Native application
|
|
72
|
+
* @param context Application context
|
|
73
|
+
*/
|
|
74
|
+
fun reload(context: Context) {
|
|
75
|
+
val currentActivity = getCurrentActivity(context)
|
|
76
|
+
HotUpdaterFactory.getInstance(context).reload(currentActivity)
|
|
77
|
+
}
|
|
94
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Gets the current activity from ReactApplicationContext
|
|
81
|
+
* @param context Context that might be a ReactApplicationContext
|
|
82
|
+
* @return The current activity or null
|
|
83
|
+
*/
|
|
95
84
|
private fun getCurrentActivity(context: Context): Activity? =
|
|
96
85
|
if (context is ReactApplicationContext) {
|
|
97
86
|
context.currentActivity
|
|
98
87
|
} else {
|
|
99
88
|
null
|
|
100
89
|
}
|
|
101
|
-
|
|
102
|
-
fun reload(context: Context) {
|
|
103
|
-
val reactIntegrationManager = ReactIntegrationManager(context)
|
|
104
|
-
val activity: Activity? = getCurrentActivity(context)
|
|
105
|
-
val reactApplication: ReactApplication =
|
|
106
|
-
reactIntegrationManager.getReactApplication(activity?.application)
|
|
107
|
-
val bundleURL = getJSBundleFile(context)
|
|
108
|
-
reactIntegrationManager.setJSBundle(reactApplication, bundleURL)
|
|
109
|
-
Handler(Looper.getMainLooper()).post {
|
|
110
|
-
reactIntegrationManager.reload(reactApplication)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
fun getJSBundleFile(context: Context): String {
|
|
115
|
-
val updaterPrefs = getPrefs(context)
|
|
116
|
-
val urlString = updaterPrefs.getItem("HotUpdaterBundleURL")
|
|
117
|
-
if (urlString.isNullOrEmpty()) {
|
|
118
|
-
return "assets://index.android.bundle"
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
val file = File(urlString)
|
|
122
|
-
if (!file.exists()) {
|
|
123
|
-
updaterPrefs.setItem("HotUpdaterBundleURL", null)
|
|
124
|
-
return "assets://index.android.bundle"
|
|
125
|
-
}
|
|
126
|
-
return urlString
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
fun setChannel(
|
|
130
|
-
context: Context,
|
|
131
|
-
channel: String,
|
|
132
|
-
) {
|
|
133
|
-
val updaterPrefs = getPrefs(context)
|
|
134
|
-
updaterPrefs.setItem("HotUpdaterChannel", channel)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fun getChannel(context: Context): String? {
|
|
138
|
-
val updaterPrefs = getPrefs(context)
|
|
139
|
-
return updaterPrefs.getItem("HotUpdaterChannel")
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
suspend fun updateBundle(
|
|
143
|
-
context: Context,
|
|
144
|
-
bundleId: String,
|
|
145
|
-
zipUrl: String?,
|
|
146
|
-
progressCallback: ((Double) -> Unit),
|
|
147
|
-
): Boolean {
|
|
148
|
-
Log.d("HotUpdater", "updateBundle bundleId $bundleId zipUrl $zipUrl")
|
|
149
|
-
if (zipUrl.isNullOrEmpty()) {
|
|
150
|
-
setBundleURL(context, null)
|
|
151
|
-
return true
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
val baseDir = context.getExternalFilesDir(null)
|
|
155
|
-
val bundleStoreDir = File(baseDir, "bundle-store")
|
|
156
|
-
if (!bundleStoreDir.exists()) {
|
|
157
|
-
bundleStoreDir.mkdirs()
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
val finalBundleDir = File(bundleStoreDir, bundleId)
|
|
161
|
-
if (finalBundleDir.exists()) {
|
|
162
|
-
Log.d("HotUpdater", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
|
|
163
|
-
val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
164
|
-
if (existingIndexFile != null) {
|
|
165
|
-
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
166
|
-
setBundleURL(context, existingIndexFile.absolutePath)
|
|
167
|
-
cleanupOldBundles(bundleStoreDir)
|
|
168
|
-
return true
|
|
169
|
-
} else {
|
|
170
|
-
finalBundleDir.deleteRecursively()
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
val tempDir = File(baseDir, "bundle-temp")
|
|
175
|
-
if (tempDir.exists()) {
|
|
176
|
-
tempDir.deleteRecursively()
|
|
177
|
-
}
|
|
178
|
-
tempDir.mkdirs()
|
|
179
|
-
|
|
180
|
-
val tempZipFile = File(tempDir, "bundle.zip")
|
|
181
|
-
val extractedDir = File(tempDir, "extracted")
|
|
182
|
-
extractedDir.mkdirs()
|
|
183
|
-
|
|
184
|
-
val isSuccess =
|
|
185
|
-
withContext(Dispatchers.IO) {
|
|
186
|
-
val downloadUrl = URL(zipUrl)
|
|
187
|
-
val conn =
|
|
188
|
-
try {
|
|
189
|
-
downloadUrl.openConnection() as HttpURLConnection
|
|
190
|
-
} catch (e: Exception) {
|
|
191
|
-
Log.d("HotUpdater", "Failed to open connection: ${e.message}")
|
|
192
|
-
tempDir.deleteRecursively()
|
|
193
|
-
return@withContext false
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
conn.connect()
|
|
198
|
-
val totalSize = conn.contentLength
|
|
199
|
-
if (totalSize <= 0) {
|
|
200
|
-
Log.d("HotUpdater", "Invalid content length: $totalSize")
|
|
201
|
-
tempDir.deleteRecursively()
|
|
202
|
-
return@withContext false
|
|
203
|
-
}
|
|
204
|
-
conn.inputStream.use { input ->
|
|
205
|
-
tempZipFile.outputStream().use { output ->
|
|
206
|
-
val buffer = ByteArray(8 * 1024)
|
|
207
|
-
var bytesRead: Int
|
|
208
|
-
var totalRead = 0L
|
|
209
|
-
var lastProgressTime = System.currentTimeMillis()
|
|
210
|
-
|
|
211
|
-
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
212
|
-
output.write(buffer, 0, bytesRead)
|
|
213
|
-
totalRead += bytesRead
|
|
214
|
-
val currentTime = System.currentTimeMillis()
|
|
215
|
-
if (currentTime - lastProgressTime >= 100) {
|
|
216
|
-
val progress = totalRead.toDouble() / totalSize
|
|
217
|
-
progressCallback.invoke(progress)
|
|
218
|
-
lastProgressTime = currentTime
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
progressCallback.invoke(1.0)
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
} catch (e: Exception) {
|
|
225
|
-
Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
|
|
226
|
-
tempDir.deleteRecursively()
|
|
227
|
-
return@withContext false
|
|
228
|
-
} finally {
|
|
229
|
-
conn.disconnect()
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!extractZipFileAtPath(tempZipFile.absolutePath, extractedDir.absolutePath)) {
|
|
233
|
-
Log.d("HotUpdater", "Failed to extract zip file.")
|
|
234
|
-
tempDir.deleteRecursively()
|
|
235
|
-
return@withContext false
|
|
236
|
-
}
|
|
237
|
-
true
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (!isSuccess) {
|
|
241
|
-
tempDir.deleteRecursively()
|
|
242
|
-
return false
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
val indexFileExtracted = extractedDir.walk().find { it.name == "index.android.bundle" }
|
|
246
|
-
if (indexFileExtracted == null) {
|
|
247
|
-
Log.d("HotUpdater", "index.android.bundle not found in extracted files.")
|
|
248
|
-
tempDir.deleteRecursively()
|
|
249
|
-
return false
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (finalBundleDir.exists()) {
|
|
253
|
-
finalBundleDir.deleteRecursively()
|
|
254
|
-
}
|
|
255
|
-
if (!extractedDir.renameTo(finalBundleDir)) {
|
|
256
|
-
extractedDir.copyRecursively(finalBundleDir, overwrite = true)
|
|
257
|
-
extractedDir.deleteRecursively()
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
261
|
-
if (finalIndexFile == null) {
|
|
262
|
-
Log.d("HotUpdater", "index.android.bundle not found in final directory.")
|
|
263
|
-
tempDir.deleteRecursively()
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
268
|
-
val bundlePath = finalIndexFile.absolutePath
|
|
269
|
-
Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
|
|
270
|
-
setBundleURL(context, bundlePath)
|
|
271
|
-
cleanupOldBundles(bundleStoreDir)
|
|
272
|
-
tempDir.deleteRecursively()
|
|
273
|
-
|
|
274
|
-
Log.d("HotUpdater", "Downloaded and extracted file successfully.")
|
|
275
|
-
return true
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private fun cleanupOldBundles(bundleStoreDir: File) {
|
|
279
|
-
val bundles = bundleStoreDir.listFiles { file -> file.isDirectory }?.toList() ?: return
|
|
280
|
-
val sortedBundles = bundles.sortedByDescending { it.lastModified() }
|
|
281
|
-
if (sortedBundles.size > 1) {
|
|
282
|
-
sortedBundles.drop(1).forEach { oldBundle ->
|
|
283
|
-
Log.d("HotUpdater", "Removing old bundle: ${oldBundle.name}")
|
|
284
|
-
oldBundle.deleteRecursively()
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
fun getMinBundleId(): String =
|
|
290
|
-
try {
|
|
291
|
-
val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
|
|
292
|
-
val bytes =
|
|
293
|
-
ByteArray(16).apply {
|
|
294
|
-
this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
|
|
295
|
-
this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
|
|
296
|
-
this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
|
|
297
|
-
this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
|
|
298
|
-
this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
|
|
299
|
-
this[5] = (buildTimestampMs and 0xFF).toByte()
|
|
300
|
-
this[6] = 0x70.toByte()
|
|
301
|
-
this[7] = 0x00.toByte()
|
|
302
|
-
this[8] = 0x80.toByte()
|
|
303
|
-
this[9] = 0x00.toByte()
|
|
304
|
-
this[10] = 0x00.toByte()
|
|
305
|
-
this[11] = 0x00.toByte()
|
|
306
|
-
this[12] = 0x00.toByte()
|
|
307
|
-
this[13] = 0x00.toByte()
|
|
308
|
-
this[14] = 0x00.toByte()
|
|
309
|
-
this[15] = 0x00.toByte()
|
|
310
|
-
}
|
|
311
|
-
String.format(
|
|
312
|
-
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
313
|
-
bytes[0].toInt() and 0xFF,
|
|
314
|
-
bytes[1].toInt() and 0xFF,
|
|
315
|
-
bytes[2].toInt() and 0xFF,
|
|
316
|
-
bytes[3].toInt() and 0xFF,
|
|
317
|
-
bytes[4].toInt() and 0xFF,
|
|
318
|
-
bytes[5].toInt() and 0xFF,
|
|
319
|
-
bytes[6].toInt() and 0xFF,
|
|
320
|
-
bytes[7].toInt() and 0xFF,
|
|
321
|
-
bytes[8].toInt() and 0xFF,
|
|
322
|
-
bytes[9].toInt() and 0xFF,
|
|
323
|
-
bytes[10].toInt() and 0xFF,
|
|
324
|
-
bytes[11].toInt() and 0xFF,
|
|
325
|
-
bytes[12].toInt() and 0xFF,
|
|
326
|
-
bytes[13].toInt() and 0xFF,
|
|
327
|
-
bytes[14].toInt() and 0xFF,
|
|
328
|
-
bytes[15].toInt() and 0xFF,
|
|
329
|
-
)
|
|
330
|
-
} catch (e: Exception) {
|
|
331
|
-
"00000000-0000-0000-0000-000000000000"
|
|
332
|
-
}
|
|
333
90
|
}
|
|
334
91
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory for creating HotUpdaterImpl instances with proper dependency injection
|
|
7
|
+
*/
|
|
8
|
+
object HotUpdaterFactory {
|
|
9
|
+
@Volatile
|
|
10
|
+
private var instance: HotUpdaterImpl? = null
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets the singleton instance of HotUpdaterImpl
|
|
14
|
+
* @param context Application context
|
|
15
|
+
* @return HotUpdaterImpl instance
|
|
16
|
+
*/
|
|
17
|
+
fun getInstance(context: Context): HotUpdaterImpl =
|
|
18
|
+
instance ?: synchronized(this) {
|
|
19
|
+
instance ?: createHotUpdaterImpl(context).also { instance = it }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new HotUpdaterImpl instance with all dependencies
|
|
24
|
+
* @param context Application context
|
|
25
|
+
* @return New HotUpdaterImpl instance
|
|
26
|
+
*/
|
|
27
|
+
private fun createHotUpdaterImpl(context: Context): HotUpdaterImpl {
|
|
28
|
+
val appContext = context.applicationContext
|
|
29
|
+
val appVersion = HotUpdaterImpl.getAppVersion(appContext) ?: "unknown"
|
|
30
|
+
|
|
31
|
+
// Create services
|
|
32
|
+
val fileSystem = FileManagerService(appContext)
|
|
33
|
+
val preferences = VersionedPreferencesService(appContext, appVersion)
|
|
34
|
+
val downloadService = HttpDownloadService()
|
|
35
|
+
val unzipService = ZipFileUnzipService()
|
|
36
|
+
|
|
37
|
+
// Create bundle storage with dependencies
|
|
38
|
+
val bundleStorage =
|
|
39
|
+
BundleFileStorageService(
|
|
40
|
+
fileSystem,
|
|
41
|
+
downloadService,
|
|
42
|
+
unzipService,
|
|
43
|
+
preferences,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Create and return the implementation
|
|
47
|
+
return HotUpdaterImpl(appContext, bundleStorage, preferences)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.util.Log
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core implementation class for HotUpdater functionality
|
|
11
|
+
*/
|
|
12
|
+
class HotUpdaterImpl(
|
|
13
|
+
private val context: Context,
|
|
14
|
+
private val bundleStorage: BundleStorageService,
|
|
15
|
+
private val preferences: PreferencesService,
|
|
16
|
+
) {
|
|
17
|
+
/**
|
|
18
|
+
* Gets the app version
|
|
19
|
+
* @param context Application context
|
|
20
|
+
* @return App version name or null if not available
|
|
21
|
+
*/
|
|
22
|
+
fun getAppVersion(): String? =
|
|
23
|
+
try {
|
|
24
|
+
val packageInfo =
|
|
25
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
|
26
|
+
context.packageManager.getPackageInfo(
|
|
27
|
+
context.packageName,
|
|
28
|
+
android.content.pm.PackageManager.PackageInfoFlags
|
|
29
|
+
.of(0),
|
|
30
|
+
)
|
|
31
|
+
} else {
|
|
32
|
+
@Suppress("DEPRECATION")
|
|
33
|
+
context.packageManager.getPackageInfo(context.packageName, 0)
|
|
34
|
+
}
|
|
35
|
+
packageInfo.versionName
|
|
36
|
+
} catch (e: Exception) {
|
|
37
|
+
null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
companion object {
|
|
41
|
+
private const val DEFAULT_CHANNEL = "production"
|
|
42
|
+
|
|
43
|
+
fun getAppVersion(context: Context): String? =
|
|
44
|
+
try {
|
|
45
|
+
val packageInfo =
|
|
46
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
|
47
|
+
context.packageManager.getPackageInfo(
|
|
48
|
+
context.packageName,
|
|
49
|
+
android.content.pm.PackageManager.PackageInfoFlags
|
|
50
|
+
.of(0),
|
|
51
|
+
)
|
|
52
|
+
} else {
|
|
53
|
+
@Suppress("DEPRECATION")
|
|
54
|
+
context.packageManager.getPackageInfo(context.packageName, 0)
|
|
55
|
+
}
|
|
56
|
+
packageInfo.versionName
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generates a bundle ID based on build timestamp
|
|
64
|
+
* @return The minimum bundle ID string
|
|
65
|
+
*/
|
|
66
|
+
fun getMinBundleId(): String =
|
|
67
|
+
try {
|
|
68
|
+
val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
|
|
69
|
+
val bytes =
|
|
70
|
+
ByteArray(16).apply {
|
|
71
|
+
this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
|
|
72
|
+
this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
|
|
73
|
+
this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
|
|
74
|
+
this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
|
|
75
|
+
this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
|
|
76
|
+
this[5] = (buildTimestampMs and 0xFF).toByte()
|
|
77
|
+
this[6] = 0x70.toByte()
|
|
78
|
+
this[7] = 0x00.toByte()
|
|
79
|
+
this[8] = 0x80.toByte()
|
|
80
|
+
this[9] = 0x00.toByte()
|
|
81
|
+
this[10] = 0x00.toByte()
|
|
82
|
+
this[11] = 0x00.toByte()
|
|
83
|
+
this[12] = 0x00.toByte()
|
|
84
|
+
this[13] = 0x00.toByte()
|
|
85
|
+
this[14] = 0x00.toByte()
|
|
86
|
+
this[15] = 0x00.toByte()
|
|
87
|
+
}
|
|
88
|
+
String.format(
|
|
89
|
+
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
90
|
+
bytes[0].toInt() and 0xFF,
|
|
91
|
+
bytes[1].toInt() and 0xFF,
|
|
92
|
+
bytes[2].toInt() and 0xFF,
|
|
93
|
+
bytes[3].toInt() and 0xFF,
|
|
94
|
+
bytes[4].toInt() and 0xFF,
|
|
95
|
+
bytes[5].toInt() and 0xFF,
|
|
96
|
+
bytes[6].toInt() and 0xFF,
|
|
97
|
+
bytes[7].toInt() and 0xFF,
|
|
98
|
+
bytes[8].toInt() and 0xFF,
|
|
99
|
+
bytes[9].toInt() and 0xFF,
|
|
100
|
+
bytes[10].toInt() and 0xFF,
|
|
101
|
+
bytes[11].toInt() and 0xFF,
|
|
102
|
+
bytes[12].toInt() and 0xFF,
|
|
103
|
+
bytes[13].toInt() and 0xFF,
|
|
104
|
+
bytes[14].toInt() and 0xFF,
|
|
105
|
+
bytes[15].toInt() and 0xFF,
|
|
106
|
+
)
|
|
107
|
+
} catch (e: Exception) {
|
|
108
|
+
"00000000-0000-0000-0000-000000000000"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets the current update channel
|
|
113
|
+
* @return The channel name or null if not set
|
|
114
|
+
*/
|
|
115
|
+
fun getChannel(): String {
|
|
116
|
+
val id = context.resources.getIdentifier("hot_updater_channel", "string", context.packageName)
|
|
117
|
+
return if (id != 0) {
|
|
118
|
+
context.getString(id).takeIf { it.isNotEmpty() } ?: DEFAULT_CHANNEL
|
|
119
|
+
} else {
|
|
120
|
+
DEFAULT_CHANNEL
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets the path to the bundle file
|
|
126
|
+
* @return The path to the bundle file
|
|
127
|
+
*/
|
|
128
|
+
fun getJSBundleFile(): String = bundleStorage.getBundleURL()
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Updates the bundle from the specified URL
|
|
132
|
+
* @param bundleId ID of the bundle to update
|
|
133
|
+
* @param fileUrl URL of the bundle file to download (or null to reset)
|
|
134
|
+
* @param progressCallback Callback for download progress updates
|
|
135
|
+
* @return true if the update was successful
|
|
136
|
+
*/
|
|
137
|
+
suspend fun updateBundle(
|
|
138
|
+
bundleId: String,
|
|
139
|
+
fileUrl: String?,
|
|
140
|
+
progressCallback: (Double) -> Unit,
|
|
141
|
+
): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, progressCallback)
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reloads the React Native application
|
|
145
|
+
* @param activity Current activity (optional)
|
|
146
|
+
*/
|
|
147
|
+
fun reload(activity: Activity? = null) {
|
|
148
|
+
val reactIntegrationManager = ReactIntegrationManager(context)
|
|
149
|
+
val application = activity?.application ?: return
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
val reactApplication = reactIntegrationManager.getReactApplication(application)
|
|
153
|
+
val bundleURL = getJSBundleFile()
|
|
154
|
+
|
|
155
|
+
reactIntegrationManager.setJSBundle(reactApplication, bundleURL)
|
|
156
|
+
|
|
157
|
+
Handler(Looper.getMainLooper()).post {
|
|
158
|
+
reactIntegrationManager.reload(reactApplication)
|
|
159
|
+
}
|
|
160
|
+
} catch (e: Exception) {
|
|
161
|
+
Log.e("HotUpdaterImpl", "Failed to reload application", e)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gets the current activity from ReactApplicationContext
|
|
167
|
+
* @param context Context that might be a ReactApplicationContext
|
|
168
|
+
* @return The current activity or null
|
|
169
|
+
*/
|
|
170
|
+
@Suppress("UNUSED_PARAMETER")
|
|
171
|
+
fun getCurrentActivity(context: Context): Activity? {
|
|
172
|
+
// This would need to be implemented differently or moved
|
|
173
|
+
// since it requires ReactApplicationContext which introduces circular dependencies
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
}
|