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