<lambda>null1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.kernel
3
4 import android.app.Activity
5 import android.app.ActivityManager
6 import android.app.ActivityManager.AppTask
7 import android.app.ActivityManager.RecentTaskInfo
8 import android.app.Application
9 import android.app.RemoteInput
10 import android.content.Context
11 import android.content.Intent
12 import android.net.Uri
13 import android.nfc.NfcAdapter
14 import android.os.Bundle
15 import android.util.Log
16 import android.widget.Toast
17 import com.facebook.hermes.reactexecutor.HermesExecutorFactory
18 import com.facebook.proguard.annotations.DoNotStrip
19 import com.facebook.react.ReactInstanceManager
20 import com.facebook.react.ReactRootView
21 import com.facebook.react.bridge.Arguments
22 import com.facebook.react.bridge.JavaScriptExecutorFactory
23 import com.facebook.react.bridge.ReadableMap
24 import com.facebook.react.common.LifecycleState
25 import com.facebook.react.jscexecutor.JSCExecutorFactory
26 import com.facebook.react.modules.network.ReactCookieJarContainer
27 import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
28 import com.facebook.react.shell.MainReactPackage
29 import com.facebook.soloader.SoLoader
30 import de.greenrobot.event.EventBus
31 import expo.modules.jsonutils.require
32 import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromOpenIntent
33 import expo.modules.notifications.service.delegates.ExpoHandlingDelegate
34 import expo.modules.manifests.core.Manifest
35 import expo.modules.manifests.core.NewManifest
36 import host.exp.exponent.*
37 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback
38 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus
39 import host.exp.exponent.analytics.EXL
40 import host.exp.exponent.di.NativeModuleDepsProvider
41 import host.exp.exponent.exceptions.ExceptionUtils
42 import host.exp.exponent.experience.BaseExperienceActivity
43 import host.exp.exponent.experience.ErrorActivity
44 import host.exp.exponent.experience.ExperienceActivity
45 import host.exp.exponent.experience.HomeActivity
46 import host.exp.exponent.headless.InternalHeadlessAppLoader
47 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage
48 import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback
49 import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent
50 import host.exp.exponent.kernel.ExponentUrls.toHttp
51 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions
52 import host.exp.exponent.network.ExponentNetwork
53 import host.exp.exponent.notifications.ExponentNotification
54 import host.exp.exponent.notifications.ExponentNotificationManager
55 import host.exp.exponent.notifications.NotificationActionCenter
56 import host.exp.exponent.notifications.ScopedNotificationsUtils
57 import host.exp.exponent.storage.ExponentDB
58 import host.exp.exponent.storage.ExponentSharedPreferences
59 import host.exp.exponent.utils.AsyncCondition
60 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
61 import host.exp.exponent.utils.BundleJSONConverter
62 import host.exp.expoview.BuildConfig
63 import host.exp.expoview.ExpoViewBuildConfig
64 import host.exp.expoview.Exponent
65 import host.exp.expoview.Exponent.BundleListener
66 import okhttp3.OkHttpClient
67 import org.json.JSONException
68 import org.json.JSONObject
69 import versioned.host.exp.exponent.ExpoTurboPackage
70 import versioned.host.exp.exponent.ExponentPackage
71 import versioned.host.exp.exponent.ReactUnthemedRootView
72 import java.lang.ref.WeakReference
73 import java.util.*
74 import java.util.concurrent.TimeUnit
75 import javax.inject.Inject
76
77 // TOOD: need to figure out when we should reload the kernel js. Do we do it every time you visit
78 // the home screen? only when the app gets kicked out of memory?
79 class Kernel : KernelInterface() {
80 class KernelStartedRunningEvent
81
82 class ExperienceActivityTask(val manifestUrl: String) {
83 var taskId = 0
84 var experienceActivity: WeakReference<ExperienceActivity>? = null
85 var activityId = 0
86 var bundleUrl: String? = null
87 }
88
89 // React
90 var reactInstanceManager: ReactInstanceManager? = null
91 private set
92
93 // Contexts
94 @Inject
95 lateinit var context: Context
96
97 @Inject
98 lateinit var applicationContext: Application
99
100 @Inject
101 lateinit var exponentManifest: ExponentManifest
102
103 @Inject
104 lateinit var exponentSharedPreferences: ExponentSharedPreferences
105
106 @Inject
107 lateinit var exponentNetwork: ExponentNetwork
108
109 var activityContext: Activity? = null
110 set(value) {
111 if (value != null) {
112 field = value
113 }
114 }
115
116 private var optimisticActivity: ExperienceActivity? = null
117
118 private var optimisticTaskId: Int? = null
119
120 private fun experienceActivityTaskForTaskId(taskId: Int): ExperienceActivityTask? {
121 return manifestUrlToExperienceActivityTask.values.find { it.taskId == taskId }
122 }
123
124 // Misc
125 var isStarted = false
126 private set
127 private var hasError = false
128
129 private fun updateKernelRNOkHttp() {
130 val client = OkHttpClient.Builder()
131 .connectTimeout(0, TimeUnit.MILLISECONDS)
132 .readTimeout(0, TimeUnit.MILLISECONDS)
133 .writeTimeout(0, TimeUnit.MILLISECONDS)
134 .cookieJar(ReactCookieJarContainer())
135 .cache(exponentNetwork.cache)
136
137 if (BuildConfig.DEBUG) {
138 // FIXME: 8/9/17
139 // broke with lib versioning
140 // clientBuilder.addNetworkInterceptor(new StethoInterceptor());
141 }
142 ReactNativeStaticHelpers.setExponentNetwork(exponentNetwork)
143 }
144
145 private val kernelInitialURL: String?
146 get() {
147 val activity = activityContext ?: return null
148 val intent = activity.intent ?: return null
149 val action = intent.action
150 val uri = intent.data
151 return if ((
152 uri != null &&
153 ((Intent.ACTION_VIEW == action) || (NfcAdapter.ACTION_NDEF_DISCOVERED == action))
154 )
155 ) {
156 uri.toString()
157 } else null
158 }
159
160 // Don't call this until a loading screen is up, since it has to do some work on the main thread.
161 fun startJSKernel(activity: Activity?) {
162 if (Constants.isStandaloneApp()) {
163 return
164 }
165 activityContext = activity
166 SoLoader.init(context, false)
167 synchronized(this) {
168 if (isStarted && !hasError) {
169 return
170 }
171 isStarted = true
172 }
173 hasError = false
174 if (!exponentSharedPreferences.shouldUseEmbeddedKernel()) {
175 try {
176 // Make sure we can get the manifest successfully. This can fail in dev mode
177 // if the kernel packager is not running.
178 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest
179 } catch (e: Throwable) {
180 Exponent.instance
181 .runOnUiThread { // Hack to make this show up for a while. Can't use an Alert because LauncherActivity has a transparent theme. This should only be seen by internal developers.
182 var i = 0
183 while (i < 3) {
184 Toast.makeText(
185 activityContext,
186 "Kernel manifest invalid. Make sure `expo start` is running inside of exponent/home and rebuild the app.",
187 Toast.LENGTH_LONG
188 ).show()
189 i++
190 }
191 }
192 return
193 }
194 }
195
196 // On first run use the embedded kernel js but fire off a request for the new js in the background.
197 val bundleUrlToLoad =
198 bundleUrl + (if (ExpoViewBuildConfig.DEBUG) "" else "?versionName=" + ExpoViewKernel.instance.versionName)
199 if (exponentSharedPreferences.shouldUseEmbeddedKernel()) {
200 kernelBundleListener().onBundleLoaded(Constants.EMBEDDED_KERNEL_PATH)
201 } else {
202 var shouldNotUseKernelCache =
203 exponentSharedPreferences.getBoolean(ExponentSharedPreferences.ExponentSharedPreferencesKey.SHOULD_NOT_USE_KERNEL_CACHE)
204 if (!ExpoViewBuildConfig.DEBUG) {
205 val oldKernelRevisionId =
206 exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.KERNEL_REVISION_ID, "")
207 if (oldKernelRevisionId != kernelRevisionId) {
208 shouldNotUseKernelCache = true
209 }
210 }
211 Exponent.instance.loadJSBundle(
212 null,
213 bundleUrlToLoad,
214 bundleAssetRequestHeaders,
215 KernelConstants.KERNEL_BUNDLE_ID,
216 RNObject.UNVERSIONED,
217 kernelBundleListener(),
218 shouldNotUseKernelCache
219 )
220 }
221 }
222
223 private fun kernelBundleListener(): BundleListener {
224 return object : BundleListener {
225 override fun onBundleLoaded(localBundlePath: String) {
226 if (!ExpoViewBuildConfig.DEBUG) {
227 exponentSharedPreferences.setString(
228 ExponentSharedPreferences.ExponentSharedPreferencesKey.KERNEL_REVISION_ID,
229 kernelRevisionId
230 )
231 }
232 Exponent.instance.runOnUiThread {
233 val initialURL = kernelInitialURL
234 val builder = ReactInstanceManager.builder()
235 .setApplication(applicationContext)
236 .setCurrentActivity(activityContext)
237 .setJSBundleFile(localBundlePath)
238 .setJavaScriptExecutorFactory(jsExecutorFactory)
239 .addPackage(MainReactPackage())
240 .addPackage(
241 ExponentPackage.kernelExponentPackage(
242 context,
243 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest,
244 HomeActivity.homeExpoPackages(),
245 HomeActivity.Companion,
246 initialURL
247 )
248 )
249 .addPackage(
250 ExpoTurboPackage.kernelExpoTurboPackage(
251 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest, initialURL
252 )
253 )
254 .setInitialLifecycleState(LifecycleState.RESUMED)
255 if (!KernelConfig.FORCE_NO_KERNEL_DEBUG_MODE && exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.isDevelopmentMode()) {
256 Exponent.enableDeveloperSupport(
257 kernelDebuggerHost, kernelMainModuleName,
258 RNObject.wrap(builder)
259 )
260 }
261 reactInstanceManager = builder.build()
262 reactInstanceManager!!.createReactContextInBackground()
263 reactInstanceManager!!.onHostResume(activityContext, null)
264 isRunning = true
265 EventBus.getDefault().postSticky(KernelStartedRunningEvent())
266 EXL.d(TAG, "Kernel started running.")
267
268 // Reset this flag if we crashed
269 exponentSharedPreferences.setBoolean(
270 ExponentSharedPreferences.ExponentSharedPreferencesKey.SHOULD_NOT_USE_KERNEL_CACHE,
271 false
272 )
273 }
274 }
275
276 override fun onError(e: Exception) {
277 setHasError()
278 if (ExpoViewBuildConfig.DEBUG) {
279 handleError("Can't load kernel. Are you sure your packager is running and your phone is on the same wifi? " + e.message)
280 } else {
281 handleError("Expo requires an internet connection.")
282 EXL.d(TAG, "Expo requires an internet connection." + e.message)
283 }
284 }
285 }
286 }
287
288 private val kernelDebuggerHost: String
289 get() = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getDebuggerHost()
290 private val kernelMainModuleName: String
291 get() = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getMainModuleName()
292 private val bundleUrl: String?
293 get() {
294 return try {
295 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getBundleURL()
296 } catch (e: JSONException) {
297 KernelProvider.instance.handleError(e)
298 null
299 }
300 }
301 private val bundleAssetRequestHeaders: JSONObject
302 get() {
303 return try {
304 val manifestAndAssetRequestHeaders = exponentManifest.getKernelManifestAndAssetRequestHeaders()
305 val manifest = manifestAndAssetRequestHeaders.manifest
306 if (manifest is NewManifest) {
307 val bundleKey = manifest.getLaunchAsset().getString("key")
308 val map: Map<String, JSONObject> = manifestAndAssetRequestHeaders.assetRequestHeaders.let { it.keys().asSequence().associateWith { key -> it.require(key) } } ?: mapOf()
309 map[bundleKey] ?: JSONObject()
310 } else {
311 JSONObject()
312 }
313 } catch (e: JSONException) {
314 KernelProvider.instance.handleError(e)
315 JSONObject()
316 }
317 }
318 private val kernelRevisionId: String?
319 get() {
320 return try {
321 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getRevisionId()
322 } catch (e: JSONException) {
323 KernelProvider.instance.handleError(e)
324 null
325 }
326 }
327 var isRunning: Boolean = false
328 get() = field && !hasError
329 private set
330
331 val reactRootView: ReactRootView
332 get() {
333 val reactRootView: ReactRootView = ReactUnthemedRootView(activityContext)
334 reactRootView.startReactApplication(
335 reactInstanceManager,
336 KernelConstants.HOME_MODULE_NAME,
337 kernelLaunchOptions
338 )
339 return reactRootView
340 }
341 private val kernelLaunchOptions: Bundle
342 get() {
343 val exponentProps = JSONObject()
344 val referrer = exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.REFERRER_KEY)
345 if (referrer != null) {
346 try {
347 exponentProps.put("referrer", referrer)
348 } catch (e: JSONException) {
349 EXL.e(TAG, e)
350 }
351 }
352 val bundle = Bundle()
353 try {
354 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps))
355 } catch (e: JSONException) {
356 throw Error("JSONObject failed to be converted to Bundle", e)
357 }
358 return bundle
359 }
360 private val jsExecutorFactory: JavaScriptExecutorFactory
361 get() {
362 val manifest = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest
363 val appName = manifest.getName() ?: ""
364 val deviceName = AndroidInfoHelpers.getFriendlyDeviceName()
365
366 val jsEngineFromManifest = manifest.jsEngine
367 return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory(
368 appName,
369 deviceName
370 )
371 }
372
373 fun hasOptionsForManifestUrl(manifestUrl: String?): Boolean {
374 return manifestUrlToOptions.containsKey(manifestUrl)
375 }
376
377 fun popOptionsForManifestUrl(manifestUrl: String?): ExperienceOptions? {
378 return manifestUrlToOptions.remove(manifestUrl)
379 }
380
381 fun addAppLoaderForManifestUrl(manifestUrl: String, appLoader: ExpoUpdatesAppLoader) {
382 manifestUrlToAppLoader[manifestUrl] = appLoader
383 }
384
385 override fun getAppLoaderForManifestUrl(manifestUrl: String?): ExpoUpdatesAppLoader? {
386 return manifestUrlToAppLoader[manifestUrl]
387 }
388
389 fun getExperienceActivityTask(manifestUrl: String): ExperienceActivityTask {
390 var task = manifestUrlToExperienceActivityTask[manifestUrl]
391 if (task != null) {
392 return task
393 }
394 task = ExperienceActivityTask(manifestUrl)
395 manifestUrlToExperienceActivityTask[manifestUrl] = task
396 return task
397 }
398
399 fun removeExperienceActivityTask(manifestUrl: String?) {
400 if (manifestUrl != null) {
401 manifestUrlToExperienceActivityTask.remove(manifestUrl)
402 }
403 }
404
405 fun openHomeActivity() {
406 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
407 for (task: AppTask in manager.appTasks) {
408 val baseIntent = task.taskInfo.baseIntent
409 if ((HomeActivity::class.java.name == baseIntent.component!!.className)) {
410 task.moveToFront()
411 return
412 }
413 }
414 val intent = Intent(activityContext, HomeActivity::class.java)
415 addIntentDocumentFlags(intent)
416 activityContext!!.startActivity(intent)
417 }
418
419 private fun openShellAppActivity(forceCache: Boolean) {
420 try {
421 val activityClass = Class.forName("host.exp.exponent.MainActivity")
422 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
423 for (task: AppTask in manager.appTasks) {
424 val baseIntent = task.taskInfo.baseIntent
425 if ((activityClass.name == baseIntent.component!!.className)) {
426 moveTaskToFront(task.taskInfo.id)
427 return
428 }
429 }
430 val intent = Intent(activityContext, activityClass)
431 addIntentDocumentFlags(intent)
432 if (forceCache) {
433 intent.putExtra(KernelConstants.LOAD_FROM_CACHE_KEY, true)
434 }
435 activityContext!!.startActivity(intent)
436 } catch (e: ClassNotFoundException) {
437 throw IllegalStateException("Could not find activity to open (MainActivity is not present).")
438 }
439 }
440
441 /*
442 *
443 * Manifests
444 *
445 */
446 fun handleIntent(activity: Activity, intent: Intent) {
447 try {
448 if (intent.getBooleanExtra("EXKernelDisableNuxDefaultsKey", false)) {
449 Constants.DISABLE_NUX = true
450 }
451 } catch (e: Throwable) {
452 }
453 activityContext = activity
454 if (intent.action != null && (ExpoHandlingDelegate.OPEN_APP_INTENT_ACTION == intent.action)) {
455 if (!openExperienceFromNotificationIntent(intent)) {
456 openDefaultUrl()
457 }
458 return
459 }
460 val bundle = intent.extras
461 val uri = intent.data
462 val intentUri = uri?.toString()
463 if (bundle != null) {
464 // Notification
465 val notification = bundle.getString(KernelConstants.NOTIFICATION_KEY) // deprecated
466 val notificationObject = bundle.getString(KernelConstants.NOTIFICATION_OBJECT_KEY)
467 val notificationManifestUrl = bundle.getString(KernelConstants.NOTIFICATION_MANIFEST_URL_KEY)
468 if (notificationManifestUrl != null) {
469 val exponentNotification = ExponentNotification.fromJSONObjectString(notificationObject)
470 if (exponentNotification != null) {
471 // Add action type
472 if (bundle.containsKey(KernelConstants.NOTIFICATION_ACTION_TYPE_KEY)) {
473 exponentNotification.actionType = bundle.getString(KernelConstants.NOTIFICATION_ACTION_TYPE_KEY)
474 val manager = ExponentNotificationManager(context)
475 val experienceKey = ExperienceKey(exponentNotification.experienceScopeKey)
476 manager.cancel(experienceKey, exponentNotification.notificationId)
477 }
478 // Add remote input
479 val remoteInput = RemoteInput.getResultsFromIntent(intent)
480 if (remoteInput != null) {
481 exponentNotification.inputText = remoteInput.getString(NotificationActionCenter.KEY_TEXT_REPLY)
482 }
483 }
484 openExperience(
485 ExperienceOptions(
486 notificationManifestUrl,
487 intentUri ?: notificationManifestUrl,
488 notification,
489 exponentNotification
490 )
491 )
492 return
493 }
494
495 // Shortcut
496 // TODO: Remove once we decide to stop supporting shortcuts to experiences.
497 val shortcutManifestUrl = bundle.getString(KernelConstants.SHORTCUT_MANIFEST_URL_KEY)
498 if (shortcutManifestUrl != null) {
499 openExperience(ExperienceOptions(shortcutManifestUrl, intentUri, null))
500 return
501 }
502 }
503 if (uri != null && shouldOpenUrl(uri)) {
504 if (Constants.INITIAL_URL == null) {
505 // We got an "exp://", "exps://", "http://", or "https://" app link
506 openExperience(ExperienceOptions(uri.toString(), uri.toString(), null))
507 return
508 } else {
509 // We got a custom scheme link
510 // TODO: we still might want to parse this if we're running a different experience inside a
511 // shell app. For example, we are running Brighten in the List shell and go to Twitter login.
512 // We might want to set the return uri to thelistapp://exp.host/@brighten/brighten+deeplink
513 // But we also can't break thelistapp:// deep links that look like thelistapp://l/listid
514 openExperience(ExperienceOptions(Constants.INITIAL_URL, uri.toString(), null))
515 return
516 }
517 }
518 openDefaultUrl()
519 }
520
521 // Certain links (i.e. 'expo.io/expo-go') should just open the HomeScreen
522 private fun shouldOpenUrl(uri: Uri): Boolean {
523 val host = uri.host ?: ""
524 val path = uri.path ?: ""
525 return !(((host == "expo.io") || (host == "expo.dev")) && (path == "/expo-go"))
526 }
527
528 private fun openExperienceFromNotificationIntent(intent: Intent): Boolean {
529 val response = getNotificationResponseFromOpenIntent(intent)
530 val experienceScopeKey = ScopedNotificationsUtils.getExperienceScopeKey(response) ?: return false
531 val exponentDBObject = try {
532 val exponentDBObjectInner = ExponentDB.experienceScopeKeyToExperienceSync(experienceScopeKey)
533 if (exponentDBObjectInner == null) {
534 Log.w("expo-notifications", "Couldn't find experience from scopeKey: $experienceScopeKey")
535 }
536 exponentDBObjectInner
537 } catch (e: JSONException) {
538 Log.w("expo-notifications", "Couldn't deserialize experience from scopeKey: $experienceScopeKey")
539 null
540 } ?: return false
541
542 val manifestUrl = exponentDBObject.manifestUrl
543 openExperience(ExperienceOptions(manifestUrl, manifestUrl, null))
544 return true
545 }
546
547 private fun openDefaultUrl() {
548 val defaultUrl =
549 if (Constants.INITIAL_URL == null) KernelConstants.HOME_MANIFEST_URL else Constants.INITIAL_URL
550 openExperience(ExperienceOptions(defaultUrl, defaultUrl, null))
551 }
552
553 override fun openExperience(options: ExperienceOptions) {
554 openManifestUrl(getManifestUrlFromFullUri(options.manifestUri), options, true)
555 }
556
557 private fun getManifestUrlFromFullUri(uriString: String?): String? {
558 if (uriString == null) {
559 return null
560 }
561
562 val uri = Uri.parse(uriString)
563 val builder = uri.buildUpon()
564 val deepLinkPositionDashes =
565 uriString.indexOf(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH)
566 if (deepLinkPositionDashes >= 0) {
567 // do this safely so we preserve any query string
568 val pathSegments = uri.pathSegments
569 builder.path(null)
570 for (segment: String in pathSegments) {
571 if ((ExponentManifest.DEEP_LINK_SEPARATOR == segment)) {
572 break
573 }
574 builder.appendEncodedPath(segment)
575 }
576 }
577
578 // transfer the release-channel param to the built URL as this will cause Expo Go to treat
579 // this as a different project
580 var releaseChannel = uri.getQueryParameter(ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL)
581 builder.query(null)
582 if (releaseChannel != null) {
583 // release channels cannot contain the ' ' character, so if this is present,
584 // it must be an encoded form of '+' which indicated a deep link in SDK <27.
585 // therefore, nothing after this is part of the release channel name so we should strip it.
586 // TODO: remove this check once SDK 26 and below are no longer supported
587 val releaseChannelDeepLinkPosition = releaseChannel.indexOf(' ')
588 if (releaseChannelDeepLinkPosition > -1) {
589 releaseChannel = releaseChannel.substring(0, releaseChannelDeepLinkPosition)
590 }
591 builder.appendQueryParameter(
592 ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL,
593 releaseChannel
594 )
595 }
596
597 // transfer the expo-updates query params: runtime-version, channel-name
598 val expoUpdatesQueryParameters = listOf(
599 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION,
600 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME
601 )
602 for (queryParameter: String in expoUpdatesQueryParameters) {
603 val queryParameterValue = uri.getQueryParameter(queryParameter)
604 if (queryParameterValue != null) {
605 builder.appendQueryParameter(queryParameter, queryParameterValue)
606 }
607 }
608
609 // ignore fragments as well (e.g. those added by auth-session)
610 builder.fragment(null)
611 var newUriString = builder.build().toString()
612 val deepLinkPositionPlus = newUriString.indexOf('+')
613 if (deepLinkPositionPlus >= 0 && deepLinkPositionDashes < 0) {
614 // need to keep this for backwards compatibility
615 newUriString = newUriString.substring(0, deepLinkPositionPlus)
616 }
617
618 // manifest url doesn't have a trailing slash
619 if (newUriString.isNotEmpty()) {
620 val lastUrlChar = newUriString[newUriString.length - 1]
621 if (lastUrlChar == '/') {
622 newUriString = newUriString.substring(0, newUriString.length - 1)
623 }
624 }
625 return newUriString
626 }
627
628 private fun openManifestUrl(
629 manifestUrl: String?,
630 options: ExperienceOptions?,
631 isOptimistic: Boolean,
632 forceCache: Boolean = false
633 ) {
634 SoLoader.init(context, false)
635 if (options == null) {
636 manifestUrlToOptions.remove(manifestUrl)
637 } else {
638 manifestUrlToOptions[manifestUrl] = options
639 }
640 if (manifestUrl == null || (manifestUrl == KernelConstants.HOME_MANIFEST_URL)) {
641 openHomeActivity()
642 return
643 }
644 if (Constants.isStandaloneApp()) {
645 openShellAppActivity(forceCache)
646 return
647 }
648 ErrorActivity.clearErrorList()
649 val tasks: List<AppTask> = experienceActivityTasks
650 var existingTask: AppTask? = run {
651 for (i in tasks.indices) {
652 val task = tasks[i]
653 // When deep linking from `NotificationForwarderActivity`, the task will finish immediately.
654 // There is race condition to retrieve the taskInfo from the finishing task.
655 // Uses try-catch to handle the cases.
656 try {
657 val baseIntent = task.taskInfo.baseIntent
658 if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && (
659 baseIntent.getStringExtra(
660 KernelConstants.MANIFEST_URL_KEY
661 ) == manifestUrl
662 )
663 ) {
664 return@run task
665 }
666 } catch (e: Exception) {}
667 }
668 return@run null
669 }
670
671 if (isOptimistic && existingTask == null) {
672 openOptimisticExperienceActivity(manifestUrl)
673 }
674 if (existingTask != null) {
675 try {
676 moveTaskToFront(existingTask.taskInfo.id)
677 } catch (e: IllegalArgumentException) {
678 // Sometimes task can't be found.
679 existingTask = null
680 openOptimisticExperienceActivity(manifestUrl)
681 }
682 }
683 val finalExistingTask = existingTask
684 if (existingTask == null) {
685 ExpoUpdatesAppLoader(
686 manifestUrl,
687 object : AppLoaderCallback {
688 override fun onOptimisticManifest(optimisticManifest: Manifest) {
689 Exponent.instance
690 .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) }
691 }
692
693 override fun onManifestCompleted(manifest: Manifest) {
694 Exponent.instance.runOnUiThread {
695 try {
696 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask)
697 } catch (e: JSONException) {
698 handleError(e)
699 }
700 }
701 }
702
703 override fun onBundleCompleted(localBundlePath: String) {
704 Exponent.instance.runOnUiThread { sendBundleToExperienceActivity(localBundlePath) }
705 }
706
707 override fun emitEvent(params: JSONObject) {
708 val task = manifestUrlToExperienceActivityTask[manifestUrl]
709 if (task != null) {
710 val experienceActivity = task.experienceActivity!!.get()
711 experienceActivity?.emitUpdatesEvent(params)
712 }
713 }
714
715 override fun updateStatus(status: AppLoaderStatus) {
716 if (optimisticActivity != null) {
717 optimisticActivity!!.setLoadingProgressStatusIfEnabled(status)
718 }
719 }
720
721 override fun onError(e: Exception) {
722 Exponent.instance.runOnUiThread { handleError(e) }
723 }
724 },
725 forceCache
726 ).start(context)
727 }
728 }
729
730 @Throws(JSONException::class)
731 private fun openManifestUrlStep2(
732 manifestUrl: String,
733 manifest: Manifest,
734 existingTask: AppTask?
735 ) {
736 val bundleUrl = toHttp(manifest.getBundleURL())
737 val task = getExperienceActivityTask(manifestUrl)
738 task.bundleUrl = bundleUrl
739 ExponentManifest.normalizeManifestInPlace(manifest, manifestUrl)
740 if (existingTask == null) {
741 sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl)
742 }
743 val params = Arguments.createMap().apply {
744 putString("manifestUrl", manifestUrl)
745 putString("manifestString", manifest.toString())
746 }
747 queueEvent(
748 "ExponentKernel.addHistoryItem", params,
749 object : KernelEventCallback {
750 override fun onEventSuccess(result: ReadableMap) {
751 EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.")
752 }
753
754 override fun onEventFailure(errorMessage: String?) {
755 EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage")
756 }
757 }
758 )
759 killOrphanedLauncherActivities()
760 }
761
762 /*
763 *
764 * Optimistic experiences
765 *
766 */
767 private fun openOptimisticExperienceActivity(manifestUrl: String?) {
768 try {
769 val intent = Intent(activityContext, ExperienceActivity::class.java).apply {
770 addIntentDocumentFlags(this)
771 putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl)
772 putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true)
773 }
774 activityContext!!.startActivity(intent)
775 } catch (e: Throwable) {
776 EXL.e(TAG, e)
777 }
778 }
779
780 fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) {
781 optimisticActivity = experienceActivity
782 optimisticTaskId = taskId
783 AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY)
784 AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY)
785 }
786
787 fun sendOptimisticManifestToExperienceActivity(optimisticManifest: Manifest) {
788 AsyncCondition.wait(
789 KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY,
790 object : AsyncConditionListener {
791 override fun isReady(): Boolean {
792 return optimisticActivity != null && optimisticTaskId != null
793 }
794
795 override fun execute() {
796 optimisticActivity!!.setOptimisticManifest(optimisticManifest)
797 }
798 }
799 )
800 }
801
802 private fun sendManifestToExperienceActivity(
803 manifestUrl: String,
804 manifest: Manifest,
805 bundleUrl: String,
806 ) {
807 AsyncCondition.wait(
808 KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY,
809 object : AsyncConditionListener {
810 override fun isReady(): Boolean {
811 return optimisticActivity != null && optimisticTaskId != null
812 }
813
814 override fun execute() {
815 optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl)
816 AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY)
817 }
818 }
819 )
820 }
821
822 private fun sendBundleToExperienceActivity(localBundlePath: String) {
823 AsyncCondition.wait(
824 KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY,
825 object : AsyncConditionListener {
826 override fun isReady(): Boolean {
827 return optimisticActivity != null && optimisticTaskId != null
828 }
829
830 override fun execute() {
831 optimisticActivity!!.setBundle(localBundlePath)
832 optimisticActivity = null
833 optimisticTaskId = null
834 }
835 }
836 )
837 }
838
839 /*
840 *
841 * Tasks
842 *
843 */
844 val tasks: List<AppTask>
845 get() {
846 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
847 return manager.appTasks
848 }
849
850 // Get list of tasks in our format.
851 val experienceActivityTasks: List<AppTask>
852 get() = tasks
853
854 // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly.
855 // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem.
856 // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing
857 // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind
858 // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out
859 // the root cause.
860 private fun killOrphanedLauncherActivities() {
861 try {
862 // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities
863 RecentTaskInfo::class.java.getDeclaredField("numActivities")
864 for (task: AppTask in tasks) {
865 val taskInfo = task.taskInfo
866 if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) {
867 task.finishAndRemoveTask()
868 return
869 }
870 if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) {
871 task.finishAndRemoveTask()
872 return
873 }
874 }
875 } catch (e: NoSuchFieldException) {
876 // Don't EXL here because this isn't actually a problem
877 Log.e(TAG, e.toString())
878 } catch (e: Throwable) {
879 EXL.e(TAG, e)
880 }
881 }
882
883 fun moveTaskToFront(taskId: Int) {
884 tasks.find { it.taskInfo.id == taskId }?.also { task ->
885 // If we have the task in memory, tell the ExperienceActivity to check for new options.
886 // Otherwise options will be added in initialProps when the Experience starts.
887 val exponentTask = experienceActivityTaskForTaskId(taskId)
888 if (exponentTask != null) {
889 val experienceActivity = exponentTask.experienceActivity!!.get()
890 experienceActivity?.shouldCheckOptions()
891 }
892 task.moveToFront()
893 }
894 }
895
896 fun killActivityStack(activity: Activity) {
897 val exponentTask = experienceActivityTaskForTaskId(activity.taskId)
898 if (exponentTask != null) {
899 removeExperienceActivityTask(exponentTask.manifestUrl)
900 }
901
902 // Kill the current task.
903 val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
904 manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() }
905 }
906
907 override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean {
908 var activity: ExperienceActivity? = null
909 for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) {
910 if (manifestUrl == experienceActivityTask.manifestUrl) {
911 val weakActivity =
912 if (experienceActivityTask.experienceActivity == null) {
913 null
914 } else {
915 experienceActivityTask.experienceActivity!!.get()
916 }
917 activity = weakActivity
918 if (weakActivity == null) {
919 // No activity, just force a reload
920 break
921 }
922 Exponent.instance.runOnUiThread { weakActivity.startLoading() }
923 break
924 }
925 }
926 activity?.let { killActivityStack(it) }
927 openManifestUrl(manifestUrl, null, true, forceCache)
928 return true
929 }
930
931 override fun handleError(errorMessage: String) {
932 handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true)
933 }
934
935 override fun handleError(exception: Exception) {
936 handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true, ExceptionUtils.exceptionToErrorHeader(exception))
937 }
938
939 // TODO: probably need to call this from other places.
940 fun setHasError() {
941 hasError = true
942 }
943
944 companion object {
945 private val TAG = Kernel::class.java.simpleName
946 private lateinit var instance: Kernel
947
948 // Activities/Tasks
949 private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>()
950 private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>()
951 private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>()
952
953 private fun addIntentDocumentFlags(intent: Intent) = intent.apply {
954 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
955 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
956 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
957 }
958
959 @JvmStatic
960 @DoNotStrip
961 fun reloadVisibleExperience(activityId: Int) {
962 val manifestUrl = getManifestUrlForActivityId(activityId)
963 if (manifestUrl != null) {
964 instance.reloadVisibleExperience(manifestUrl, false)
965 }
966 }
967
968 // Called from DevServerHelper via ReactNativeStaticHelpers
969 @JvmStatic
970 @DoNotStrip
971 fun getManifestUrlForActivityId(activityId: Int): String? {
972 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl
973 }
974
975 // Called from DevServerHelper via ReactNativeStaticHelpers
976 @JvmStatic
977 @DoNotStrip
978 fun getBundleUrlForActivityId(
979 activityId: Int,
980 host: String,
981 mainModuleId: String?,
982 bundleTypeId: String?,
983 devMode: Boolean,
984 jsMinify: Boolean
985 ): String? {
986 // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private
987 // BundleType enum for the possible values) but may need to
988 if (activityId == -1) {
989 // This is the kernel
990 return instance.bundleUrl
991 }
992 if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) {
993 return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId)
994 }
995 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
996 }
997
998 // <= SDK 25
999 @DoNotStrip
1000 fun getBundleUrlForActivityId(
1001 activityId: Int,
1002 host: String,
1003 jsModulePath: String?,
1004 devMode: Boolean,
1005 jsMinify: Boolean
1006 ): String? {
1007 if (activityId == -1) {
1008 // This is the kernel
1009 return instance.bundleUrl
1010 }
1011 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
1012 }
1013
1014 // <= SDK 21
1015 @DoNotStrip
1016 fun getBundleUrlForActivityId(
1017 activityId: Int,
1018 host: String,
1019 jsModulePath: String?,
1020 devMode: Boolean,
1021 hmr: Boolean,
1022 jsMinify: Boolean
1023 ): String? {
1024 if (activityId == -1) {
1025 // This is the kernel
1026 return instance.bundleUrl
1027 }
1028 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task ->
1029 var url = task.bundleUrl ?: return null
1030 if (hmr) {
1031 url = if (url.contains("hot=false")) {
1032 url.replace("hot=false", "hot=true")
1033 } else {
1034 "$url&hot=true"
1035 }
1036 }
1037 return url
1038 }
1039 }
1040
1041 /*
1042 *
1043 * Error handling
1044 *
1045 */
1046 // Called using reflection from ReactAndroid.
1047 @DoNotStrip
1048 fun handleReactNativeError(
1049 errorMessage: String?,
1050 detailsUnversioned: Any?,
1051 exceptionId: Int?,
1052 isFatal: Boolean
1053 ) {
1054 handleReactNativeError(
1055 developerErrorMessage(errorMessage),
1056 detailsUnversioned,
1057 exceptionId,
1058 isFatal
1059 )
1060 }
1061
1062 // Called using reflection from ReactAndroid.
1063 @DoNotStrip
1064 fun handleReactNativeError(
1065 throwable: Throwable?,
1066 errorMessage: String?,
1067 detailsUnversioned: Any?,
1068 exceptionId: Int?,
1069 isFatal: Boolean
1070 ) {
1071 handleReactNativeError(
1072 developerErrorMessage(errorMessage),
1073 detailsUnversioned,
1074 exceptionId,
1075 isFatal
1076 )
1077 }
1078
1079 private fun handleReactNativeError(
1080 errorMessage: ExponentErrorMessage,
1081 detailsUnversioned: Any?,
1082 exceptionId: Int?,
1083 isFatal: Boolean,
1084 errorHeader: String? = null,
1085 ) {
1086 val stackList = ArrayList<Bundle>()
1087 if (detailsUnversioned != null) {
1088 val details = RNObject.wrap(detailsUnversioned)
1089 val arguments = RNObject("com.facebook.react.bridge.Arguments")
1090 arguments.loadVersion(details.version())
1091 for (i in 0 until details.call("size") as Int) {
1092 try {
1093 val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle
1094 stackList.add(bundle)
1095 } catch (e: Exception) {
1096 e.printStackTrace()
1097 }
1098 }
1099 } else if (BuildConfig.DEBUG) {
1100 val stackTraceElements = Thread.currentThread().stackTrace
1101 // stackTraceElements starts with a bunch of stuff we don't care about.
1102 for (i in 2 until stackTraceElements.size) {
1103 val element = stackTraceElements[i]
1104 if ((
1105 (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) &&
1106 ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError"))
1107 )
1108 ) {
1109 // Ignore these base error handling methods.
1110 continue
1111 }
1112 val bundle = Bundle().apply {
1113 putInt("column", 0)
1114 putInt("lineNumber", element.lineNumber)
1115 putString("methodName", element.methodName)
1116 putString("file", element.fileName)
1117 }
1118 stackList.add(bundle)
1119 }
1120 }
1121 val stack = stackList.toTypedArray()
1122 BaseExperienceActivity.addError(
1123 ExponentError(
1124 errorMessage, errorHeader, stack,
1125 getExceptionId(exceptionId), isFatal
1126 )
1127 )
1128 }
1129
1130 private fun getExceptionId(originalId: Int?): Int {
1131 return if (originalId == null || originalId == -1) {
1132 (-(Math.random() * Int.MAX_VALUE)).toInt()
1133 } else originalId
1134 }
1135 }
1136
1137 init {
1138 NativeModuleDepsProvider.instance.inject(Kernel::class.java, this)
1139 instance = this
1140 updateKernelRNOkHttp()
1141 }
1142 }
1143