native-update 1.0.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/CapacitorNativeUpdate.podspec +18 -0
- package/LICENSE +21 -0
- package/Readme.md +451 -0
- package/android/build.gradle +92 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/android/gradle.properties +17 -0
- package/android/proguard-rules.pro +29 -0
- package/android/settings.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +34 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppReviewPlugin.kt +153 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt +275 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +390 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateManager.kt +46 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt +333 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateWorker.kt +251 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/CapacitorNativeUpdatePlugin.kt +265 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +526 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +99 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt +249 -0
- package/dist/esm/__tests__/bundle-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/bundle-manager.test.js +123 -0
- package/dist/esm/__tests__/bundle-manager.test.js.map +1 -0
- package/dist/esm/__tests__/config.test.d.ts +1 -0
- package/dist/esm/__tests__/config.test.js +69 -0
- package/dist/esm/__tests__/config.test.js.map +1 -0
- package/dist/esm/__tests__/integration.test.d.ts +1 -0
- package/dist/esm/__tests__/integration.test.js +78 -0
- package/dist/esm/__tests__/integration.test.js.map +1 -0
- package/dist/esm/__tests__/security.test.d.ts +1 -0
- package/dist/esm/__tests__/security.test.js +54 -0
- package/dist/esm/__tests__/security.test.js.map +1 -0
- package/dist/esm/__tests__/version-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/version-manager.test.js +45 -0
- package/dist/esm/__tests__/version-manager.test.js.map +1 -0
- package/dist/esm/app-review/app-review-manager.d.ts +24 -0
- package/dist/esm/app-review/app-review-manager.js +195 -0
- package/dist/esm/app-review/app-review-manager.js.map +1 -0
- package/dist/esm/app-review/index.d.ts +5 -0
- package/dist/esm/app-review/index.js +6 -0
- package/dist/esm/app-review/index.js.map +1 -0
- package/dist/esm/app-review/platform-review-handler.d.ts +20 -0
- package/dist/esm/app-review/platform-review-handler.js +138 -0
- package/dist/esm/app-review/platform-review-handler.js.map +1 -0
- package/dist/esm/app-review/review-conditions-checker.d.ts +22 -0
- package/dist/esm/app-review/review-conditions-checker.js +155 -0
- package/dist/esm/app-review/review-conditions-checker.js.map +1 -0
- package/dist/esm/app-review/review-rate-limiter.d.ts +23 -0
- package/dist/esm/app-review/review-rate-limiter.js +164 -0
- package/dist/esm/app-review/review-rate-limiter.js.map +1 -0
- package/dist/esm/app-review/types.d.ts +41 -0
- package/dist/esm/app-review/types.js +2 -0
- package/dist/esm/app-review/types.js.map +1 -0
- package/dist/esm/app-update/app-update-checker.d.ts +13 -0
- package/dist/esm/app-update/app-update-checker.js +104 -0
- package/dist/esm/app-update/app-update-checker.js.map +1 -0
- package/dist/esm/app-update/app-update-installer.d.ts +19 -0
- package/dist/esm/app-update/app-update-installer.js +123 -0
- package/dist/esm/app-update/app-update-installer.js.map +1 -0
- package/dist/esm/app-update/app-update-manager.d.ts +28 -0
- package/dist/esm/app-update/app-update-manager.js +199 -0
- package/dist/esm/app-update/app-update-manager.js.map +1 -0
- package/dist/esm/app-update/app-update-notifier.d.ts +14 -0
- package/dist/esm/app-update/app-update-notifier.js +100 -0
- package/dist/esm/app-update/app-update-notifier.js.map +1 -0
- package/dist/esm/app-update/index.d.ts +6 -0
- package/dist/esm/app-update/index.js +7 -0
- package/dist/esm/app-update/index.js.map +1 -0
- package/dist/esm/app-update/platform-app-update.d.ts +19 -0
- package/dist/esm/app-update/platform-app-update.js +129 -0
- package/dist/esm/app-update/platform-app-update.js.map +1 -0
- package/dist/esm/app-update/types.d.ts +58 -0
- package/dist/esm/app-update/types.js +12 -0
- package/dist/esm/app-update/types.js.map +1 -0
- package/dist/esm/background-update/background-scheduler.d.ts +17 -0
- package/dist/esm/background-update/background-scheduler.js +195 -0
- package/dist/esm/background-update/background-scheduler.js.map +1 -0
- package/dist/esm/background-update/index.d.ts +3 -0
- package/dist/esm/background-update/index.js +3 -0
- package/dist/esm/background-update/index.js.map +1 -0
- package/dist/esm/background-update/notification-manager.d.ts +29 -0
- package/dist/esm/background-update/notification-manager.js +89 -0
- package/dist/esm/background-update/notification-manager.js.map +1 -0
- package/dist/esm/core/analytics.d.ts +70 -0
- package/dist/esm/core/analytics.js +137 -0
- package/dist/esm/core/analytics.js.map +1 -0
- package/dist/esm/core/cache-manager.d.ts +72 -0
- package/dist/esm/core/cache-manager.js +275 -0
- package/dist/esm/core/cache-manager.js.map +1 -0
- package/dist/esm/core/config.d.ts +48 -0
- package/dist/esm/core/config.js +83 -0
- package/dist/esm/core/config.js.map +1 -0
- package/dist/esm/core/errors.d.ts +51 -0
- package/dist/esm/core/errors.js +80 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/logger.d.ts +21 -0
- package/dist/esm/core/logger.js +109 -0
- package/dist/esm/core/logger.js.map +1 -0
- package/dist/esm/core/performance.d.ts +53 -0
- package/dist/esm/core/performance.js +140 -0
- package/dist/esm/core/performance.js.map +1 -0
- package/dist/esm/core/plugin-manager.d.ts +66 -0
- package/dist/esm/core/plugin-manager.js +148 -0
- package/dist/esm/core/plugin-manager.js.map +1 -0
- package/dist/esm/core/security.d.ts +93 -0
- package/dist/esm/core/security.js +315 -0
- package/dist/esm/core/security.js.map +1 -0
- package/dist/esm/definitions.d.ts +639 -0
- package/dist/esm/definitions.js +103 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/live-update/bundle-manager.d.ts +94 -0
- package/dist/esm/live-update/bundle-manager.js +310 -0
- package/dist/esm/live-update/bundle-manager.js.map +1 -0
- package/dist/esm/live-update/certificate-pinning.d.ts +38 -0
- package/dist/esm/live-update/certificate-pinning.js +78 -0
- package/dist/esm/live-update/certificate-pinning.js.map +1 -0
- package/dist/esm/live-update/download-manager.d.ts +67 -0
- package/dist/esm/live-update/download-manager.js +319 -0
- package/dist/esm/live-update/download-manager.js.map +1 -0
- package/dist/esm/live-update/update-manager.d.ts +52 -0
- package/dist/esm/live-update/update-manager.js +294 -0
- package/dist/esm/live-update/update-manager.js.map +1 -0
- package/dist/esm/live-update/version-manager.d.ts +84 -0
- package/dist/esm/live-update/version-manager.js +335 -0
- package/dist/esm/live-update/version-manager.js.map +1 -0
- package/dist/esm/plugin.d.ts +6 -0
- package/dist/esm/plugin.js +283 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/security/crypto.d.ts +25 -0
- package/dist/esm/security/crypto.js +70 -0
- package/dist/esm/security/crypto.js.map +1 -0
- package/dist/esm/security/validator.d.ts +60 -0
- package/dist/esm/security/validator.js +143 -0
- package/dist/esm/security/validator.js.map +1 -0
- package/dist/esm/web.d.ts +74 -0
- package/dist/esm/web.js +595 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +2 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.esm.js +2 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -0
- package/docs/APP_REVIEW_GUIDE.md +768 -0
- package/docs/BUNDLE_SIGNING.md +264 -0
- package/docs/LIVE_UPDATES_GUIDE.md +650 -0
- package/docs/MIGRATION.md +192 -0
- package/docs/NATIVE_UPDATES_GUIDE.md +694 -0
- package/docs/QUICK_START.md +606 -0
- package/docs/README.md +111 -0
- package/docs/REMAINING_FEATURES.md +139 -0
- package/docs/api/app-review-api.md +259 -0
- package/docs/api/app-update-api.md +238 -0
- package/docs/api/events-api.md +451 -0
- package/docs/api/live-update-api.md +265 -0
- package/docs/background-updates.md +392 -0
- package/docs/examples/advanced-scenarios.md +410 -0
- package/docs/examples/basic-usage.md +185 -0
- package/docs/features/app-reviews.md +975 -0
- package/docs/features/app-updates.md +785 -0
- package/docs/features/live-updates.md +633 -0
- package/docs/getting-started/configuration.md +468 -0
- package/docs/getting-started/installation.md +209 -0
- package/docs/getting-started/quick-start.md +379 -0
- package/docs/guides/deployment-guide.md +333 -0
- package/docs/guides/migration-from-codepush.md +142 -0
- package/docs/guides/security-best-practices.md +1057 -0
- package/docs/guides/testing-guide.md +373 -0
- package/docs/production-readiness.md +478 -0
- package/docs/security/certificate-pinning.md +122 -0
- package/docs/server-requirements.md +147 -0
- package/ios/Plugin/AppReview/AppReviewPlugin.swift +158 -0
- package/ios/Plugin/AppUpdate/AppUpdatePlugin.swift +234 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundNotificationManager.swift +329 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +396 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.m +45 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.swift +190 -0
- package/ios/Plugin/Info.plist +43 -0
- package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +689 -0
- package/ios/Plugin/LiveUpdate/WebViewConfiguration.swift +45 -0
- package/ios/Plugin/Security/SecurityManager.swift +289 -0
- package/package.json +90 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
package com.aoneahsan.nativeupdate
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import com.getcapacitor.JSObject
|
|
6
|
+
import com.getcapacitor.PluginCall
|
|
7
|
+
import kotlinx.coroutines.*
|
|
8
|
+
import okhttp3.*
|
|
9
|
+
import okhttp3.tls.HandshakeCertificates
|
|
10
|
+
import java.io.File
|
|
11
|
+
import java.io.FileOutputStream
|
|
12
|
+
import java.security.MessageDigest
|
|
13
|
+
import java.util.concurrent.TimeUnit
|
|
14
|
+
import javax.net.ssl.X509TrustManager
|
|
15
|
+
|
|
16
|
+
class LiveUpdatePlugin(
|
|
17
|
+
private val activity: Activity,
|
|
18
|
+
private val context: Context
|
|
19
|
+
) {
|
|
20
|
+
private var config: JSObject? = null
|
|
21
|
+
private var progressListener: ((JSObject) -> Unit)? = null
|
|
22
|
+
private var stateChangeListener: ((JSObject) -> Unit)? = null
|
|
23
|
+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
24
|
+
private lateinit var okHttpClient: OkHttpClient
|
|
25
|
+
private val securityManager = SecurityManager(context)
|
|
26
|
+
|
|
27
|
+
init {
|
|
28
|
+
// Initialize OkHttp with default settings
|
|
29
|
+
okHttpClient = createOkHttpClient()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private fun createOkHttpClient(): OkHttpClient {
|
|
33
|
+
val builder = OkHttpClient.Builder()
|
|
34
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
35
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
36
|
+
.writeTimeout(30, TimeUnit.SECONDS)
|
|
37
|
+
|
|
38
|
+
// Configure certificate pinning if config is available
|
|
39
|
+
if (config != null) {
|
|
40
|
+
val certificatePinner = securityManager.getCertificatePinner()
|
|
41
|
+
if (certificatePinner != null) {
|
|
42
|
+
builder.certificatePinner(certificatePinner)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return builder.build()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun configure(config: JSObject) {
|
|
50
|
+
this.config = config
|
|
51
|
+
|
|
52
|
+
// Configure security manager
|
|
53
|
+
securityManager.configure(config)
|
|
54
|
+
|
|
55
|
+
// Recreate OkHttpClient with new configuration
|
|
56
|
+
okHttpClient = createOkHttpClient()
|
|
57
|
+
|
|
58
|
+
// Validate configuration
|
|
59
|
+
val serverUrl = config.getString("serverUrl")
|
|
60
|
+
if (serverUrl != null && !serverUrl.startsWith("https://")) {
|
|
61
|
+
val enforceHttps = config.getJSObject("security")?.getBool("enforceHttps") ?: true
|
|
62
|
+
if (enforceHttps) {
|
|
63
|
+
throw IllegalArgumentException("Server URL must use HTTPS")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun setProgressListener(listener: (JSObject) -> Unit) {
|
|
69
|
+
progressListener = listener
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun setStateChangeListener(listener: (JSObject) -> Unit) {
|
|
73
|
+
stateChangeListener = listener
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun sync(call: PluginCall) {
|
|
77
|
+
scope.launch {
|
|
78
|
+
try {
|
|
79
|
+
val channel = call.getString("channel") ?: config?.getString("channel") ?: "production"
|
|
80
|
+
val serverUrl = config?.getString("serverUrl") ?: run {
|
|
81
|
+
call.reject("Server URL not configured")
|
|
82
|
+
return@launch
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for updates
|
|
86
|
+
val latestVersion = checkForUpdates(serverUrl, channel)
|
|
87
|
+
|
|
88
|
+
if (latestVersion != null) {
|
|
89
|
+
val result = JSObject()
|
|
90
|
+
result.put("status", "UPDATE_AVAILABLE")
|
|
91
|
+
result.put("version", latestVersion.getString("version"))
|
|
92
|
+
result.put("description", latestVersion.getString("description"))
|
|
93
|
+
call.resolve(result)
|
|
94
|
+
} else {
|
|
95
|
+
val result = JSObject()
|
|
96
|
+
result.put("status", "UP_TO_DATE")
|
|
97
|
+
result.put("version", getCurrentVersion())
|
|
98
|
+
call.resolve(result)
|
|
99
|
+
}
|
|
100
|
+
} catch (e: Exception) {
|
|
101
|
+
val error = JSObject()
|
|
102
|
+
error.put("code", "NETWORK_ERROR")
|
|
103
|
+
error.put("message", e.message)
|
|
104
|
+
|
|
105
|
+
val result = JSObject()
|
|
106
|
+
result.put("status", "ERROR")
|
|
107
|
+
result.put("error", error)
|
|
108
|
+
call.resolve(result)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun download(call: PluginCall) {
|
|
114
|
+
scope.launch {
|
|
115
|
+
try {
|
|
116
|
+
val url = call.getString("url") ?: run {
|
|
117
|
+
call.reject("URL is required")
|
|
118
|
+
return@launch
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val version = call.getString("version") ?: run {
|
|
122
|
+
call.reject("Version is required")
|
|
123
|
+
return@launch
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
val checksum = call.getString("checksum") ?: run {
|
|
127
|
+
call.reject("Checksum is required")
|
|
128
|
+
return@launch
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate URL
|
|
132
|
+
if (!url.startsWith("https://") && config?.getJSObject("security")?.getBool("enforceHttps") != false) {
|
|
133
|
+
call.reject("INSECURE_URL", "Download URL must use HTTPS")
|
|
134
|
+
return@launch
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
val bundleId = "bundle-${System.currentTimeMillis()}"
|
|
138
|
+
val downloadDir = File(context.filesDir, "updates/$bundleId")
|
|
139
|
+
downloadDir.mkdirs()
|
|
140
|
+
|
|
141
|
+
// Start download
|
|
142
|
+
val downloadedFile = downloadBundle(url, downloadDir, bundleId)
|
|
143
|
+
|
|
144
|
+
// Verify checksum
|
|
145
|
+
if (!verifyChecksum(downloadedFile, checksum)) {
|
|
146
|
+
downloadedFile.delete()
|
|
147
|
+
call.reject("CHECKSUM_ERROR", "Bundle checksum validation failed")
|
|
148
|
+
return@launch
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Verify signature if provided
|
|
152
|
+
val signature = call.getString("signature")
|
|
153
|
+
if (signature != null) {
|
|
154
|
+
val securityConfig = config?.getJSObject("security")
|
|
155
|
+
val publicKey = securityConfig?.getString("publicKey")
|
|
156
|
+
val enableSignatureValidation = securityConfig?.getBoolean("enableSignatureValidation", false) ?: false
|
|
157
|
+
|
|
158
|
+
if (enableSignatureValidation && publicKey != null) {
|
|
159
|
+
val fileBytes = downloadedFile.readBytes()
|
|
160
|
+
if (!securityManager.verifySignature(fileBytes, signature, publicKey)) {
|
|
161
|
+
downloadedFile.delete()
|
|
162
|
+
call.reject("SIGNATURE_ERROR", "Bundle signature validation failed")
|
|
163
|
+
return@launch
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create bundle info
|
|
169
|
+
val bundleInfo = JSObject()
|
|
170
|
+
bundleInfo.put("bundleId", bundleId)
|
|
171
|
+
bundleInfo.put("version", version)
|
|
172
|
+
bundleInfo.put("path", downloadedFile.absolutePath)
|
|
173
|
+
bundleInfo.put("downloadTime", System.currentTimeMillis())
|
|
174
|
+
bundleInfo.put("size", downloadedFile.length())
|
|
175
|
+
bundleInfo.put("status", "READY")
|
|
176
|
+
bundleInfo.put("checksum", checksum)
|
|
177
|
+
bundleInfo.put("verified", true)
|
|
178
|
+
|
|
179
|
+
// Save bundle info
|
|
180
|
+
saveBundleInfo(bundleInfo)
|
|
181
|
+
|
|
182
|
+
// Notify state change
|
|
183
|
+
stateChangeListener?.invoke(JSObject().apply {
|
|
184
|
+
put("status", "READY")
|
|
185
|
+
put("bundleId", bundleId)
|
|
186
|
+
put("version", version)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
call.resolve(bundleInfo)
|
|
190
|
+
} catch (e: Exception) {
|
|
191
|
+
call.reject("DOWNLOAD_ERROR", e.message)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fun set(call: PluginCall) {
|
|
197
|
+
val bundleId = call.getString("bundleId") ?: run {
|
|
198
|
+
call.reject("Bundle ID is required")
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Set active bundle
|
|
203
|
+
setActiveBundle(bundleId)
|
|
204
|
+
call.resolve()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun reload(call: PluginCall) {
|
|
208
|
+
// In Android, we need to restart the activity or reload the WebView
|
|
209
|
+
activity.runOnUiThread {
|
|
210
|
+
activity.recreate()
|
|
211
|
+
}
|
|
212
|
+
call.resolve()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fun reset(call: PluginCall) {
|
|
216
|
+
// Reset to original bundle
|
|
217
|
+
clearAllBundles()
|
|
218
|
+
call.resolve()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun current(call: PluginCall) {
|
|
222
|
+
val currentBundle = getCurrentBundleInfo()
|
|
223
|
+
call.resolve(currentBundle)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fun list(call: PluginCall) {
|
|
227
|
+
val bundles = getAllBundles()
|
|
228
|
+
val result = JSObject()
|
|
229
|
+
result.put("bundles", bundles)
|
|
230
|
+
call.resolve(result)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fun delete(call: PluginCall) {
|
|
234
|
+
val bundleId = call.getString("bundleId")
|
|
235
|
+
if (bundleId != null) {
|
|
236
|
+
deleteBundle(bundleId)
|
|
237
|
+
} else {
|
|
238
|
+
val keepVersions = call.getInt("keepVersions")
|
|
239
|
+
if (keepVersions != null) {
|
|
240
|
+
cleanupOldBundles(keepVersions)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
call.resolve()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun notifyAppReady(call: PluginCall) {
|
|
247
|
+
// Mark current bundle as verified
|
|
248
|
+
markBundleAsVerified()
|
|
249
|
+
call.resolve()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fun getLatest(call: PluginCall) {
|
|
253
|
+
scope.launch {
|
|
254
|
+
try {
|
|
255
|
+
val serverUrl = config?.getString("serverUrl") ?: run {
|
|
256
|
+
call.reject("Server URL not configured")
|
|
257
|
+
return@launch
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
val channel = config?.getString("channel") ?: "production"
|
|
261
|
+
val latestVersion = checkForUpdates(serverUrl, channel)
|
|
262
|
+
|
|
263
|
+
if (latestVersion != null) {
|
|
264
|
+
val result = JSObject()
|
|
265
|
+
result.put("available", true)
|
|
266
|
+
result.put("version", latestVersion.getString("version"))
|
|
267
|
+
result.put("url", latestVersion.getString("url"))
|
|
268
|
+
result.put("notes", latestVersion.getString("notes"))
|
|
269
|
+
call.resolve(result)
|
|
270
|
+
} else {
|
|
271
|
+
val result = JSObject()
|
|
272
|
+
result.put("available", false)
|
|
273
|
+
call.resolve(result)
|
|
274
|
+
}
|
|
275
|
+
} catch (e: Exception) {
|
|
276
|
+
call.reject("NETWORK_ERROR", e.message)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fun setChannel(call: PluginCall) {
|
|
282
|
+
val channel = call.getString("channel") ?: run {
|
|
283
|
+
call.reject("Channel is required")
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
config?.put("channel", channel)
|
|
288
|
+
call.resolve()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fun setUpdateUrl(call: PluginCall) {
|
|
292
|
+
val url = call.getString("url") ?: run {
|
|
293
|
+
call.reject("URL is required")
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!url.startsWith("https://") && config?.getJSObject("security")?.getBool("enforceHttps") != false) {
|
|
298
|
+
call.reject("INSECURE_URL", "Update URL must use HTTPS")
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
config?.put("serverUrl", url)
|
|
303
|
+
call.resolve()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fun validateUpdate(call: PluginCall) {
|
|
307
|
+
scope.launch {
|
|
308
|
+
try {
|
|
309
|
+
val bundlePath = call.getString("bundlePath") ?: run {
|
|
310
|
+
call.reject("Bundle path is required")
|
|
311
|
+
return@launch
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
val checksum = call.getString("checksum") ?: run {
|
|
315
|
+
call.reject("Checksum is required")
|
|
316
|
+
return@launch
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
val file = File(bundlePath)
|
|
320
|
+
val checksumValid = verifyChecksum(file, checksum)
|
|
321
|
+
|
|
322
|
+
val result = JSObject()
|
|
323
|
+
result.put("isValid", checksumValid)
|
|
324
|
+
|
|
325
|
+
val details = JSObject()
|
|
326
|
+
details.put("checksumValid", checksumValid)
|
|
327
|
+
result.put("details", details)
|
|
328
|
+
|
|
329
|
+
call.resolve(result)
|
|
330
|
+
} catch (e: Exception) {
|
|
331
|
+
call.reject("VALIDATION_ERROR", e.message)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Private helper methods
|
|
337
|
+
|
|
338
|
+
private suspend fun checkForUpdates(serverUrl: String, channel: String): JSObject? {
|
|
339
|
+
return withContext(Dispatchers.IO) {
|
|
340
|
+
try {
|
|
341
|
+
val request = Request.Builder()
|
|
342
|
+
.url("$serverUrl/check?channel=$channel")
|
|
343
|
+
.build()
|
|
344
|
+
|
|
345
|
+
val response = okHttpClient.newCall(request).execute()
|
|
346
|
+
if (response.isSuccessful) {
|
|
347
|
+
val body = response.body?.string()
|
|
348
|
+
// Parse JSON response
|
|
349
|
+
body?.let { JSObject(it) }
|
|
350
|
+
} else {
|
|
351
|
+
null
|
|
352
|
+
}
|
|
353
|
+
} catch (e: Exception) {
|
|
354
|
+
null
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private suspend fun downloadBundle(url: String, downloadDir: File, bundleId: String): File {
|
|
360
|
+
return withContext(Dispatchers.IO) {
|
|
361
|
+
val request = Request.Builder()
|
|
362
|
+
.url(url)
|
|
363
|
+
.build()
|
|
364
|
+
|
|
365
|
+
val response = okHttpClient.newCall(request).execute()
|
|
366
|
+
if (!response.isSuccessful) {
|
|
367
|
+
throw Exception("Download failed: ${response.code}")
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
val file = File(downloadDir, "bundle.zip")
|
|
371
|
+
val totalBytes = response.body?.contentLength() ?: -1
|
|
372
|
+
var downloadedBytes = 0L
|
|
373
|
+
|
|
374
|
+
response.body?.byteStream()?.use { input ->
|
|
375
|
+
FileOutputStream(file).use { output ->
|
|
376
|
+
val buffer = ByteArray(8192)
|
|
377
|
+
var read: Int
|
|
378
|
+
|
|
379
|
+
while (input.read(buffer).also { read = it } != -1) {
|
|
380
|
+
output.write(buffer, 0, read)
|
|
381
|
+
downloadedBytes += read
|
|
382
|
+
|
|
383
|
+
// Report progress
|
|
384
|
+
if (totalBytes > 0) {
|
|
385
|
+
val percent = ((downloadedBytes * 100) / totalBytes).toInt()
|
|
386
|
+
progressListener?.invoke(JSObject().apply {
|
|
387
|
+
put("percent", percent)
|
|
388
|
+
put("bytesDownloaded", downloadedBytes)
|
|
389
|
+
put("totalBytes", totalBytes)
|
|
390
|
+
put("bundleId", bundleId)
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
file
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private fun verifyChecksum(file: File, expectedChecksum: String): Boolean {
|
|
402
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
403
|
+
file.inputStream().use { input ->
|
|
404
|
+
val buffer = ByteArray(8192)
|
|
405
|
+
var read: Int
|
|
406
|
+
while (input.read(buffer).also { read = it } != -1) {
|
|
407
|
+
digest.update(buffer, 0, read)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
val calculatedChecksum = digest.digest().joinToString("") { "%02x".format(it) }
|
|
412
|
+
return calculatedChecksum == expectedChecksum
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private fun getCurrentVersion(): String {
|
|
416
|
+
// Get current bundle version or app version
|
|
417
|
+
return "1.0.0"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private fun saveBundleInfo(bundleInfo: JSObject) {
|
|
421
|
+
// Save bundle info to SharedPreferences or database
|
|
422
|
+
val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
|
|
423
|
+
val bundleId = bundleInfo.getString("bundleId")
|
|
424
|
+
prefs.edit().putString(bundleId, bundleInfo.toString()).apply()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private fun setActiveBundle(bundleId: String) {
|
|
428
|
+
val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
|
|
429
|
+
prefs.edit().putString("active_bundle", bundleId).apply()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private fun clearAllBundles() {
|
|
433
|
+
val updateDir = File(context.filesDir, "updates")
|
|
434
|
+
updateDir.deleteRecursively()
|
|
435
|
+
|
|
436
|
+
val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
|
|
437
|
+
prefs.edit().clear().apply()
|
|
438
|
+
|
|
439
|
+
val activePrefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
|
|
440
|
+
activePrefs.edit().remove("active_bundle").apply()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun getCurrentBundleInfo(): JSObject {
|
|
444
|
+
val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
|
|
445
|
+
val activeBundleId = prefs.getString("active_bundle", null)
|
|
446
|
+
|
|
447
|
+
if (activeBundleId != null) {
|
|
448
|
+
val bundlePrefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
|
|
449
|
+
val bundleData = bundlePrefs.getString(activeBundleId, null)
|
|
450
|
+
if (bundleData != null) {
|
|
451
|
+
return JSObject(bundleData)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Return default bundle
|
|
456
|
+
return JSObject().apply {
|
|
457
|
+
put("bundleId", "default")
|
|
458
|
+
put("version", "1.0.0")
|
|
459
|
+
put("path", "/")
|
|
460
|
+
put("downloadTime", System.currentTimeMillis())
|
|
461
|
+
put("size", 0)
|
|
462
|
+
put("status", "ACTIVE")
|
|
463
|
+
put("checksum", "")
|
|
464
|
+
put("verified", true)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private fun getAllBundles(): List<JSObject> {
|
|
469
|
+
val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
|
|
470
|
+
return prefs.all.mapNotNull { entry ->
|
|
471
|
+
try {
|
|
472
|
+
JSObject(entry.value as String)
|
|
473
|
+
} catch (e: Exception) {
|
|
474
|
+
null
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private fun deleteBundle(bundleId: String) {
|
|
480
|
+
// Delete bundle files
|
|
481
|
+
val bundleDir = File(context.filesDir, "updates/$bundleId")
|
|
482
|
+
bundleDir.deleteRecursively()
|
|
483
|
+
|
|
484
|
+
// Remove from preferences
|
|
485
|
+
val prefs = context.getSharedPreferences("native_update_bundles", Context.MODE_PRIVATE)
|
|
486
|
+
prefs.edit().remove(bundleId).apply()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private fun cleanupOldBundles(keepVersions: Int) {
|
|
490
|
+
val bundles = getAllBundles().sortedByDescending { it.getLong("downloadTime") }
|
|
491
|
+
if (bundles.size > keepVersions) {
|
|
492
|
+
bundles.drop(keepVersions).forEach { bundle ->
|
|
493
|
+
bundle.getString("bundleId")?.let { deleteBundle(it) }
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private fun markBundleAsVerified() {
|
|
499
|
+
val prefs = context.getSharedPreferences("native_update", Context.MODE_PRIVATE)
|
|
500
|
+
prefs.edit().putBoolean("current_bundle_verified", true).apply()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Async method for background update checks
|
|
504
|
+
suspend fun getLatestVersionAsync(): LatestVersion? {
|
|
505
|
+
return try {
|
|
506
|
+
val serverUrl = config?.getString("serverUrl") ?: return null
|
|
507
|
+
val channel = config?.getString("channel") ?: "production"
|
|
508
|
+
|
|
509
|
+
val latestVersion = checkForUpdates(serverUrl, channel)
|
|
510
|
+
|
|
511
|
+
if (latestVersion != null) {
|
|
512
|
+
LatestVersion(
|
|
513
|
+
available = true,
|
|
514
|
+
version = latestVersion.getString("version")
|
|
515
|
+
)
|
|
516
|
+
} else {
|
|
517
|
+
LatestVersion(
|
|
518
|
+
available = false,
|
|
519
|
+
version = null
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
} catch (e: Exception) {
|
|
523
|
+
null
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package com.aoneahsan.nativeupdate
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.work.ExistingWorkPolicy
|
|
8
|
+
import androidx.work.OneTimeWorkRequestBuilder
|
|
9
|
+
import androidx.work.WorkManager
|
|
10
|
+
import androidx.work.workDataOf
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles notification actions for background updates
|
|
14
|
+
*/
|
|
15
|
+
class NotificationActionReceiver : BroadcastReceiver() {
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
const val ACTION_INSTALL_NOW = "com.aoneahsan.nativeupdate.ACTION_INSTALL_NOW"
|
|
19
|
+
const val ACTION_INSTALL_LATER = "com.aoneahsan.nativeupdate.ACTION_INSTALL_LATER"
|
|
20
|
+
const val ACTION_DISMISS = "com.aoneahsan.nativeupdate.ACTION_DISMISS"
|
|
21
|
+
|
|
22
|
+
const val EXTRA_UPDATE_TYPE = "update_type"
|
|
23
|
+
const val EXTRA_BUNDLE_ID = "bundle_id"
|
|
24
|
+
const val EXTRA_VERSION = "version"
|
|
25
|
+
|
|
26
|
+
private const val TAG = "NotificationActionReceiver"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
30
|
+
val action = intent.action ?: return
|
|
31
|
+
|
|
32
|
+
Log.d(TAG, "Received action: $action")
|
|
33
|
+
|
|
34
|
+
when (action) {
|
|
35
|
+
ACTION_INSTALL_NOW -> handleInstallNow(context, intent)
|
|
36
|
+
ACTION_INSTALL_LATER -> handleInstallLater(context, intent)
|
|
37
|
+
ACTION_DISMISS -> handleDismiss(context, intent)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Cancel the notification
|
|
41
|
+
val notificationManager = BackgroundNotificationManager(context)
|
|
42
|
+
notificationManager.cancelUpdateNotification()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun handleInstallNow(context: Context, intent: Intent) {
|
|
46
|
+
val updateType = intent.getStringExtra(EXTRA_UPDATE_TYPE) ?: return
|
|
47
|
+
|
|
48
|
+
when (updateType) {
|
|
49
|
+
"live_update" -> {
|
|
50
|
+
val bundleId = intent.getStringExtra(EXTRA_BUNDLE_ID) ?: return
|
|
51
|
+
// Trigger immediate installation through WorkManager
|
|
52
|
+
val workRequest = OneTimeWorkRequestBuilder<BackgroundUpdateWorker>()
|
|
53
|
+
.setInputData(workDataOf(
|
|
54
|
+
"action" to "install_bundle",
|
|
55
|
+
"bundle_id" to bundleId
|
|
56
|
+
))
|
|
57
|
+
.build()
|
|
58
|
+
|
|
59
|
+
WorkManager.getInstance(context).enqueueUniqueWork(
|
|
60
|
+
"install_update_$bundleId",
|
|
61
|
+
ExistingWorkPolicy.REPLACE,
|
|
62
|
+
workRequest
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Log.d(TAG, "Scheduled immediate installation for bundle: $bundleId")
|
|
66
|
+
}
|
|
67
|
+
"app_update" -> {
|
|
68
|
+
// Open Play Store for app update
|
|
69
|
+
try {
|
|
70
|
+
val packageName = context.packageName
|
|
71
|
+
val playStoreIntent = Intent(Intent.ACTION_VIEW).apply {
|
|
72
|
+
data = android.net.Uri.parse("market://details?id=$packageName")
|
|
73
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
74
|
+
}
|
|
75
|
+
context.startActivity(playStoreIntent)
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
Log.e(TAG, "Failed to open Play Store", e)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun handleInstallLater(context: Context, intent: Intent) {
|
|
84
|
+
// Store preference to install later
|
|
85
|
+
val prefs = context.getSharedPreferences("capacitor_native_update", Context.MODE_PRIVATE)
|
|
86
|
+
prefs.edit().apply {
|
|
87
|
+
putBoolean("pending_update", true)
|
|
88
|
+
putLong("deferred_until", System.currentTimeMillis() + 24 * 60 * 60 * 1000) // 24 hours
|
|
89
|
+
apply()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Log.d(TAG, "Update deferred for 24 hours")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun handleDismiss(context: Context, intent: Intent) {
|
|
96
|
+
// Just dismiss the notification, already handled in onReceive
|
|
97
|
+
Log.d(TAG, "Update notification dismissed")
|
|
98
|
+
}
|
|
99
|
+
}
|