@nebula-rn/host 0.0.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.
Files changed (37) hide show
  1. package/NebulaHost.podspec +23 -0
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build.gradle +27 -0
  11. package/android/consumer-rules.pro +1 -0
  12. package/android/src/main/AndroidManifest.xml +1 -0
  13. package/android/src/main/java/com/hectorzhuang/nebula/NebulaActivity.kt +290 -0
  14. package/android/src/main/java/com/hectorzhuang/nebula/NebulaAppManager.kt +134 -0
  15. package/android/src/main/java/com/hectorzhuang/nebula/NebulaConfig.kt +324 -0
  16. package/android/src/main/java/com/hectorzhuang/nebula/NebulaEventHub.kt +49 -0
  17. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHost.kt +145 -0
  18. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHostModalActivity.kt +178 -0
  19. package/android/src/main/java/com/hectorzhuang/nebula/NebulaManifestManager.kt +130 -0
  20. package/android/src/main/java/com/hectorzhuang/nebula/NebulaNativeModule.kt +604 -0
  21. package/android/src/main/java/com/hectorzhuang/nebula/NebulaPackage.kt +16 -0
  22. package/android/src/main/java/com/hectorzhuang/nebula/NebulaRouter.kt +300 -0
  23. package/ios/Nebula/NebulaAppManager.swift +355 -0
  24. package/ios/Nebula/NebulaConfig.swift +549 -0
  25. package/ios/Nebula/NebulaContainerController.swift +580 -0
  26. package/ios/Nebula/NebulaDevLoading.swift +333 -0
  27. package/ios/Nebula/NebulaHost.swift +611 -0
  28. package/ios/Nebula/NebulaManifest.swift +214 -0
  29. package/ios/Nebula/NebulaNativeModule.swift +682 -0
  30. package/ios/Nebula/NebulaNativeModuleBridge.m +364 -0
  31. package/ios/Nebula/NebulaPerformanceMonitor.swift +46 -0
  32. package/ios/Nebula/NebulaRouter.swift +594 -0
  33. package/ios/Nebula/NebulaRouterBridge.m +19 -0
  34. package/ios/Nebula/RNInstanceViewController.swift +52 -0
  35. package/package.json +41 -0
  36. package/react-native.config.js +14 -0
  37. package/src/index.ts +9 -0
@@ -0,0 +1,324 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import android.app.Application
4
+ import java.io.File
5
+ import java.io.FileOutputStream
6
+ import java.net.URI
7
+ import java.net.URL
8
+ import java.net.URLConnection
9
+ import java.util.zip.ZipInputStream
10
+ import java.util.concurrent.ConcurrentHashMap
11
+
12
+ object NebulaConfig {
13
+ private const val PREFS_NAME = "nebula.config"
14
+ private const val SERVER_BASE_URL_KEY = "_serverBaseURL"
15
+ private val installedApps = ConcurrentHashMap<String, NebulaInstalledApp>()
16
+ private lateinit var application: Application
17
+
18
+ var serverBaseURL: String?
19
+ get() = application.getSharedPreferences(PREFS_NAME, 0).getString(SERVER_BASE_URL_KEY, null)
20
+ set(value) {
21
+ val prefs = application.getSharedPreferences(PREFS_NAME, 0)
22
+ val normalized = normalizeServerBaseURL(value)
23
+ if (normalized == null) {
24
+ prefs.edit().remove(SERVER_BASE_URL_KEY).apply()
25
+ } else {
26
+ prefs.edit().putString(SERVER_BASE_URL_KEY, normalized).apply()
27
+ }
28
+ }
29
+
30
+ fun initialize(app: Application) {
31
+ application = app
32
+ restoreInstalledApps()
33
+ }
34
+
35
+ fun sandboxDir(appId: String): File {
36
+ val dir = File(application.filesDir, "nebula/$appId")
37
+ if (!dir.exists()) {
38
+ dir.mkdirs()
39
+ }
40
+ return dir
41
+ }
42
+
43
+ fun bundleFile(appId: String): File = File(sandboxDir(appId), "index.android.bundle")
44
+
45
+ fun installApp(
46
+ appId: String,
47
+ bundleURL: String,
48
+ connectToURLMetroServer: Boolean = false,
49
+ ): NebulaInstalledApp {
50
+ val sandboxDir = sandboxDir(appId)
51
+ val bundleFile = bundleFile(appId)
52
+ downloadToFile(bundleURL, bundleFile)
53
+ var manifest: NebulaManifest? = null
54
+ deriveManifestUrl(bundleURL)?.let { manifestURL ->
55
+ runCatching {
56
+ downloadToFile(manifestURL, File(sandboxDir, "app.json"))
57
+ manifest = NebulaManifestManager.loadManifest(appId, sandboxDir)
58
+ }
59
+ }
60
+
61
+ val installed =
62
+ NebulaInstalledApp(
63
+ appId = appId,
64
+ mode = if (connectToURLMetroServer) "development" else "production",
65
+ bundlePath = bundleFile.absolutePath,
66
+ sourceUrl = bundleURL,
67
+ version = if (connectToURLMetroServer) null else manifest?.version,
68
+ updateStrategy =
69
+ if (connectToURLMetroServer) {
70
+ "manual"
71
+ } else {
72
+ manifest?.updateStrategy ?: "manual"
73
+ },
74
+ )
75
+ installedApps[appId] = installed
76
+ persistInstalledApp(installed)
77
+ return installed
78
+ }
79
+
80
+ fun installApp(
81
+ appId: String,
82
+ bundleURL: String,
83
+ manifestURL: String,
84
+ assetsURL: String,
85
+ connectToURLMetroServer: Boolean = false,
86
+ ): NebulaInstalledApp {
87
+ val sandboxDir = sandboxDir(appId)
88
+ val bundleFile = bundleFile(appId)
89
+ downloadToFile(bundleURL, bundleFile)
90
+ downloadToFile(manifestURL, File(sandboxDir, "app.json"))
91
+ if (!connectToURLMetroServer) {
92
+ downloadAndExtractAssets(assetsURL, sandboxDir)
93
+ }
94
+ val manifest = NebulaManifestManager.loadManifest(appId, sandboxDir)
95
+ val installed =
96
+ NebulaInstalledApp(
97
+ appId = appId,
98
+ mode = if (connectToURLMetroServer) "development" else "production",
99
+ bundlePath = bundleFile.absolutePath,
100
+ sourceUrl = bundleURL,
101
+ version = if (connectToURLMetroServer) null else manifest?.version,
102
+ updateStrategy =
103
+ if (connectToURLMetroServer) {
104
+ "manual"
105
+ } else {
106
+ manifest?.updateStrategy ?: "manual"
107
+ },
108
+ )
109
+ installedApps[appId] = installed
110
+ persistInstalledApp(installed)
111
+ return installed
112
+ }
113
+
114
+ fun getInstalledApp(appId: String): NebulaInstalledApp? = installedApps[appId]
115
+
116
+ fun isDevelopmentMode(installedApp: NebulaInstalledApp): Boolean {
117
+ return installedApp.mode.equals("development", ignoreCase = true)
118
+ }
119
+
120
+ fun getDevServerHost(installedApp: NebulaInstalledApp): String? {
121
+ if (!isDevelopmentMode(installedApp)) {
122
+ return null
123
+ }
124
+
125
+ val uri = runCatching { URI(installedApp.sourceUrl) }.getOrNull() ?: return null
126
+ val host = uri.host ?: return null
127
+ val port = uri.port
128
+ return if (port > 0) "$host:$port" else host
129
+ }
130
+
131
+ fun getDevModulePath(installedApp: NebulaInstalledApp): String? {
132
+ if (!isDevelopmentMode(installedApp)) {
133
+ return null
134
+ }
135
+
136
+ val uri = runCatching { URI(installedApp.sourceUrl) }.getOrNull() ?: return null
137
+ val path = uri.path?.trim().orEmpty()
138
+ if (path.isEmpty()) {
139
+ return null
140
+ }
141
+
142
+ val trimmedPath = path.trimStart('/')
143
+ return if (trimmedPath.endsWith(".bundle")) {
144
+ trimmedPath.removeSuffix(".bundle")
145
+ } else {
146
+ trimmedPath
147
+ }
148
+ }
149
+
150
+ fun installedAppIds(): List<String> = installedApps.keys.sorted()
151
+
152
+ fun clearApp(appId: String) {
153
+ installedApps.remove(appId)
154
+ application.getSharedPreferences(PREFS_NAME, 0).edit().remove(appId).apply()
155
+ NebulaManifestManager.removeManifest(appId)
156
+ sandboxDir(appId).deleteRecursively()
157
+ }
158
+
159
+ private fun persistInstalledApp(installedApp: NebulaInstalledApp) {
160
+ val json =
161
+ """
162
+ {"appId":"${installedApp.appId}","mode":"${installedApp.mode}","bundlePath":"${installedApp.bundlePath}","sourceUrl":"${installedApp.sourceUrl}","version":${installedApp.version?.let { "\"$it\"" } ?: "null"},"updateStrategy":"${installedApp.updateStrategy}"}
163
+ """.trimIndent()
164
+ application.getSharedPreferences(PREFS_NAME, 0).edit().putString(installedApp.appId, json)
165
+ .apply()
166
+ }
167
+
168
+ private fun restoreInstalledApps() {
169
+ val prefs = application.getSharedPreferences(PREFS_NAME, 0).all
170
+ prefs.forEach { (key, value) ->
171
+ if (key == SERVER_BASE_URL_KEY) {
172
+ return@forEach
173
+ }
174
+ val raw = value as? String ?: return@forEach
175
+ runCatching {
176
+ val json = org.json.JSONObject(raw)
177
+ installedApps[key] =
178
+ NebulaInstalledApp(
179
+ appId = json.optString("appId", key),
180
+ mode = json.optString("mode", "production"),
181
+ bundlePath = json.optString("bundlePath"),
182
+ sourceUrl = json.optString("sourceUrl"),
183
+ version = json.optString("version", null),
184
+ updateStrategy = json.optString("updateStrategy", "manual"),
185
+ )
186
+ NebulaManifestManager.loadManifest(key, sandboxDir(key))
187
+ }
188
+ }
189
+ }
190
+
191
+ private fun deriveManifestUrl(bundleURL: String): String? {
192
+ return runCatching {
193
+ val uri = URI(bundleURL)
194
+ val path = uri.path ?: return null
195
+ val parentPath = path.substringBeforeLast('/', "")
196
+ if (parentPath.isEmpty()) {
197
+ return null
198
+ }
199
+ URI(
200
+ uri.scheme,
201
+ uri.authority,
202
+ "$parentPath/app.json",
203
+ null,
204
+ null,
205
+ ).toString()
206
+ }.getOrNull()
207
+ }
208
+
209
+ fun resolveRemoteUrl(rawURL: String): String {
210
+ val trimmedURL = rawURL.trim()
211
+ if (trimmedURL.isEmpty() || trimmedURL.startsWith("file://")) {
212
+ return rawURL
213
+ }
214
+
215
+ val base = serverBaseURL ?: return trimmedURL
216
+ val baseUri = runCatching { URI(base) }.getOrNull() ?: return trimmedURL
217
+
218
+ if (trimmedURL.startsWith("/")) {
219
+ return URI(baseUri.scheme, baseUri.authority, trimmedURL, null, null).toString()
220
+ }
221
+
222
+ val remoteUri = runCatching { URI(trimmedURL) }.getOrNull()
223
+ if (remoteUri?.scheme?.lowercase() in listOf("http", "https")) {
224
+ if (shouldReplaceRemoteHost(remoteUri?.host)) {
225
+ return URI(
226
+ baseUri.scheme,
227
+ baseUri.authority,
228
+ remoteUri?.path,
229
+ remoteUri?.query,
230
+ remoteUri?.fragment,
231
+ ).toString()
232
+ }
233
+ return trimmedURL
234
+ }
235
+
236
+ val normalizedBasePath = baseUri.path?.trim('/').orEmpty()
237
+ val normalizedRelativePath = trimmedURL.trim('/')
238
+ val mergedPath =
239
+ listOf(normalizedBasePath, normalizedRelativePath).filter { it.isNotEmpty() }
240
+ .joinToString("/")
241
+ return URI(baseUri.scheme, baseUri.authority, "/$mergedPath", null, null).toString()
242
+ }
243
+
244
+ private fun downloadToFile(source: String, destination: File) {
245
+ destination.parentFile?.mkdirs()
246
+ val connection = openConnection(source)
247
+ connection.getInputStream().use { input ->
248
+ FileOutputStream(destination).use { output ->
249
+ input.copyTo(output)
250
+ }
251
+ }
252
+ }
253
+
254
+ private fun openConnection(source: String): URLConnection {
255
+ return if (source.startsWith("file://")) {
256
+ URI(source).toURL().openConnection()
257
+ } else if (source.startsWith("/")) {
258
+ File(source).toURI().toURL().openConnection()
259
+ } else {
260
+ URL(source).openConnection().apply {
261
+ connectTimeout = 15000
262
+ readTimeout = 15000
263
+ }
264
+ }
265
+ }
266
+
267
+ fun normalizeServerBaseURL(rawURL: String?): String? {
268
+ val trimmedURL = rawURL?.trim().orEmpty()
269
+ if (trimmedURL.isEmpty()) {
270
+ return null
271
+ }
272
+
273
+ val uri = runCatching { URI(trimmedURL) }.getOrNull() ?: return null
274
+ val scheme = uri.scheme?.lowercase() ?: return null
275
+ if (scheme != "http" && scheme != "https") {
276
+ return null
277
+ }
278
+ val host = uri.host ?: return null
279
+ val normalizedPath = uri.path?.trimEnd('/').orEmpty()
280
+ return URI(
281
+ scheme,
282
+ uri.userInfo,
283
+ host,
284
+ uri.port,
285
+ normalizedPath,
286
+ null,
287
+ null,
288
+ ).toString()
289
+ }
290
+
291
+ private fun shouldReplaceRemoteHost(host: String?): Boolean {
292
+ val normalizedHost = host?.lowercase() ?: return false
293
+ return normalizedHost == "localhost" || normalizedHost == "127.0.0.1" || normalizedHost == "0.0.0.0"
294
+ }
295
+
296
+ private fun downloadAndExtractAssets(assetsURL: String, sandboxDir: File) {
297
+ val zipFile = File(sandboxDir, "assets.zip")
298
+ try {
299
+ downloadToFile(assetsURL, zipFile)
300
+ extractZipFile(zipFile, sandboxDir)
301
+ } finally {
302
+ zipFile.delete()
303
+ }
304
+ }
305
+
306
+ private fun extractZipFile(zipFile: File, destinationDir: File) {
307
+ ZipInputStream(zipFile.inputStream().buffered()).use { zipStream ->
308
+ var entry = zipStream.nextEntry
309
+ while (entry != null) {
310
+ val entryFile = File(destinationDir, entry.name)
311
+ if (entry.isDirectory) {
312
+ entryFile.mkdirs()
313
+ } else {
314
+ entryFile.parentFile?.mkdirs()
315
+ FileOutputStream(entryFile).use { output ->
316
+ zipStream.copyTo(output)
317
+ }
318
+ }
319
+ zipStream.closeEntry()
320
+ entry = zipStream.nextEntry
321
+ }
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,49 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import java.lang.ref.WeakReference
4
+ import java.util.UUID
5
+ import java.util.concurrent.CopyOnWriteArrayList
6
+
7
+ data class NebulaBridgeMessage(
8
+ val direction: String,
9
+ val appId: String,
10
+ val message: Map<String, Any?>,
11
+ val sourceEmitterId: String,
12
+ val timestamp: Double,
13
+ )
14
+
15
+ data class NebulaPageLifecycleEvent(
16
+ val appId: String,
17
+ val instanceId: String,
18
+ val routePath: String,
19
+ val type: String,
20
+ )
21
+
22
+ object NebulaEventHub {
23
+ private val moduleRefs = CopyOnWriteArrayList<WeakReference<NebulaNativeModule>>()
24
+
25
+ fun nextEmitterId(): String = UUID.randomUUID().toString()
26
+
27
+ fun register(module: NebulaNativeModule) {
28
+ cleanup()
29
+ moduleRefs.add(WeakReference(module))
30
+ }
31
+
32
+ fun unregister(module: NebulaNativeModule) {
33
+ moduleRefs.removeAll { it.get() == null || it.get() === module }
34
+ }
35
+
36
+ fun publishBridgeMessage(message: NebulaBridgeMessage) {
37
+ cleanup()
38
+ moduleRefs.forEach { it.get()?.handleBridgeMessage(message) }
39
+ }
40
+
41
+ fun publishPageLifecycle(event: NebulaPageLifecycleEvent) {
42
+ cleanup()
43
+ moduleRefs.forEach { it.get()?.handlePageLifecycle(event) }
44
+ }
45
+
46
+ private fun cleanup() {
47
+ moduleRefs.removeAll { it.get() == null }
48
+ }
49
+ }
@@ -0,0 +1,145 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.content.pm.ApplicationInfo
6
+ import android.os.Bundle
7
+ import android.preference.PreferenceManager
8
+ import com.facebook.react.ReactApplication
9
+ import com.facebook.react.ReactHost
10
+ import com.facebook.react.ReactPackage
11
+ import java.net.URL
12
+ import java.util.concurrent.Executors
13
+ import org.json.JSONObject
14
+
15
+ interface NebulaHostDelegate {
16
+ fun isDebugBuild(): Boolean = false
17
+
18
+ fun createMiniAppPackages(application: Application): List<ReactPackage> = emptyList()
19
+ }
20
+
21
+ object NebulaHost {
22
+ private lateinit var application: Application
23
+ private var delegate: NebulaHostDelegate? = null
24
+ @Volatile private var topActivity: Activity? = null
25
+ @Volatile private var hostModalActivity: Activity? = null
26
+ private val ioExecutor = Executors.newSingleThreadExecutor()
27
+
28
+ fun initialize(app: Application, delegate: NebulaHostDelegate? = null) {
29
+ application = app
30
+ this.delegate = delegate
31
+ NebulaConfig.initialize(app)
32
+ NebulaAppManager.initialize(app, delegate)
33
+ app.registerActivityLifecycleCallbacks(
34
+ object : Application.ActivityLifecycleCallbacks {
35
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
36
+ override fun onActivityStarted(activity: Activity) = Unit
37
+ override fun onActivityResumed(activity: Activity) {
38
+ topActivity = activity
39
+ }
40
+ override fun onActivityPaused(activity: Activity) = Unit
41
+ override fun onActivityStopped(activity: Activity) = Unit
42
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
43
+ override fun onActivityDestroyed(activity: Activity) {
44
+ if (topActivity === activity) {
45
+ topActivity = null
46
+ }
47
+ }
48
+ },
49
+ )
50
+ }
51
+
52
+ fun currentActivity(): Activity? = topActivity
53
+
54
+ fun presentedHostModalActivity(): Activity? = hostModalActivity
55
+
56
+ fun setPresentedHostModalActivity(activity: Activity?) {
57
+ hostModalActivity = activity
58
+ }
59
+
60
+ fun isDebugBuild(): Boolean =
61
+ delegate?.isDebugBuild()
62
+ ?: ((application.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0)
63
+
64
+ fun createMiniAppPackages(): List<ReactPackage> = delegate?.createMiniAppPackages(application) ?: emptyList()
65
+
66
+ fun resolveHostReactHost(app: Application = application): ReactHost {
67
+ return when (app) {
68
+ is ReactApplication -> app.reactHost ?: error("ReactApplication.reactHost is null")
69
+ else -> error("Application does not implement ReactApplication")
70
+ }
71
+ }
72
+
73
+ fun installApp(
74
+ appId: String,
75
+ bundleURL: String,
76
+ connectToURLMetroServer: Boolean = false,
77
+ ): NebulaInstalledApp {
78
+ val installedApp = NebulaConfig.installApp(appId, bundleURL, connectToURLMetroServer)
79
+ NebulaAppManager.invalidate(appId)
80
+ return installedApp
81
+ }
82
+
83
+ fun preloadApp(appId: String) {
84
+ NebulaAppManager.preload(appId)
85
+ }
86
+
87
+ fun closeApp(appId: String) {
88
+ NebulaRouter.closeApp(appId).getOrThrow()
89
+ }
90
+
91
+ fun uninstallApp(appId: String) {
92
+ NebulaAppManager.invalidate(appId)
93
+ NebulaConfig.clearApp(appId)
94
+ }
95
+
96
+ fun openApp(activity: Activity, appId: String, initialProps: Map<String, Any?> = emptyMap()) {
97
+ val routePath =
98
+ NebulaManifestManager.getManifest(appId)?.entryPagePath
99
+ ?: NebulaManifestManager.loadManifest(appId, NebulaConfig.sandboxDir(appId))?.entryPagePath
100
+ ?: "/"
101
+ NebulaRouter.openApp(activity, appId, routePath, initialProps)
102
+ }
103
+
104
+ fun installedApps(): List<String> = NebulaConfig.installedAppIds()
105
+
106
+ fun handleIncomingUrl(activity: Activity, url: android.net.Uri?): Boolean {
107
+ if (url == null) {
108
+ return false
109
+ }
110
+ val isReviewInstallLink =
111
+ (url.host == "review" && url.path.orEmpty().startsWith("/install/")) ||
112
+ (url.host == "miniapp" && url.path.orEmpty().startsWith("/install/")) ||
113
+ url.path.orEmpty().startsWith("/review/install/")
114
+ if (!isReviewInstallLink) {
115
+ return false
116
+ }
117
+
118
+ val installUrl = url.getQueryParameter("installUrl") ?: return false
119
+ ioExecutor.execute {
120
+ runCatching {
121
+ val resolvedInstallUrl = NebulaConfig.resolveRemoteUrl(installUrl)
122
+ val payload = JSONObject(URL(resolvedInstallUrl).readText())
123
+ val appId = payload.getString("appId")
124
+ val bundles = payload.optJSONObject("bundles")
125
+ val rawBundleUrl =
126
+ bundles?.optString("android")?.takeIf { it.isNotBlank() }
127
+ ?: payload.optString("bundleUrl").takeIf { it.isNotBlank() }
128
+ ?: error("Missing Android bundle URL in review install payload")
129
+ val bundleUrl = NebulaConfig.resolveRemoteUrl(rawBundleUrl)
130
+ val manifestUrl = NebulaConfig.resolveRemoteUrl(payload.getString("manifestUrl"))
131
+ val assetsUrl =
132
+ payload.optJSONObject("assetsUrl")?.optString("android")?.takeIf { it.isNotBlank() }
133
+ ?: error("Missing Android assets URL in review install payload")
134
+ val resolvedAssetsUrl = NebulaConfig.resolveRemoteUrl(assetsUrl)
135
+ NebulaConfig.installApp(appId, bundleUrl, manifestUrl, resolvedAssetsUrl, false)
136
+ activity.runOnUiThread {
137
+ openApp(activity, appId)
138
+ }
139
+ }.onFailure { error ->
140
+ error.printStackTrace()
141
+ }
142
+ }
143
+ return true
144
+ }
145
+ }
@@ -0,0 +1,178 @@
1
+ package com.hectorzhuang.nebula
2
+
3
+ import android.content.Context
4
+ import android.content.Intent
5
+ import android.content.pm.PackageManager
6
+ import android.os.Bundle
7
+ import android.view.View
8
+ import android.widget.FrameLayout
9
+ import androidx.appcompat.app.AppCompatActivity
10
+ import com.facebook.react.ReactHost
11
+ import com.facebook.react.interfaces.fabric.ReactSurface
12
+ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
13
+ import com.facebook.react.modules.core.PermissionAwareActivity
14
+ import com.facebook.react.modules.core.PermissionListener
15
+ import org.json.JSONObject
16
+
17
+ class NebulaHostModalActivity :
18
+ AppCompatActivity(),
19
+ DefaultHardwareBackBtnHandler,
20
+ PermissionAwareActivity {
21
+ companion object {
22
+ private const val EXTRA_MODULE_NAME = "nebula.modal.module_name"
23
+ private const val EXTRA_INITIAL_PROPS = "nebula.modal.initial_props"
24
+
25
+ fun createIntent(
26
+ context: Context,
27
+ moduleName: String,
28
+ initialProps: Map<String, Any?>,
29
+ ): Intent {
30
+ return Intent(context, NebulaHostModalActivity::class.java).apply {
31
+ putExtra(EXTRA_MODULE_NAME, moduleName)
32
+ putExtra(EXTRA_INITIAL_PROPS, JSONObject(initialProps).toString())
33
+ }
34
+ }
35
+ }
36
+
37
+ private lateinit var rootHost: FrameLayout
38
+ private var reactSurface: ReactSurface? = null
39
+ private var permissionListener: PermissionListener? = null
40
+
41
+ override fun onCreate(savedInstanceState: Bundle?) {
42
+ super.onCreate(savedInstanceState)
43
+
44
+ val moduleName = intent.getStringExtra(EXTRA_MODULE_NAME) ?: error("Missing moduleName")
45
+ val initialProps = parseJsonMap(intent.getStringExtra(EXTRA_INITIAL_PROPS)).toBundle()
46
+
47
+ rootHost = FrameLayout(this).apply {
48
+ id = View.generateViewId()
49
+ layoutParams =
50
+ FrameLayout.LayoutParams(
51
+ FrameLayout.LayoutParams.MATCH_PARENT,
52
+ FrameLayout.LayoutParams.MATCH_PARENT,
53
+ )
54
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
55
+ }
56
+
57
+ setContentView(rootHost)
58
+
59
+ reactSurface = resolveReactHost().createSurface(this, moduleName, initialProps)
60
+ reactSurface?.start()
61
+ reactSurface?.view?.let { view ->
62
+ rootHost.addView(
63
+ view,
64
+ FrameLayout.LayoutParams(
65
+ FrameLayout.LayoutParams.MATCH_PARENT,
66
+ FrameLayout.LayoutParams.MATCH_PARENT,
67
+ ),
68
+ )
69
+ }
70
+
71
+ NebulaHost.setPresentedHostModalActivity(this)
72
+ }
73
+
74
+ override fun onResume() {
75
+ super.onResume()
76
+ resolveReactHost().onHostResume(this, this)
77
+ }
78
+
79
+ override fun onPause() {
80
+ resolveReactHost().onHostPause(this)
81
+ super.onPause()
82
+ }
83
+
84
+ override fun onDestroy() {
85
+ reactSurface?.stop()
86
+ reactSurface?.clear()
87
+ reactSurface = null
88
+ resolveReactHost().onHostDestroy(this)
89
+ if (NebulaHost.presentedHostModalActivity() === this) {
90
+ NebulaHost.setPresentedHostModalActivity(null)
91
+ }
92
+ super.onDestroy()
93
+ }
94
+
95
+ override fun invokeDefaultOnBackPressed() {
96
+ super.onBackPressed()
97
+ }
98
+
99
+ override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
100
+ return super.checkPermission(permission, pid, uid)
101
+ }
102
+
103
+ override fun checkSelfPermission(permission: String): Int {
104
+ return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
105
+ super.checkSelfPermission(permission)
106
+ } else {
107
+ PackageManager.PERMISSION_GRANTED
108
+ }
109
+ }
110
+
111
+ override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
112
+ return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
113
+ super.shouldShowRequestPermissionRationale(permission)
114
+ } else {
115
+ false
116
+ }
117
+ }
118
+
119
+ override fun requestPermissions(
120
+ permissions: Array<String>,
121
+ requestCode: Int,
122
+ listener: PermissionListener?,
123
+ ) {
124
+ permissionListener = listener
125
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
126
+ super.requestPermissions(permissions, requestCode)
127
+ }
128
+ }
129
+
130
+ override fun onRequestPermissionsResult(
131
+ requestCode: Int,
132
+ permissions: Array<String>,
133
+ grantResults: IntArray,
134
+ ) {
135
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
136
+ val shouldClear =
137
+ permissionListener?.onRequestPermissionsResult(
138
+ requestCode,
139
+ permissions,
140
+ grantResults,
141
+ ) ?: false
142
+ if (shouldClear) {
143
+ permissionListener = null
144
+ }
145
+ }
146
+
147
+ private fun resolveReactHost(): ReactHost {
148
+ return NebulaHost.resolveHostReactHost(application)
149
+ }
150
+
151
+ private fun parseJsonMap(raw: String?): Map<String, Any?> {
152
+ if (raw.isNullOrBlank()) {
153
+ return emptyMap()
154
+ }
155
+ return NebulaManifestManager.jsonObjectToMap(JSONObject(raw))
156
+ }
157
+ }
158
+
159
+ private fun Map<String, Any?>.toBundle(): Bundle {
160
+ val bundle = Bundle()
161
+ forEach { (key, value) ->
162
+ when (value) {
163
+ null -> bundle.putString(key, null)
164
+ is String -> bundle.putString(key, value)
165
+ is Int -> bundle.putInt(key, value)
166
+ is Double -> bundle.putDouble(key, value)
167
+ is Boolean -> bundle.putBoolean(key, value)
168
+ is Float -> bundle.putFloat(key, value)
169
+ is Long -> bundle.putLong(key, value)
170
+ is Map<*, *> -> {
171
+ @Suppress("UNCHECKED_CAST")
172
+ bundle.putBundle(key, (value as Map<String, Any?>).toBundle())
173
+ }
174
+ else -> bundle.putString(key, value.toString())
175
+ }
176
+ }
177
+ return bundle
178
+ }