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