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