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