expo-dev-launcher 0.7.0 → 0.8.3
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/CHANGELOG.md +52 -0
- package/README.md +1 -1
- package/android/build.gradle +2 -1
- package/android/src/debug/assets/expo_dev_launcher_android.bundle +396 -403
- package/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt +10 -6
- package/android/src/debug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherAppLoaderFactory.kt +5 -5
- package/android/src/debug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherExpoAppLoader.kt +5 -5
- package/android/src/debug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherLocalAppLoader.kt +3 -3
- package/android/src/debug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherPublishedAppLoader.kt +2 -6
- package/android/src/debug/java/expo/modules/devlauncher/modules/DevLauncherInternalModule.kt +6 -1
- package/android/src/debug/java/expo/modules/devlauncher/modules/DevLauncherModule.kt +1 -1
- package/android/src/main/java/expo/modules/devlauncher/launcher/DevLauncherControllerInterface.kt +2 -2
- package/android/src/main/java/expo/modules/devlauncher/launcher/configurators/DevLauncherExpoActivityConfigurator.kt +12 -11
- package/android/src/main/java/expo/modules/devlauncher/launcher/manifest/DevLauncherManifestParser.kt +4 -2
- package/android/src/main/java/expo/modules/devlauncher/launcher/manifest/DevLauncherManifestTypes.kt +11 -26
- package/android/src/main/java/expo/modules/devlauncher/launcher/menu/DevLauncherMenuDelegate.kt +3 -3
- package/android/src/release/java/expo/modules/devlauncher/DevLauncherController.kt +2 -2
- package/android/src/testDebug/java/expo/modules/devlauncher/DevLauncherControllerTest.kt +50 -0
- package/android/src/testDebug/java/expo/modules/devlauncher/launcher/DevLauncherRecentlyOpenedAppsRegistryTest.kt +43 -0
- package/android/src/testDebug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherAppLoaderFactoryTest.kt +3 -3
- package/android/src/testDebug/java/expo/modules/devlauncher/modules/DevLauncherModuleTest.kt +38 -0
- package/build/DevLauncherErrorManager.js +7 -0
- package/build/DevLauncherErrorManager.js.map +1 -1
- package/expo-dev-launcher.podspec +2 -0
- package/ios/EXDevLauncher.m +8 -2
- package/ios/EXDevLauncher.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/EXDevLauncher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/EXDevLauncher.xcodeproj/xcuserdata/brentvatne.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/EXDevLauncher.xcworkspace/xcuserdata/brentvatne.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/EXDevLauncherController.h +2 -2
- package/ios/EXDevLauncherController.m +22 -11
- package/ios/EXDevLauncherInternal.m +5 -0
- package/ios/EXDevLauncherMenuDelegate.swift +3 -3
- package/ios/Errors/EXDevLauncherRedBox.m +5 -0
- package/ios/Manifest/EXDevLauncherManifestHelper.swift +15 -12
- package/ios/Manifest/EXDevLauncherManifestParser.h +3 -3
- package/ios/Manifest/EXDevLauncherManifestParser.m +17 -5
- package/ios/Tests/EXDevLauncherManifestHelperTests.swift +28 -0
- package/ios/Tests/EXDevLauncherManifestParserTests.m +113 -0
- package/ios/Tests/EXDevLauncherModuleTests.m +39 -0
- package/ios/devvyc 2021-05-14 18-19-10/devvyc.ipa +0 -0
- package/ios/main.jsbundle +414 -420
- package/package.json +6 -6
- package/plugin/build/constants.d.ts +1 -0
- package/plugin/build/constants.js +4 -0
- package/plugin/build/utils.d.ts +2 -0
- package/plugin/build/utils.js +24 -0
- package/plugin/build/withDevLauncher.js +27 -39
- package/plugin/build/withDevLauncherAppDelegate.d.ts +1 -0
- package/plugin/build/withDevLauncherAppDelegate.js +161 -16
- package/plugin/src/constants.ts +1 -0
- package/plugin/src/utils.ts +25 -0
- package/plugin/src/withDevLauncher.ts +32 -45
- package/plugin/src/withDevLauncherAppDelegate.ts +211 -23
- package/src/DevLauncherErrorManager.ts +7 -0
- package/android/src/main/java/expo/modules/devlauncher/launcher/manifest/DevLauncherManifest.kt +0 -111
- package/ios/Manifest/EXDevLauncherManifest.swift +0 -158
|
@@ -28,12 +28,12 @@ import expo.modules.devlauncher.launcher.DevLauncherLifecycle
|
|
|
28
28
|
import expo.modules.devlauncher.launcher.DevLauncherReactActivityDelegateSupplier
|
|
29
29
|
import expo.modules.devlauncher.launcher.DevLauncherRecentlyOpenedAppsRegistry
|
|
30
30
|
import expo.modules.devlauncher.launcher.loaders.DevLauncherAppLoaderFactoryInterface
|
|
31
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
32
31
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifestParser
|
|
33
32
|
import expo.modules.devlauncher.launcher.menu.DevLauncherMenuDelegate
|
|
34
33
|
import expo.modules.devlauncher.react.activitydelegates.DevLauncherReactActivityNOPDelegate
|
|
35
34
|
import expo.modules.devlauncher.react.activitydelegates.DevLauncherReactActivityRedirectDelegate
|
|
36
35
|
import expo.modules.devlauncher.tests.DevLauncherTestInterceptor
|
|
36
|
+
import expo.modules.manifests.core.Manifest
|
|
37
37
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
38
38
|
import kotlinx.coroutines.GlobalScope
|
|
39
39
|
import kotlinx.coroutines.launch
|
|
@@ -72,7 +72,7 @@ class DevLauncherController private constructor()
|
|
|
72
72
|
override val devClientHost = DevLauncherClientHost((context as Application), DEV_LAUNCHER_HOST)
|
|
73
73
|
|
|
74
74
|
private val recentlyOpedAppsRegistry = DevLauncherRecentlyOpenedAppsRegistry(context)
|
|
75
|
-
override var manifest:
|
|
75
|
+
override var manifest: Manifest? = null
|
|
76
76
|
private set
|
|
77
77
|
override var manifestURL: Uri? = null
|
|
78
78
|
private set
|
|
@@ -188,16 +188,20 @@ class DevLauncherController private constructor()
|
|
|
188
188
|
}
|
|
189
189
|
return true
|
|
190
190
|
}
|
|
191
|
+
|
|
192
|
+
intent?.let {
|
|
193
|
+
return handleExternalIntent(it)
|
|
194
|
+
}
|
|
195
|
+
|
|
191
196
|
return false
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
private fun handleExternalIntent(intent: Intent): Boolean {
|
|
195
|
-
if (mode
|
|
196
|
-
|
|
200
|
+
if (mode != Mode.APP && intent.action != Intent.ACTION_MAIN) {
|
|
201
|
+
pendingIntentRegistry.intent = intent
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
|
|
200
|
-
return true
|
|
204
|
+
return false
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
private fun ensureHostWasCleared(host: ReactNativeHost, activityToBeInvalidated: ReactActivity? = null) {
|
|
@@ -8,15 +8,15 @@ import expo.modules.devlauncher.helpers.loadUpdate
|
|
|
8
8
|
import expo.modules.devlauncher.koin.DevLauncherKoinComponent
|
|
9
9
|
import expo.modules.devlauncher.koin.optInject
|
|
10
10
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
11
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
12
11
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifestParser
|
|
12
|
+
import expo.modules.manifests.core.Manifest
|
|
13
13
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
14
14
|
import org.koin.core.component.inject
|
|
15
15
|
import java.lang.IllegalStateException
|
|
16
16
|
|
|
17
17
|
interface DevLauncherAppLoaderFactoryInterface {
|
|
18
18
|
suspend fun createAppLoader(url: Uri, manifestParser: DevLauncherManifestParser): DevLauncherAppLoader
|
|
19
|
-
fun getManifest():
|
|
19
|
+
fun getManifest(): Manifest?
|
|
20
20
|
fun shouldUseDeveloperSupport(): Boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ class DevLauncherAppLoaderFactory : DevLauncherKoinComponent, DevLauncherAppLoad
|
|
|
27
27
|
private val controller: DevLauncherControllerInterface by inject()
|
|
28
28
|
|
|
29
29
|
private var instanceWasCreated = false
|
|
30
|
-
private var manifest:
|
|
30
|
+
private var manifest: Manifest? = null
|
|
31
31
|
private var useDeveloperSupport = true
|
|
32
32
|
|
|
33
33
|
override suspend fun createAppLoader(url: Uri, manifestParser: DevLauncherManifestParser): DevLauncherAppLoader {
|
|
@@ -45,7 +45,7 @@ class DevLauncherAppLoaderFactory : DevLauncherKoinComponent, DevLauncherAppLoad
|
|
|
45
45
|
} else {
|
|
46
46
|
val configuration = createUpdatesConfigurationWithUrl(url)
|
|
47
47
|
val update = updatesInterface!!.loadUpdate(configuration, context) {
|
|
48
|
-
manifest =
|
|
48
|
+
manifest = Manifest.fromManifestJson(it) // TODO: might be able to pass actual manifest object in here
|
|
49
49
|
return@loadUpdate !manifest!!.isUsingDeveloperTool()
|
|
50
50
|
}
|
|
51
51
|
if (manifest!!.isUsingDeveloperTool()) {
|
|
@@ -59,7 +59,7 @@ class DevLauncherAppLoaderFactory : DevLauncherKoinComponent, DevLauncherAppLoad
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
override fun getManifest():
|
|
62
|
+
override fun getManifest(): Manifest? = checkIfInstanceWasCreated { manifest }
|
|
63
63
|
|
|
64
64
|
override fun shouldUseDeveloperSupport(): Boolean = checkIfInstanceWasCreated { useDeveloperSupport }
|
|
65
65
|
|
package/android/src/debug/java/expo/modules/devlauncher/launcher/loaders/DevLauncherExpoAppLoader.kt
CHANGED
|
@@ -12,11 +12,11 @@ import expo.modules.devlauncher.helpers.isValidColor
|
|
|
12
12
|
import expo.modules.devlauncher.helpers.setProtectedDeclaredField
|
|
13
13
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
14
14
|
import expo.modules.devlauncher.launcher.configurators.DevLauncherExpoActivityConfigurator
|
|
15
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
16
15
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherUserInterface
|
|
16
|
+
import expo.modules.manifests.core.Manifest
|
|
17
17
|
|
|
18
18
|
abstract class DevLauncherExpoAppLoader(
|
|
19
|
-
private val manifest:
|
|
19
|
+
private val manifest: Manifest,
|
|
20
20
|
appHost: ReactNativeHost,
|
|
21
21
|
context: Context,
|
|
22
22
|
controller: DevLauncherControllerInterface,
|
|
@@ -39,7 +39,7 @@ abstract class DevLauncherExpoAppLoader(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
private fun applyUserInterfaceStyle(context: ReactContext) {
|
|
42
|
-
val userInterfaceStyle = when (manifest.
|
|
42
|
+
val userInterfaceStyle = when (manifest.getAndroidUserInterfaceStyle()) {
|
|
43
43
|
DevLauncherUserInterface.DARK -> "dark"
|
|
44
44
|
DevLauncherUserInterface.LIGHT -> "light"
|
|
45
45
|
else -> return
|
|
@@ -70,7 +70,7 @@ abstract class DevLauncherExpoAppLoader(
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
private fun applyBackgroundColor(view: View) {
|
|
73
|
-
val backgroundColor = manifest.
|
|
73
|
+
val backgroundColor = manifest.getAndroidBackgroundColor() ?: return
|
|
74
74
|
if (!isValidColor(backgroundColor)) {
|
|
75
75
|
return
|
|
76
76
|
}
|
|
@@ -78,6 +78,6 @@ abstract class DevLauncherExpoAppLoader(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
override fun getAppName(): String? {
|
|
81
|
-
return manifest.
|
|
81
|
+
return manifest.getName()
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -5,19 +5,19 @@ import android.net.Uri
|
|
|
5
5
|
import com.facebook.react.ReactNativeHost
|
|
6
6
|
import expo.modules.devlauncher.helpers.injectReactInterceptor
|
|
7
7
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
8
|
-
import expo.modules.
|
|
8
|
+
import expo.modules.manifests.core.Manifest
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Implementation of DevLauncherExpoAppLoader which is used for loading locally served projects in
|
|
12
12
|
* development.
|
|
13
13
|
*/
|
|
14
14
|
class DevLauncherLocalAppLoader(
|
|
15
|
-
private val manifest:
|
|
15
|
+
private val manifest: Manifest,
|
|
16
16
|
private val appHost: ReactNativeHost,
|
|
17
17
|
private val context: Context,
|
|
18
18
|
controller: DevLauncherControllerInterface
|
|
19
19
|
) : DevLauncherExpoAppLoader(manifest, appHost, context, controller) {
|
|
20
20
|
override fun injectBundleLoader(): Boolean {
|
|
21
|
-
return injectReactInterceptor(context, appHost, Uri.parse(manifest.
|
|
21
|
+
return injectReactInterceptor(context, appHost, Uri.parse(manifest.getBundleURL()))
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -3,19 +3,15 @@ package expo.modules.devlauncher.launcher.loaders
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import com.facebook.react.ReactNativeHost
|
|
5
5
|
import expo.modules.devlauncher.helpers.injectLocalBundleLoader
|
|
6
|
-
import expo.modules.devlauncher.helpers.isValidColor
|
|
7
|
-
import expo.modules.devlauncher.helpers.setProtectedDeclaredField
|
|
8
6
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
9
|
-
import expo.modules.
|
|
10
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherUserInterface
|
|
11
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
7
|
+
import expo.modules.manifests.core.Manifest
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
10
|
* Implementation of DevLauncherExpoAppLoader which is used for loading published projects served
|
|
15
11
|
* remotely.
|
|
16
12
|
*/
|
|
17
13
|
class DevLauncherPublishedAppLoader(
|
|
18
|
-
manifest:
|
|
14
|
+
manifest: Manifest,
|
|
19
15
|
private val localBundlePath: String,
|
|
20
16
|
private val appHost: ReactNativeHost,
|
|
21
17
|
context: Context,
|
package/android/src/debug/java/expo/modules/devlauncher/modules/DevLauncherInternalModule.kt
CHANGED
|
@@ -100,7 +100,12 @@ class DevLauncherInternalModule(reactContext: ReactApplicationContext?)
|
|
|
100
100
|
|
|
101
101
|
@ReactMethod
|
|
102
102
|
fun getPendingDeepLink(promise: Promise) {
|
|
103
|
-
|
|
103
|
+
intentRegistry.intent?.data?.let {
|
|
104
|
+
promise.resolve(it.toString())
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
promise.resolve(intentRegistry.intent?.action)
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
private fun onNewPendingIntent(intent: Intent) {
|
|
@@ -15,7 +15,7 @@ class DevLauncherModule(reactContext: ReactApplicationContext?) : ReactContextBa
|
|
|
15
15
|
|
|
16
16
|
override fun getConstants(): Map<String, Any?> {
|
|
17
17
|
val manifestString = try {
|
|
18
|
-
controller?.manifest?.
|
|
18
|
+
controller?.manifest?.getRawJson().toString()
|
|
19
19
|
} catch (_: IllegalStateException) {
|
|
20
20
|
null
|
|
21
21
|
}
|
package/android/src/main/java/expo/modules/devlauncher/launcher/DevLauncherControllerInterface.kt
CHANGED
|
@@ -7,7 +7,7 @@ import com.facebook.react.ReactActivityDelegate
|
|
|
7
7
|
import com.facebook.react.ReactNativeHost
|
|
8
8
|
import com.facebook.react.bridge.ReactContext
|
|
9
9
|
import expo.modules.devlauncher.DevLauncherController
|
|
10
|
-
import expo.modules.
|
|
10
|
+
import expo.modules.manifests.core.Manifest
|
|
11
11
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
12
12
|
|
|
13
13
|
interface DevLauncherControllerInterface {
|
|
@@ -21,7 +21,7 @@ interface DevLauncherControllerInterface {
|
|
|
21
21
|
fun getCurrentReactActivityDelegate(activity: ReactActivity, delegateSupplierDevLauncher: DevLauncherReactActivityDelegateSupplier): ReactActivityDelegate
|
|
22
22
|
fun handleIntent(intent: Intent?, activityToBeInvalidated: ReactActivity?): Boolean
|
|
23
23
|
|
|
24
|
-
val manifest:
|
|
24
|
+
val manifest: Manifest?
|
|
25
25
|
val manifestURL: Uri?
|
|
26
26
|
val devClientHost: DevLauncherClientHost
|
|
27
27
|
val mode: DevLauncherController.Mode
|
|
@@ -16,29 +16,29 @@ import com.facebook.react.ReactActivity
|
|
|
16
16
|
import expo.modules.devlauncher.helpers.RGBAtoARGB
|
|
17
17
|
import expo.modules.devlauncher.helpers.isValidColor
|
|
18
18
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherOrientation
|
|
19
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
20
19
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherStatusBarStyle
|
|
20
|
+
import expo.modules.manifests.core.Manifest
|
|
21
21
|
|
|
22
22
|
class DevLauncherExpoActivityConfigurator(
|
|
23
|
-
private var manifest:
|
|
23
|
+
private var manifest: Manifest,
|
|
24
24
|
private val context: Context
|
|
25
25
|
) {
|
|
26
26
|
fun applyTaskDescription(activity: Activity) {
|
|
27
|
-
if (!isValidColor(manifest.
|
|
27
|
+
if (!isValidColor(manifest.getPrimaryColor())) {
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
|
-
val color = Color.parseColor(manifest.
|
|
30
|
+
val color = Color.parseColor(manifest.getPrimaryColor())
|
|
31
31
|
val icon = BitmapFactory.decodeResource(context.resources, context.applicationInfo.icon)
|
|
32
32
|
|
|
33
33
|
activity.setTaskDescription(ActivityManager.TaskDescription(
|
|
34
|
-
manifest.
|
|
34
|
+
manifest.getName(),
|
|
35
35
|
icon,
|
|
36
36
|
color
|
|
37
37
|
))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
fun applyOrientation(activity: ReactActivity) {
|
|
41
|
-
when (manifest.
|
|
41
|
+
when (manifest.getOrientation()) {
|
|
42
42
|
DevLauncherOrientation.DEFAULT -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
43
43
|
DevLauncherOrientation.PORTRAIT -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
44
44
|
DevLauncherOrientation.LANDSCAPE -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
@@ -47,10 +47,11 @@ class DevLauncherExpoActivityConfigurator(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
fun applyStatusBarConfiguration(activity: ReactActivity) {
|
|
50
|
-
val
|
|
51
|
-
val
|
|
52
|
-
val
|
|
53
|
-
val
|
|
50
|
+
val statusBarOptions = manifest.getAndroidStatusBarOptions()
|
|
51
|
+
val style = statusBarOptions?.optString("barStyle")
|
|
52
|
+
val backgroundColor = statusBarOptions?.optString("backgroundColor")
|
|
53
|
+
val translucent = statusBarOptions == null || statusBarOptions.optBoolean("translucent", true)
|
|
54
|
+
val hidden = statusBarOptions != null && statusBarOptions.optBoolean("hidden", false)
|
|
54
55
|
|
|
55
56
|
activity.runOnUiThread {
|
|
56
57
|
// clear android:windowTranslucentStatus flag from Window as RN achieves translucency using WindowInsets
|
|
@@ -80,7 +81,7 @@ class DevLauncherExpoActivityConfigurator(
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
@UiThread
|
|
83
|
-
private fun setStyle(style:
|
|
84
|
+
private fun setStyle(style: String?, activity: Activity): String {
|
|
84
85
|
var appliedStatusBarStyle = DevLauncherStatusBarStyle.LIGHT
|
|
85
86
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
86
87
|
val decorView = activity.window.decorView
|
|
@@ -3,7 +3,9 @@ package expo.modules.devlauncher.launcher.manifest
|
|
|
3
3
|
import android.net.Uri
|
|
4
4
|
import expo.modules.devlauncher.helpers.await
|
|
5
5
|
import expo.modules.devlauncher.helpers.fetch
|
|
6
|
+
import expo.modules.manifests.core.Manifest
|
|
6
7
|
import okhttp3.OkHttpClient
|
|
8
|
+
import org.json.JSONObject
|
|
7
9
|
import java.io.Reader
|
|
8
10
|
|
|
9
11
|
class DevLauncherManifestParser(
|
|
@@ -28,9 +30,9 @@ class DevLauncherManifestParser(
|
|
|
28
30
|
return response.body()!!.charStream()
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
suspend fun parseManifest():
|
|
33
|
+
suspend fun parseManifest(): Manifest {
|
|
32
34
|
downloadManifest().use {
|
|
33
|
-
return
|
|
35
|
+
return Manifest.fromManifestJson(JSONObject(it.readText()))
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|
package/android/src/main/java/expo/modules/devlauncher/launcher/manifest/DevLauncherManifestTypes.kt
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
package expo.modules.devlauncher.launcher.manifest
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
DEFAULT,
|
|
8
|
-
|
|
9
|
-
@SerializedName("portrait")
|
|
10
|
-
PORTRAIT,
|
|
11
|
-
|
|
12
|
-
@SerializedName("landscape")
|
|
13
|
-
LANDSCAPE
|
|
3
|
+
object DevLauncherOrientation {
|
|
4
|
+
const val DEFAULT = "default"
|
|
5
|
+
const val PORTRAIT = "portrait"
|
|
6
|
+
const val LANDSCAPE = "landscape"
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@SerializedName("dark")
|
|
21
|
-
DARK,
|
|
22
|
-
|
|
23
|
-
@SerializedName("light")
|
|
24
|
-
LIGHT
|
|
9
|
+
object DevLauncherUserInterface {
|
|
10
|
+
const val AUTOMATIC = "automatic"
|
|
11
|
+
const val DARK = "dark"
|
|
12
|
+
const val LIGHT = "light"
|
|
25
13
|
}
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@SerializedName("light-content")
|
|
32
|
-
LIGHT
|
|
15
|
+
object DevLauncherStatusBarStyle {
|
|
16
|
+
const val DARK = "dark-content"
|
|
17
|
+
const val LIGHT = "light-content"
|
|
33
18
|
}
|
package/android/src/main/java/expo/modules/devlauncher/launcher/menu/DevLauncherMenuDelegate.kt
CHANGED
|
@@ -26,10 +26,10 @@ private class LauncherDelegate(private val controller: DevLauncherControllerInte
|
|
|
26
26
|
|
|
27
27
|
private class AppDelegate(private val controller: DevLauncherControllerInterface) : DevMenuDelegateInterface {
|
|
28
28
|
override fun appInfo(): Bundle = Bundle().apply {
|
|
29
|
-
putString("appName", controller.manifest?.
|
|
30
|
-
putString("appVersion", controller.manifest?.
|
|
29
|
+
putString("appName", controller.manifest?.getName() ?: "Development Client - App")
|
|
30
|
+
putString("appVersion", controller.manifest?.getVersion())
|
|
31
31
|
putString("appIcon", null)
|
|
32
|
-
putString("hostUrl", controller.manifest?.
|
|
32
|
+
putString("hostUrl", controller.manifest?.getHostUri()
|
|
33
33
|
?: reactInstanceManager().devSupportManager?.sourceUrl)
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -10,7 +10,7 @@ import com.facebook.react.bridge.ReactContext
|
|
|
10
10
|
import expo.modules.devlauncher.launcher.DevLauncherClientHost
|
|
11
11
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
12
12
|
import expo.modules.devlauncher.launcher.DevLauncherReactActivityDelegateSupplier
|
|
13
|
-
import expo.modules.
|
|
13
|
+
import expo.modules.manifests.core.Manifest
|
|
14
14
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
15
15
|
|
|
16
16
|
const val DEV_LAUNCHER_IS_NOT_AVAILABLE = "DevLauncher isn't available in release builds"
|
|
@@ -28,7 +28,7 @@ class DevLauncherController private constructor() : DevLauncherControllerInterfa
|
|
|
28
28
|
override val devClientHost: DevLauncherClientHost
|
|
29
29
|
get() = throw IllegalStateException(DEV_LAUNCHER_IS_NOT_AVAILABLE)
|
|
30
30
|
|
|
31
|
-
override val manifest:
|
|
31
|
+
override val manifest: Manifest
|
|
32
32
|
get() = throw IllegalStateException(DEV_LAUNCHER_IS_NOT_AVAILABLE)
|
|
33
33
|
|
|
34
34
|
override val manifestURL: Uri
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package expo.modules.devlauncher
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import com.facebook.react.ReactNativeHost
|
|
7
|
+
import com.google.common.truth.Truth
|
|
8
|
+
import expo.interfaces.devmenu.DevMenuManagerInterface
|
|
9
|
+
import expo.modules.devlauncher.koin.DevLauncherKoinContext
|
|
10
|
+
import expo.modules.devlauncher.tests.DevLauncherTestInterceptor
|
|
11
|
+
import io.mockk.mockk
|
|
12
|
+
import io.mockk.verify
|
|
13
|
+
import org.junit.Before
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import org.robolectric.RobolectricTestRunner
|
|
17
|
+
|
|
18
|
+
@RunWith(RobolectricTestRunner::class)
|
|
19
|
+
class DevLauncherControllerTest {
|
|
20
|
+
|
|
21
|
+
internal class DevLauncherTestInterceptorAllowReinitialization : DevLauncherTestInterceptor {
|
|
22
|
+
override fun allowReinitialization() = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Before
|
|
26
|
+
fun setup() {
|
|
27
|
+
val reactNativeHost = mockk<ReactNativeHost>(relaxed = true)
|
|
28
|
+
DevLauncherKoinContext.reinitialize()
|
|
29
|
+
DevLauncherKoinContext.app.koin.declare(DevLauncherTestInterceptorAllowReinitialization())
|
|
30
|
+
DevLauncherController.initialize(ApplicationProvider.getApplicationContext(), reactNativeHost)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun `sets shouldAutoLaunch on dev menu manager`() {
|
|
35
|
+
val controller = DevLauncherController.instance as DevLauncherController
|
|
36
|
+
val mockDevMenuManager = mockk<DevMenuManagerInterface>(relaxed = true)
|
|
37
|
+
controller.devMenuManager = mockDevMenuManager
|
|
38
|
+
Truth.assertThat(controller.canLaunchDevMenuOnStart).isTrue()
|
|
39
|
+
|
|
40
|
+
controller.handleIntent(Intent().apply {
|
|
41
|
+
data = Uri.parse("https://expo-development-client")
|
|
42
|
+
putExtra("EXDevMenuDisableAutoLaunch", true)
|
|
43
|
+
}, null)
|
|
44
|
+
|
|
45
|
+
verify {
|
|
46
|
+
mockDevMenuManager.setCanLaunchDevMenuOnStart(false)
|
|
47
|
+
}
|
|
48
|
+
Truth.assertThat(controller.canLaunchDevMenuOnStart).isFalse()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package expo.modules.devlauncher.launcher
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import androidx.test.core.app.ApplicationProvider
|
|
5
|
+
import com.google.common.truth.Truth
|
|
6
|
+
import io.mockk.every
|
|
7
|
+
import io.mockk.mockkObject
|
|
8
|
+
import io.mockk.unmockkAll
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import org.junit.runner.RunWith
|
|
11
|
+
import org.robolectric.RobolectricTestRunner
|
|
12
|
+
|
|
13
|
+
@RunWith(RobolectricTestRunner::class)
|
|
14
|
+
internal class DevLauncherRecentlyOpenedAppsRegistryTest {
|
|
15
|
+
@Test
|
|
16
|
+
fun `adds app to registry`() {
|
|
17
|
+
val urlString = "http://localhost:8081"
|
|
18
|
+
val registry = DevLauncherRecentlyOpenedAppsRegistry(ApplicationProvider.getApplicationContext())
|
|
19
|
+
registry.appWasOpened(Uri.parse(urlString), "test-app")
|
|
20
|
+
val apps = registry.getRecentlyOpenedApps()
|
|
21
|
+
Truth.assertThat(apps[urlString]).isEqualTo("test-app")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun `removes app from registry after 3 days have elapsed`() {
|
|
26
|
+
val currentTime = System.currentTimeMillis()
|
|
27
|
+
val time3DaysAnd1SecondAgo = currentTime - ((1000 * 60 * 60 * 24 * 3) + 1000)
|
|
28
|
+
|
|
29
|
+
mockkObject(DevLauncherRecentlyOpenedAppsRegistry.TimeHelper)
|
|
30
|
+
every {
|
|
31
|
+
DevLauncherRecentlyOpenedAppsRegistry.TimeHelper.getCurrentTime()
|
|
32
|
+
} returns time3DaysAnd1SecondAgo
|
|
33
|
+
|
|
34
|
+
val urlString = "http://localhost:8081"
|
|
35
|
+
val registry = DevLauncherRecentlyOpenedAppsRegistry(ApplicationProvider.getApplicationContext())
|
|
36
|
+
registry.appWasOpened(Uri.parse(urlString), "test-app")
|
|
37
|
+
|
|
38
|
+
unmockkAll()
|
|
39
|
+
|
|
40
|
+
val apps = registry.getRecentlyOpenedApps()
|
|
41
|
+
Truth.assertThat(apps[urlString]).isNull()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -8,8 +8,8 @@ import com.google.common.truth.Truth
|
|
|
8
8
|
import expo.modules.devlauncher.helpers.loadUpdate
|
|
9
9
|
import expo.modules.devlauncher.koin.DevLauncherKoinContext
|
|
10
10
|
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
11
|
-
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifest
|
|
12
11
|
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifestParser
|
|
12
|
+
import expo.modules.manifests.core.Manifest
|
|
13
13
|
import expo.modules.updatesinterface.UpdatesInterface
|
|
14
14
|
import io.mockk.coEvery
|
|
15
15
|
import io.mockk.mockk
|
|
@@ -64,7 +64,7 @@ internal class DevLauncherAppLoaderFactoryTest {
|
|
|
64
64
|
val appLoaderFactory = DevLauncherAppLoaderFactory()
|
|
65
65
|
|
|
66
66
|
val manifestParser = mockk<DevLauncherManifestParser>()
|
|
67
|
-
val manifest =
|
|
67
|
+
val manifest = Manifest.fromManifestJson(JSONObject(developmentManifestJSONString))
|
|
68
68
|
coEvery { manifestParser.isManifestUrl() } returns true
|
|
69
69
|
coEvery { manifestParser.parseManifest() } returns manifest
|
|
70
70
|
|
|
@@ -78,7 +78,7 @@ internal class DevLauncherAppLoaderFactoryTest {
|
|
|
78
78
|
val appLoaderFactory = DevLauncherAppLoaderFactory()
|
|
79
79
|
|
|
80
80
|
val manifestParser = mockk<DevLauncherManifestParser>()
|
|
81
|
-
val manifest =
|
|
81
|
+
val manifest = Manifest.fromManifestJson(JSONObject(publishedManifestJSONString))
|
|
82
82
|
coEvery { manifestParser.isManifestUrl() } returns true
|
|
83
83
|
coEvery { manifestParser.parseManifest() } returns manifest
|
|
84
84
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package expo.modules.devlauncher.modules
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.google.common.truth.Truth
|
|
6
|
+
import expo.modules.devlauncher.koin.DevLauncherKoinContext
|
|
7
|
+
import expo.modules.devlauncher.launcher.DevLauncherControllerInterface
|
|
8
|
+
import io.mockk.every
|
|
9
|
+
import io.mockk.mockk
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
import org.junit.runner.RunWith
|
|
12
|
+
import org.koin.dsl.module
|
|
13
|
+
import org.robolectric.RobolectricTestRunner
|
|
14
|
+
|
|
15
|
+
@RunWith(RobolectricTestRunner::class)
|
|
16
|
+
class DevLauncherModuleTest {
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun `exports manifestURL for currently loaded app`() {
|
|
20
|
+
// used by snack
|
|
21
|
+
|
|
22
|
+
val urlString = "https://exp.host/@test/test?query=param"
|
|
23
|
+
val manifestURL = Uri.parse(urlString)
|
|
24
|
+
|
|
25
|
+
val devLauncherController = mockk<DevLauncherControllerInterface>(relaxed = true)
|
|
26
|
+
DevLauncherKoinContext.app.koin.loadModules(listOf(
|
|
27
|
+
module {
|
|
28
|
+
single { devLauncherController }
|
|
29
|
+
}
|
|
30
|
+
))
|
|
31
|
+
every { devLauncherController.manifestURL } returns manifestURL
|
|
32
|
+
|
|
33
|
+
val reactApplicationContext = mockk<ReactApplicationContext>(relaxed = true)
|
|
34
|
+
val module = DevLauncherModule(reactApplicationContext)
|
|
35
|
+
val constants = module.constants
|
|
36
|
+
Truth.assertThat(constants["manifestURL"]).isEqualTo(urlString)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
let isErrorHandlingEnabled = true;
|
|
2
|
+
let wasHit = false; // whether the original error handler was called
|
|
2
3
|
const unavailableErrorPossibleSolutions = `Some possible solutions:
|
|
3
4
|
- Make sure that the method is available on the current platform.
|
|
4
5
|
- Make sure you're using the newest available version of this development client.
|
|
@@ -32,8 +33,14 @@ function customizeError(error) {
|
|
|
32
33
|
}
|
|
33
34
|
function errorHandler(originalHandler, error, isFatal) {
|
|
34
35
|
if (error instanceof Error) {
|
|
36
|
+
// Suppresses `"main" has not been registered` error only if it was caused by a different error.
|
|
37
|
+
// Otherwise, we want to show it, cause the user may forget to call `AppRegistry.registerComponent`.
|
|
38
|
+
if (wasHit && error.message?.includes('has not been registered. This can happen if')) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
customizeError(error);
|
|
36
42
|
}
|
|
43
|
+
wasHit = true;
|
|
37
44
|
originalHandler(error, isFatal);
|
|
38
45
|
}
|
|
39
46
|
export function createErrorHandler(originalHandler) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevLauncherErrorManager.js","sourceRoot":"","sources":["../src/DevLauncherErrorManager.ts"],"names":[],"mappings":"AAGA,IAAI,sBAAsB,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"DevLauncherErrorManager.js","sourceRoot":"","sources":["../src/DevLauncherErrorManager.ts"],"names":[],"mappings":"AAGA,IAAI,sBAAsB,GAAG,IAAI,CAAC;AAClC,IAAI,MAAM,GAAG,KAAK,CAAC,CAAC,gDAAgD;AAEpE,MAAM,iCAAiC,GAAG;;;;mGAIyD,CAAC;AAEpG,MAAM,gCAAgC,GAAG;;;mGAG0D,CAAC;AAEpG,SAAS,2BAA2B,CAAC,KAAiB;IACpD,KAAK,CAAC,OAAO,IAAI,MAAM,GAAG,iCAAiC,CAAC;AAC9D,CAAC;AAED,SAAS,+BAA+B,CAAC,KAAY;IACnD,KAAK,CAAC,OAAO,GAAG;;EAEhB,gCAAgC,EAAE,CAAC;AACrC,CAAC;AAED,SAAS,cAAc,CAAC,KAAyB;IAC/C,IAAI,MAAM,IAAI,KAAK,EAAE;QACnB,sCAAsC;QACtC,QAAQ,KAAK,CAAC,IAAI,EAAE;YAClB,KAAK,iBAAiB,CAAC,CAAC;gBACtB,2BAA2B,CAAC,KAAK,CAAC,CAAC;gBACnC,MAAM;aACP;SACF;KACF;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE;QACjE,+BAA+B,CAAC,KAAK,CAAC,CAAC;KACxC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,eAAe,EAAE,KAAK,EAAE,OAAO;IACnD,IAAI,KAAK,YAAY,KAAK,EAAE;QAC1B,gGAAgG;QAChG,oGAAoG;QACpG,IAAI,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,6CAA6C,CAAC,EAAE;YACpF,OAAO;SACR;QACD,cAAc,CAAC,KAAK,CAAC,CAAC;KACvB;IAED,MAAM,GAAG,IAAI,CAAC;IACd,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,eAAe;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,IAAI,sBAAsB,EAAE;YAC1B,YAAY,CAAC,eAAe,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO;SACR;QAED,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,sBAAsB,GAAG,KAAK,CAAC;AACjC,CAAC","sourcesContent":["// Similar interface to the one used in expo modules.\ntype CodedError = Error & { code?: string };\n\nlet isErrorHandlingEnabled = true;\nlet wasHit = false; // whether the original error handler was called\n\nconst unavailableErrorPossibleSolutions = `Some possible solutions:\n- Make sure that the method is available on the current platform.\n- Make sure you're using the newest available version of this development client.\n- Make sure you're running a compatible version of your JavaScript code.\n- If you've installed a new library recently, you may need to make a new development client build.`;\n\nconst moduleIsMissingPossibleSolutions = `Some possible solutions:\n- Make sure you're using the newest available version of this development client.\n- Make sure you're running a compatible version of your JavaScript code.\n- If you've installed a new library recently, you may need to make a new development client build.`;\n\nfunction customizeUnavailableMessage(error: CodedError) {\n error.message += '\\n\\n' + unavailableErrorPossibleSolutions;\n}\n\nfunction customizeModuleIsMissingMessage(error: Error) {\n error.message = `Your JavaScript code tried to access a native module that doesn't exist in this development client. \n\n${moduleIsMissingPossibleSolutions}`;\n}\n\nfunction customizeError(error: Error | CodedError) {\n if ('code' in error) {\n // It's a CodedError from expo modules\n switch (error.code) {\n case 'ERR_UNAVAILABLE': {\n customizeUnavailableMessage(error);\n break;\n }\n }\n } else if (error.message.includes('Native module cannot be null')) {\n customizeModuleIsMissingMessage(error);\n }\n}\n\nfunction errorHandler(originalHandler, error, isFatal) {\n if (error instanceof Error) {\n // Suppresses `\"main\" has not been registered` error only if it was caused by a different error.\n // Otherwise, we want to show it, cause the user may forget to call `AppRegistry.registerComponent`.\n if (wasHit && error.message?.includes('has not been registered. This can happen if')) {\n return;\n }\n customizeError(error);\n }\n\n wasHit = true;\n originalHandler(error, isFatal);\n}\n\nexport function createErrorHandler(originalHandler) {\n return (error, isFatal) => {\n if (isErrorHandlingEnabled) {\n errorHandler(originalHandler, error, isFatal);\n return;\n }\n\n originalHandler(error, isFatal);\n };\n}\n\nexport function disableErrorHandling() {\n isErrorHandlingEnabled = false;\n}\n"]}
|
|
@@ -13,6 +13,7 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.platform = :ios, '11.0'
|
|
14
14
|
s.swift_version = '5.2'
|
|
15
15
|
s.source = { :git => 'https://github.com/github_account/expo-development-client.git', :tag => "#{s.version}" }
|
|
16
|
+
s.static_framework = true
|
|
16
17
|
s.source_files = 'ios/**/*.{h,m,swift,cpp}'
|
|
17
18
|
s.preserve_paths = 'ios/**/*.{h,m,swift}'
|
|
18
19
|
s.exclude_files = ['ios/Unsafe/**/*.{h,m,mm,swift,cpp}', 'ios/Tests/**/*.{h,m,swift}']
|
|
@@ -37,6 +38,7 @@ Pod::Spec.new do |s|
|
|
|
37
38
|
|
|
38
39
|
s.dependency "React-Core"
|
|
39
40
|
s.dependency "expo-dev-menu-interface"
|
|
41
|
+
s.dependency "EXManifests"
|
|
40
42
|
s.dependency "EXUpdatesInterface"
|
|
41
43
|
|
|
42
44
|
s.subspec 'Unsafe' do |unsafe|
|
package/ios/EXDevLauncher.m
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
#import "EXDevLauncher.h"
|
|
4
4
|
#import "EXDevLauncherController.h"
|
|
5
5
|
|
|
6
|
+
#if __has_include(<EXDevLauncher/EXDevLauncher-Swift.h>)
|
|
7
|
+
// For cocoapods framework, the generated swift header will be inside EXDevLauncher module
|
|
8
|
+
#import <EXDevLauncher/EXDevLauncher-Swift.h>
|
|
9
|
+
#else
|
|
6
10
|
#import <EXDevLauncher-Swift.h>
|
|
11
|
+
#endif
|
|
7
12
|
|
|
8
13
|
@implementation EXDevLauncher
|
|
9
14
|
|
|
@@ -16,10 +21,11 @@ RCT_EXPORT_MODULE()
|
|
|
16
21
|
|
|
17
22
|
- (NSDictionary *)constantsToExport
|
|
18
23
|
{
|
|
19
|
-
|
|
24
|
+
NSDictionary *rawManifestJSON = [EXDevLauncherController.sharedInstance appManifest].rawManifestJSON;
|
|
25
|
+
NSData *manifestStringData = rawManifestJSON ? [NSJSONSerialization dataWithJSONObject:rawManifestJSON options:kNilOptions error:NULL] : nil;
|
|
20
26
|
NSString *manifestURLString = [EXDevLauncherController.sharedInstance appManifestURL].absoluteString;
|
|
21
27
|
return @{
|
|
22
|
-
@"manifestString":
|
|
28
|
+
@"manifestString": manifestStringData ? [[NSString alloc] initWithData:manifestStringData encoding:NSUTF8StringEncoding] : [NSNull null],
|
|
23
29
|
@"manifestURL": manifestURLString ?: [NSNull null]
|
|
24
30
|
};
|
|
25
31
|
}
|