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