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.proguard.annotations.DoNotStrip
20 import com.facebook.react.ReactInstanceManager
21 import com.facebook.react.ReactRootView
22 import com.facebook.react.bridge.Arguments
23 import com.facebook.react.bridge.JavaScriptContextHolder
24 import com.facebook.react.bridge.ReactApplicationContext
25 import com.facebook.react.bridge.ReadableMap
26 import com.facebook.react.common.LifecycleState
27 import com.facebook.react.modules.network.ReactCookieJarContainer
28 import com.facebook.react.shell.MainReactPackage
29 import com.facebook.soloader.SoLoader
30 import de.greenrobot.event.EventBus
31 import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromOpenIntent
32 import expo.modules.notifications.service.delegates.ExpoHandlingDelegate
33 import expo.modules.manifests.core.Manifest
34 import host.exp.exponent.*
35 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback
36 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus
37 import host.exp.exponent.analytics.EXL
38 import host.exp.exponent.di.NativeModuleDepsProvider
39 import host.exp.exponent.exceptions.ExceptionUtils
40 import host.exp.exponent.experience.BaseExperienceActivity
41 import host.exp.exponent.experience.ErrorActivity
42 import host.exp.exponent.experience.ExperienceActivity
43 import host.exp.exponent.experience.HomeActivity
44 import host.exp.exponent.headless.InternalHeadlessAppLoader
45 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage
46 import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback
47 import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent
48 import host.exp.exponent.kernel.ExponentUrls.toHttp
49 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions
50 import host.exp.exponent.network.ExponentNetwork
51 import host.exp.exponent.notifications.ExponentNotification
52 import host.exp.exponent.notifications.ExponentNotificationManager
53 import host.exp.exponent.notifications.NotificationActionCenter
54 import host.exp.exponent.notifications.ScopedNotificationsUtils
55 import host.exp.exponent.storage.ExponentDB
56 import host.exp.exponent.storage.ExponentSharedPreferences
57 import host.exp.exponent.utils.AsyncCondition
58 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
59 import host.exp.exponent.utils.BundleJSONConverter
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? = run {
652       for (i in tasks.indices) {
653         val task = tasks[i]
654         // When deep linking from `NotificationForwarderActivity`, the task will finish immediately.
655         // There is race condition to retrieve the taskInfo from the finishing task.
656         // Uses try-catch to handle the cases.
657         try {
658           val baseIntent = task.taskInfo.baseIntent
659           if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && (
660             baseIntent.getStringExtra(
661                 KernelConstants.MANIFEST_URL_KEY
662               ) == manifestUrl
663             )
664           ) {
665             return@run task
666           }
667         } catch (e: Exception) {}
668       }
669       return@run null
670     }
671 
672     if (isOptimistic && existingTask == null) {
673       openOptimisticExperienceActivity(manifestUrl)
674     }
675     if (existingTask != null) {
676       try {
677         moveTaskToFront(existingTask.taskInfo.id)
678       } catch (e: IllegalArgumentException) {
679         // Sometimes task can't be found.
680         existingTask = null
681         openOptimisticExperienceActivity(manifestUrl)
682       }
683     }
684     val finalExistingTask = existingTask
685     if (existingTask == null) {
686       ExpoUpdatesAppLoader(
687         manifestUrl,
688         object : AppLoaderCallback {
689           override fun onOptimisticManifest(optimisticManifest: Manifest) {
690             Exponent.instance
691               .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) }
692           }
693 
694           override fun onManifestCompleted(manifest: Manifest) {
695             Exponent.instance.runOnUiThread {
696               try {
697                 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask)
698               } catch (e: JSONException) {
699                 handleError(e)
700               }
701             }
702           }
703 
704           override fun onBundleCompleted(localBundlePath: String) {
705             Exponent.instance.runOnUiThread { sendBundleToExperienceActivity(localBundlePath) }
706           }
707 
708           override fun emitEvent(params: JSONObject) {
709             val task = manifestUrlToExperienceActivityTask[manifestUrl]
710             if (task != null) {
711               val experienceActivity = task.experienceActivity!!.get()
712               experienceActivity?.emitUpdatesEvent(params)
713             }
714           }
715 
716           override fun updateStatus(status: AppLoaderStatus) {
717             if (optimisticActivity != null) {
718               optimisticActivity!!.setLoadingProgressStatusIfEnabled(status)
719             }
720           }
721 
722           override fun onError(e: Exception) {
723             Exponent.instance.runOnUiThread { handleError(e) }
724           }
725         },
726         forceCache
727       ).start(context)
728     }
729   }
730 
731   @Throws(JSONException::class)
732   private fun openManifestUrlStep2(
733     manifestUrl: String,
734     manifest: Manifest,
735     existingTask: AppTask?
736   ) {
737     val bundleUrl = toHttp(manifest.getBundleURL())
738     val task = getExperienceActivityTask(manifestUrl)
739     task.bundleUrl = bundleUrl
740     ExponentManifest.normalizeManifestInPlace(manifest, manifestUrl)
741     if (existingTask == null) {
742       sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl)
743     }
744     val params = Arguments.createMap().apply {
745       putString("manifestUrl", manifestUrl)
746       putString("manifestString", manifest.toString())
747     }
748     queueEvent(
749       "ExponentKernel.addHistoryItem", params,
750       object : KernelEventCallback {
751         override fun onEventSuccess(result: ReadableMap) {
752           EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.")
753         }
754 
755         override fun onEventFailure(errorMessage: String?) {
756           EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage")
757         }
758       }
759     )
760     killOrphanedLauncherActivities()
761   }
762 
763   /*
764    *
765    * Optimistic experiences
766    *
767    */
768   private fun openOptimisticExperienceActivity(manifestUrl: String?) {
769     try {
770       val intent = Intent(activityContext, ExperienceActivity::class.java).apply {
771         addIntentDocumentFlags(this)
772         putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl)
773         putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true)
774       }
775       activityContext!!.startActivity(intent)
776     } catch (e: Throwable) {
777       EXL.e(TAG, e)
778     }
779   }
780 
781   fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) {
782     optimisticActivity = experienceActivity
783     optimisticTaskId = taskId
784     AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY)
785     AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY)
786   }
787 
788   fun sendOptimisticManifestToExperienceActivity(optimisticManifest: Manifest) {
789     AsyncCondition.wait(
790       KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY,
791       object : AsyncConditionListener {
792         override fun isReady(): Boolean {
793           return optimisticActivity != null && optimisticTaskId != null
794         }
795 
796         override fun execute() {
797           optimisticActivity!!.setOptimisticManifest(optimisticManifest)
798         }
799       }
800     )
801   }
802 
803   private fun sendManifestToExperienceActivity(
804     manifestUrl: String,
805     manifest: Manifest,
806     bundleUrl: String,
807   ) {
808     AsyncCondition.wait(
809       KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY,
810       object : AsyncConditionListener {
811         override fun isReady(): Boolean {
812           return optimisticActivity != null && optimisticTaskId != null
813         }
814 
815         override fun execute() {
816           optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl)
817           AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY)
818         }
819       }
820     )
821   }
822 
823   private fun sendBundleToExperienceActivity(localBundlePath: String) {
824     AsyncCondition.wait(
825       KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY,
826       object : AsyncConditionListener {
827         override fun isReady(): Boolean {
828           return optimisticActivity != null && optimisticTaskId != null
829         }
830 
831         override fun execute() {
832           optimisticActivity!!.setBundle(localBundlePath)
833           optimisticActivity = null
834           optimisticTaskId = null
835         }
836       }
837     )
838   }
839 
840   /*
841    *
842    * Tasks
843    *
844    */
845   val tasks: List<AppTask>
846     get() {
847       val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
848       return manager.appTasks
849     }
850 
851   // Get list of tasks in our format.
852   val experienceActivityTasks: List<AppTask>
853     get() = tasks
854 
855   // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly.
856   // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem.
857   // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing
858   // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind
859   // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out
860   // the root cause.
861   private fun killOrphanedLauncherActivities() {
862     try {
863       // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities
864       RecentTaskInfo::class.java.getDeclaredField("numActivities")
865       for (task: AppTask in tasks) {
866         val taskInfo = task.taskInfo
867         if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) {
868           task.finishAndRemoveTask()
869           return
870         }
871         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
872           if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) {
873             task.finishAndRemoveTask()
874             return
875           }
876         }
877       }
878     } catch (e: NoSuchFieldException) {
879       // Don't EXL here because this isn't actually a problem
880       Log.e(TAG, e.toString())
881     } catch (e: Throwable) {
882       EXL.e(TAG, e)
883     }
884   }
885 
886   fun moveTaskToFront(taskId: Int) {
887     tasks.find { it.taskInfo.id == taskId }?.also { task ->
888       // If we have the task in memory, tell the ExperienceActivity to check for new options.
889       // Otherwise options will be added in initialProps when the Experience starts.
890       val exponentTask = experienceActivityTaskForTaskId(taskId)
891       if (exponentTask != null) {
892         val experienceActivity = exponentTask.experienceActivity!!.get()
893         experienceActivity?.shouldCheckOptions()
894       }
895       task.moveToFront()
896     }
897   }
898 
899   fun killActivityStack(activity: Activity) {
900     val exponentTask = experienceActivityTaskForTaskId(activity.taskId)
901     if (exponentTask != null) {
902       removeExperienceActivityTask(exponentTask.manifestUrl)
903     }
904 
905     // Kill the current task.
906     val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
907     manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() }
908   }
909 
910   override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean {
911     var activity: ExperienceActivity? = null
912     for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) {
913       if (manifestUrl == experienceActivityTask.manifestUrl) {
914         val weakActivity =
915           if (experienceActivityTask.experienceActivity == null) {
916             null
917           } else {
918             experienceActivityTask.experienceActivity!!.get()
919           }
920         activity = weakActivity
921         if (weakActivity == null) {
922           // No activity, just force a reload
923           break
924         }
925         Exponent.instance.runOnUiThread { weakActivity.startLoading() }
926         break
927       }
928     }
929     activity?.let { killActivityStack(it) }
930     openManifestUrl(manifestUrl, null, true, forceCache)
931     return true
932   }
933 
934   override fun handleError(errorMessage: String) {
935     handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true)
936   }
937 
938   override fun handleError(exception: Exception) {
939     handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true)
940   }
941 
942   // TODO: probably need to call this from other places.
943   fun setHasError() {
944     hasError = true
945   }
946 
947   companion object {
948     private val TAG = Kernel::class.java.simpleName
949     private lateinit var instance: Kernel
950 
951     // Activities/Tasks
952     private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>()
953     private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>()
954     private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>()
955 
956     private fun addIntentDocumentFlags(intent: Intent) = intent.apply {
957       addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
958       addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
959       addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
960     }
961 
962     @JvmStatic
963     @DoNotStrip
964     fun reloadVisibleExperience(activityId: Int) {
965       val manifestUrl = getManifestUrlForActivityId(activityId)
966       if (manifestUrl != null) {
967         instance.reloadVisibleExperience(manifestUrl, false)
968       }
969     }
970 
971     // Called from DevServerHelper via ReactNativeStaticHelpers
972     @JvmStatic
973     @DoNotStrip
974     fun getManifestUrlForActivityId(activityId: Int): String? {
975       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl
976     }
977 
978     // Called from DevServerHelper via ReactNativeStaticHelpers
979     @JvmStatic
980     @DoNotStrip
981     fun getBundleUrlForActivityId(
982       activityId: Int,
983       host: String,
984       mainModuleId: String?,
985       bundleTypeId: String?,
986       devMode: Boolean,
987       jsMinify: Boolean
988     ): String? {
989       // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private
990       // BundleType enum for the possible values) but may need to
991       if (activityId == -1) {
992         // This is the kernel
993         return instance.bundleUrl
994       }
995       if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) {
996         return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId)
997       }
998       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
999     }
1000 
1001     // <= SDK 25
1002     @DoNotStrip
1003     fun getBundleUrlForActivityId(
1004       activityId: Int,
1005       host: String,
1006       jsModulePath: String?,
1007       devMode: Boolean,
1008       jsMinify: Boolean
1009     ): String? {
1010       if (activityId == -1) {
1011         // This is the kernel
1012         return instance.bundleUrl
1013       }
1014       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
1015     }
1016 
1017     // <= SDK 21
1018     @DoNotStrip
1019     fun getBundleUrlForActivityId(
1020       activityId: Int,
1021       host: String,
1022       jsModulePath: String?,
1023       devMode: Boolean,
1024       hmr: Boolean,
1025       jsMinify: Boolean
1026     ): String? {
1027       if (activityId == -1) {
1028         // This is the kernel
1029         return instance.bundleUrl
1030       }
1031       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task ->
1032         var url = task.bundleUrl ?: return null
1033         if (hmr) {
1034           url = if (url.contains("hot=false")) {
1035             url.replace("hot=false", "hot=true")
1036           } else {
1037             "$url&hot=true"
1038           }
1039         }
1040         return url
1041       }
1042     }
1043 
1044     /*
1045    *
1046    * Error handling
1047    *
1048    */
1049     // Called using reflection from ReactAndroid.
1050     @DoNotStrip
1051     fun handleReactNativeError(
1052       errorMessage: String?,
1053       detailsUnversioned: Any?,
1054       exceptionId: Int?,
1055       isFatal: Boolean
1056     ) {
1057       handleReactNativeError(
1058         developerErrorMessage(errorMessage),
1059         detailsUnversioned,
1060         exceptionId,
1061         isFatal
1062       )
1063     }
1064 
1065     // Called using reflection from ReactAndroid.
1066     @DoNotStrip
1067     fun handleReactNativeError(
1068       throwable: Throwable?,
1069       errorMessage: String?,
1070       detailsUnversioned: Any?,
1071       exceptionId: Int?,
1072       isFatal: Boolean
1073     ) {
1074       handleReactNativeError(
1075         developerErrorMessage(errorMessage),
1076         detailsUnversioned,
1077         exceptionId,
1078         isFatal
1079       )
1080     }
1081 
1082     private fun handleReactNativeError(
1083       errorMessage: ExponentErrorMessage,
1084       detailsUnversioned: Any?,
1085       exceptionId: Int?,
1086       isFatal: Boolean
1087     ) {
1088       val stackList = ArrayList<Bundle>()
1089       if (detailsUnversioned != null) {
1090         val details = RNObject.wrap(detailsUnversioned)
1091         val arguments = RNObject("com.facebook.react.bridge.Arguments")
1092         arguments.loadVersion(details.version())
1093         for (i in 0 until details.call("size") as Int) {
1094           try {
1095             val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle
1096             stackList.add(bundle)
1097           } catch (e: Exception) {
1098             e.printStackTrace()
1099           }
1100         }
1101       } else if (BuildConfig.DEBUG) {
1102         val stackTraceElements = Thread.currentThread().stackTrace
1103         // stackTraceElements starts with a bunch of stuff we don't care about.
1104         for (i in 2 until stackTraceElements.size) {
1105           val element = stackTraceElements[i]
1106           if ((
1107             (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) &&
1108               ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError"))
1109             )
1110           ) {
1111             // Ignore these base error handling methods.
1112             continue
1113           }
1114           val bundle = Bundle().apply {
1115             putInt("column", 0)
1116             putInt("lineNumber", element.lineNumber)
1117             putString("methodName", element.methodName)
1118             putString("file", element.fileName)
1119           }
1120           stackList.add(bundle)
1121         }
1122       }
1123       val stack = stackList.toTypedArray()
1124       BaseExperienceActivity.addError(
1125         ExponentError(
1126           errorMessage, stack,
1127           getExceptionId(exceptionId), isFatal
1128         )
1129       )
1130     }
1131 
1132     private fun getExceptionId(originalId: Int?): Int {
1133       return if (originalId == null || originalId == -1) {
1134         (-(Math.random() * Int.MAX_VALUE)).toInt()
1135       } else originalId
1136     }
1137   }
1138 
1139   init {
1140     NativeModuleDepsProvider.instance.inject(Kernel::class.java, this)
1141     instance = this
1142     updateKernelRNOkHttp()
1143   }
1144 }
1145