1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.kernel
3 
4 import android.app.Activity
5 import android.app.ActivityManager
6 import android.app.ActivityManager.AppTask
7 import android.app.ActivityManager.RecentTaskInfo
8 import android.app.Application
9 import android.app.RemoteInput
10 import android.content.Context
11 import android.content.Intent
12 import android.net.Uri
13 import android.nfc.NfcAdapter
14 import android.os.Build
15 import android.os.Bundle
16 import android.os.Handler
17 import android.util.Log
18 import android.widget.Toast
19 import com.facebook.internal.BundleJSONConverter
20 import com.facebook.proguard.annotations.DoNotStrip
21 import com.facebook.react.ReactInstanceManager
22 import com.facebook.react.ReactRootView
23 import com.facebook.react.bridge.Arguments
24 import com.facebook.react.bridge.JavaScriptContextHolder
25 import com.facebook.react.bridge.ReactApplicationContext
26 import com.facebook.react.bridge.ReadableMap
27 import com.facebook.react.common.LifecycleState
28 import com.facebook.react.modules.network.ReactCookieJarContainer
29 import com.facebook.react.shell.MainReactPackage
30 import com.facebook.soloader.SoLoader
31 import de.greenrobot.event.EventBus
32 import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromIntent
33 import expo.modules.notifications.service.delegates.ExpoHandlingDelegate
34 import expo.modules.updates.manifest.raw.RawManifest
35 import host.exp.exponent.*
36 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback
37 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus
38 import host.exp.exponent.analytics.EXL
39 import host.exp.exponent.di.NativeModuleDepsProvider
40 import host.exp.exponent.exceptions.ExceptionUtils
41 import host.exp.exponent.experience.BaseExperienceActivity
42 import host.exp.exponent.experience.ErrorActivity
43 import host.exp.exponent.experience.ExperienceActivity
44 import host.exp.exponent.experience.HomeActivity
45 import host.exp.exponent.headless.InternalHeadlessAppLoader
46 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage
47 import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback
48 import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent
49 import host.exp.exponent.kernel.ExponentUrls.toHttp
50 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions
51 import host.exp.exponent.network.ExponentNetwork
52 import host.exp.exponent.notifications.ExponentNotification
53 import host.exp.exponent.notifications.ExponentNotificationManager
54 import host.exp.exponent.notifications.NotificationActionCenter
55 import host.exp.exponent.notifications.ScopedNotificationsUtils
56 import host.exp.exponent.storage.ExponentDB
57 import host.exp.exponent.storage.ExponentSharedPreferences
58 import host.exp.exponent.utils.AsyncCondition
59 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
60 import host.exp.expoview.BuildConfig
61 import host.exp.expoview.ExpoViewBuildConfig
62 import host.exp.expoview.Exponent
63 import host.exp.expoview.Exponent.BundleListener
64 import okhttp3.OkHttpClient
65 import org.json.JSONException
66 import org.json.JSONObject
67 import versioned.host.exp.exponent.ExpoTurboPackage
68 import versioned.host.exp.exponent.ExponentPackage
69 import versioned.host.exp.exponent.ReactUnthemedRootView
70 import versioned.host.exp.exponent.modules.api.reanimated.ReanimatedJSIModulePackage
71 import java.lang.ref.WeakReference
72 import java.util.*
73 import java.util.concurrent.TimeUnit
74 import javax.inject.Inject
75 
76 // TOOD: need to figure out when we should reload the kernel js. Do we do it every time you visit
77 // the home screen? only when the app gets kicked out of memory?
78 class Kernel : KernelInterface() {
79   class KernelStartedRunningEvent
80 
81   class ExperienceActivityTask(val manifestUrl: String) {
82     var taskId = 0
83     var experienceActivity: WeakReference<ExperienceActivity>? = null
84     var activityId = 0
85     var bundleUrl: String? = null
86   }
87 
88   // React
89   var reactInstanceManager: ReactInstanceManager? = null
90     private set
91 
92   // Contexts
93   @Inject
94   lateinit var context: Context
95 
96   @Inject
97   lateinit var applicationContext: Application
98 
99   @Inject
100   lateinit var exponentManifest: ExponentManifest
101 
102   @Inject
103   lateinit var exponentSharedPreferences: ExponentSharedPreferences
104 
105   @Inject
106   lateinit var exponentNetwork: ExponentNetwork
107 
108   var activityContext: Activity? = null
109     set(value) {
110       if (value != null) {
111         field = value
112       }
113     }
114 
115   private var optimisticActivity: ExperienceActivity? = null
116 
117   private var optimisticTaskId: Int? = null
118 
119   private fun experienceActivityTaskForTaskId(taskId: Int): ExperienceActivityTask? {
120     return manifestUrlToExperienceActivityTask.values.find { it.taskId == taskId }
121   }
122 
123   // Misc
124   var isStarted = false
125     private set
126   private var hasError = false
127 
128   private fun updateKernelRNOkHttp() {
129     val client = OkHttpClient.Builder()
130       .connectTimeout(0, TimeUnit.MILLISECONDS)
131       .readTimeout(0, TimeUnit.MILLISECONDS)
132       .writeTimeout(0, TimeUnit.MILLISECONDS)
133       .cookieJar(ReactCookieJarContainer())
134       .cache(exponentNetwork.cache)
135 
136     if (BuildConfig.DEBUG) {
137       // FIXME: 8/9/17
138       // broke with lib versioning
139       // clientBuilder.addNetworkInterceptor(new StethoInterceptor());
140     }
141     ReactNativeStaticHelpers.setExponentNetwork(exponentNetwork)
142   }
143 
144   private val kernelInitialURL: String?
145     get() {
146       val activity = activityContext ?: return null
147       val intent = activity.intent ?: return null
148       val action = intent.action
149       val uri = intent.data
150       return if ((
151         uri != null &&
152           ((Intent.ACTION_VIEW == action) || (NfcAdapter.ACTION_NDEF_DISCOVERED == action))
153         )
154       ) {
155         uri.toString()
156       } else null
157     }
158 
159   // Don't call this until a loading screen is up, since it has to do some work on the main thread.
160   fun startJSKernel(activity: Activity?) {
161     if (Constants.isStandaloneApp()) {
162       return
163     }
164     activityContext = activity
165     SoLoader.init(context, false)
166     synchronized(this) {
167       if (isStarted && !hasError) {
168         return
169       }
170       isStarted = true
171     }
172     hasError = false
173     if (!exponentSharedPreferences.shouldUseInternetKernel()) {
174       try {
175         // Make sure we can get the manifest successfully. This can fail in dev mode
176         // if the kernel packager is not running.
177         exponentManifest.getKernelManifest()
178       } catch (e: Throwable) {
179         Exponent.getInstance()
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.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.getInstance().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.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.SHOULD_NOT_USE_KERNEL_CACHE)
231       if (!ExpoViewBuildConfig.DEBUG) {
232         val oldKernelRevisionId =
233           exponentSharedPreferences.getString(ExponentSharedPreferences.KERNEL_REVISION_ID, "")
234         if (oldKernelRevisionId != kernelRevisionId) {
235           shouldNotUseKernelCache = true
236         }
237       }
238       Exponent.getInstance().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.KERNEL_REVISION_ID,
255             kernelRevisionId
256           )
257         }
258         Exponent.getInstance().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               "UNVERSIONED", kernelDebuggerHost,
288               kernelMainModuleName, 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.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.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.setActionType(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.setInputText(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 = getNotificationResponseFromIntent(intent)
531     val experienceScopeKey = ScopedNotificationsUtils.getExperienceScopeKey(response) ?: return false
532     val exponentDBObject = try {
533       val exponentDBObjectInner = ExponentDB.experienceScopeKeyToExperienceSync(experienceScopeKey)
534       if (exponentDBObjectInner == null) {
535         Log.w("expo-notifications", "Couldn't find experience from scopeKey: $experienceScopeKey")
536       }
537       exponentDBObjectInner
538     } catch (e: JSONException) {
539       Log.w("expo-notifications", "Couldn't deserialize experience from scopeKey: $experienceScopeKey")
540       null
541     } ?: return false
542 
543     val manifestUrl = exponentDBObject.manifestUrl
544     openExperience(ExperienceOptions(manifestUrl, manifestUrl, null))
545     return true
546   }
547 
548   private fun openDefaultUrl() {
549     val defaultUrl =
550       if (Constants.INITIAL_URL == null) KernelConstants.HOME_MANIFEST_URL else Constants.INITIAL_URL
551     openExperience(ExperienceOptions(defaultUrl, defaultUrl, null))
552   }
553 
554   override fun openExperience(options: ExperienceOptions) {
555     openManifestUrl(getManifestUrlFromFullUri(options.manifestUri), options, true)
556   }
557 
558   private fun getManifestUrlFromFullUri(uriString: String?): String? {
559     if (uriString == null) {
560       return null
561     }
562 
563     val uri = Uri.parse(uriString)
564     val builder = uri.buildUpon()
565     val deepLinkPositionDashes =
566       uriString.indexOf(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH)
567     if (deepLinkPositionDashes >= 0) {
568       // do this safely so we preserve any query string
569       val pathSegments = uri.pathSegments
570       builder.path(null)
571       for (segment: String in pathSegments) {
572         if ((ExponentManifest.DEEP_LINK_SEPARATOR == segment)) {
573           break
574         }
575         builder.appendEncodedPath(segment)
576       }
577     }
578 
579     // transfer the release-channel param to the built URL as this will cause Expo Go to treat
580     // this as a different project
581     var releaseChannel = uri.getQueryParameter(ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL)
582     builder.query(null)
583     if (releaseChannel != null) {
584       // release channels cannot contain the ' ' character, so if this is present,
585       // it must be an encoded form of '+' which indicated a deep link in SDK <27.
586       // therefore, nothing after this is part of the release channel name so we should strip it.
587       // TODO: remove this check once SDK 26 and below are no longer supported
588       val releaseChannelDeepLinkPosition = releaseChannel.indexOf(' ')
589       if (releaseChannelDeepLinkPosition > -1) {
590         releaseChannel = releaseChannel.substring(0, releaseChannelDeepLinkPosition)
591       }
592       builder.appendQueryParameter(
593         ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL,
594         releaseChannel
595       )
596     }
597 
598     // transfer the expo-updates query params: runtime-version, channel-name
599     val expoUpdatesQueryParameters = listOf(
600       ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION,
601       ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME
602     )
603     for (queryParameter: String in expoUpdatesQueryParameters) {
604       val queryParameterValue = uri.getQueryParameter(queryParameter)
605       if (queryParameterValue != null) {
606         builder.appendQueryParameter(queryParameter, queryParameterValue)
607       }
608     }
609 
610     // ignore fragments as well (e.g. those added by auth-session)
611     builder.fragment(null)
612     var newUriString = builder.build().toString()
613     val deepLinkPositionPlus = newUriString.indexOf('+')
614     if (deepLinkPositionPlus >= 0 && deepLinkPositionDashes < 0) {
615       // need to keep this for backwards compatibility
616       newUriString = newUriString.substring(0, deepLinkPositionPlus)
617     }
618 
619     // manifest url doesn't have a trailing slash
620     if (newUriString.isNotEmpty()) {
621       val lastUrlChar = newUriString[newUriString.length - 1]
622       if (lastUrlChar == '/') {
623         newUriString = newUriString.substring(0, newUriString.length - 1)
624       }
625     }
626     return newUriString
627   }
628 
629   private fun openManifestUrl(
630     manifestUrl: String?,
631     options: ExperienceOptions?,
632     isOptimistic: Boolean,
633     forceCache: Boolean = false
634   ) {
635     SoLoader.init(context, false)
636     if (options == null) {
637       manifestUrlToOptions.remove(manifestUrl)
638     } else {
639       manifestUrlToOptions[manifestUrl] = options
640     }
641     if (manifestUrl == null || (manifestUrl == KernelConstants.HOME_MANIFEST_URL)) {
642       openHomeActivity()
643       return
644     }
645     if (Constants.isStandaloneApp()) {
646       openShellAppActivity(forceCache)
647       return
648     }
649     ErrorActivity.clearErrorList()
650     val tasks: List<AppTask> = experienceActivityTasks
651     var existingTask: AppTask? = null
652     for (i in tasks.indices) {
653       val task = tasks[i]
654       val baseIntent = task.taskInfo.baseIntent
655       if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && (
656         baseIntent.getStringExtra(
657             KernelConstants.MANIFEST_URL_KEY
658           ) == manifestUrl
659         )
660       ) {
661         existingTask = task
662         break
663       }
664     }
665     if (isOptimistic && existingTask == null) {
666       openOptimisticExperienceActivity(manifestUrl)
667     }
668     if (existingTask != null) {
669       try {
670         moveTaskToFront(existingTask.taskInfo.id)
671       } catch (e: IllegalArgumentException) {
672         // Sometimes task can't be found.
673         existingTask = null
674         openOptimisticExperienceActivity(manifestUrl)
675       }
676     }
677     val finalExistingTask = existingTask
678     if (existingTask == null) {
679       ExpoUpdatesAppLoader(
680         manifestUrl,
681         object : AppLoaderCallback {
682           override fun onOptimisticManifest(optimisticManifest: RawManifest) {
683             Exponent.getInstance()
684               .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) }
685           }
686 
687           override fun onManifestCompleted(manifest: RawManifest) {
688             Exponent.getInstance().runOnUiThread {
689               try {
690                 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask)
691               } catch (e: JSONException) {
692                 handleError(e)
693               }
694             }
695           }
696 
697           override fun onBundleCompleted(localBundlePath: String?) {
698             Exponent.getInstance().runOnUiThread { sendBundleToExperienceActivity(localBundlePath) }
699           }
700 
701           override fun emitEvent(params: JSONObject) {
702             val task = manifestUrlToExperienceActivityTask[manifestUrl]
703             if (task != null) {
704               val experienceActivity = task.experienceActivity!!.get()
705               experienceActivity?.emitUpdatesEvent(params)
706             }
707           }
708 
709           override fun updateStatus(status: AppLoaderStatus) {
710             if (optimisticActivity != null) {
711               optimisticActivity!!.setLoadingProgressStatusIfEnabled(status)
712             }
713           }
714 
715           override fun onError(e: Exception) {
716             Exponent.getInstance().runOnUiThread { handleError(e) }
717           }
718         },
719         forceCache
720       ).start(context)
721     }
722   }
723 
724   @Throws(JSONException::class)
725   private fun openManifestUrlStep2(
726     manifestUrl: String,
727     manifest: RawManifest,
728     existingTask: AppTask?
729   ) {
730     val bundleUrl = toHttp(manifest.getBundleURL())
731     val task = getExperienceActivityTask(manifestUrl)
732     task.bundleUrl = bundleUrl
733     ExponentManifest.normalizeRawManifestInPlace(manifest, manifestUrl)
734     val opts = JSONObject()
735     if (existingTask == null) {
736       sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl, opts)
737     }
738     val params = Arguments.createMap().apply {
739       putString("manifestUrl", manifestUrl)
740       putString("manifestString", manifest.toString())
741     }
742     queueEvent(
743       "ExponentKernel.addHistoryItem", params,
744       object : KernelEventCallback {
745         override fun onEventSuccess(result: ReadableMap) {
746           EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.")
747         }
748 
749         override fun onEventFailure(errorMessage: String?) {
750           EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage")
751         }
752       }
753     )
754     killOrphanedLauncherActivities()
755   }
756 
757   /*
758    *
759    * Optimistic experiences
760    *
761    */
762   private fun openOptimisticExperienceActivity(manifestUrl: String?) {
763     try {
764       val intent = Intent(activityContext, ExperienceActivity::class.java).apply {
765         addIntentDocumentFlags(this)
766         putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl)
767         putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true)
768       }
769       activityContext!!.startActivity(intent)
770     } catch (e: Throwable) {
771       EXL.e(TAG, e)
772     }
773   }
774 
775   fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) {
776     optimisticActivity = experienceActivity
777     optimisticTaskId = taskId
778     AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY)
779     AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY)
780   }
781 
782   fun sendOptimisticManifestToExperienceActivity(optimisticManifest: RawManifest?) {
783     AsyncCondition.wait(
784       KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY,
785       object : AsyncConditionListener {
786         override fun isReady(): Boolean {
787           return optimisticActivity != null && optimisticTaskId != null
788         }
789 
790         override fun execute() {
791           optimisticActivity!!.setOptimisticManifest(optimisticManifest)
792         }
793       }
794     )
795   }
796 
797   private fun sendManifestToExperienceActivity(
798     manifestUrl: String?,
799     manifest: RawManifest?,
800     bundleUrl: String?,
801     kernelOptions: JSONObject?
802   ) {
803     AsyncCondition.wait(
804       KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY,
805       object : AsyncConditionListener {
806         override fun isReady(): Boolean {
807           return optimisticActivity != null && optimisticTaskId != null
808         }
809 
810         override fun execute() {
811           optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl, kernelOptions)
812           AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY)
813         }
814       }
815     )
816   }
817 
818   fun sendBundleToExperienceActivity(localBundlePath: String?) {
819     AsyncCondition.wait(
820       KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY,
821       object : AsyncConditionListener {
822         override fun isReady(): Boolean {
823           return optimisticActivity != null && optimisticTaskId != null
824         }
825 
826         override fun execute() {
827           optimisticActivity!!.setBundle(localBundlePath)
828           optimisticActivity = null
829           optimisticTaskId = null
830         }
831       }
832     )
833   }
834 
835   /*
836    *
837    * Tasks
838    *
839    */
840   val tasks: List<AppTask>
841     get() {
842       val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
843       return manager.appTasks
844     }
845 
846   // Get list of tasks in our format.
847   val experienceActivityTasks: List<AppTask>
848     get() = tasks
849 
850   // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly.
851   // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem.
852   // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing
853   // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind
854   // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out
855   // the root cause.
856   private fun killOrphanedLauncherActivities() {
857     try {
858       // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities
859       RecentTaskInfo::class.java.getDeclaredField("numActivities")
860       for (task: AppTask in tasks) {
861         val taskInfo = task.taskInfo
862         if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) {
863           task.finishAndRemoveTask()
864           return
865         }
866         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
867           if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) {
868             task.finishAndRemoveTask()
869             return
870           }
871         }
872       }
873     } catch (e: NoSuchFieldException) {
874       // Don't EXL here because this isn't actually a problem
875       Log.e(TAG, e.toString())
876     } catch (e: Throwable) {
877       EXL.e(TAG, e)
878     }
879   }
880 
881   fun moveTaskToFront(taskId: Int) {
882     tasks.find { it.taskInfo.id == taskId }?.also { task ->
883       // If we have the task in memory, tell the ExperienceActivity to check for new options.
884       // Otherwise options will be added in initialProps when the Experience starts.
885       val exponentTask = experienceActivityTaskForTaskId(taskId)
886       if (exponentTask != null) {
887         val experienceActivity = exponentTask.experienceActivity!!.get()
888         experienceActivity?.shouldCheckOptions()
889       }
890       task.moveToFront()
891     }
892   }
893 
894   fun killActivityStack(activity: Activity) {
895     val exponentTask = experienceActivityTaskForTaskId(activity.taskId)
896     if (exponentTask != null) {
897       removeExperienceActivityTask(exponentTask.manifestUrl)
898     }
899 
900     // Kill the current task.
901     val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
902     manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() }
903   }
904 
905   override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean {
906     var activity: ExperienceActivity? = null
907     for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) {
908       if (manifestUrl == experienceActivityTask.manifestUrl) {
909         val weakActivity =
910           if (experienceActivityTask.experienceActivity == null) {
911             null
912           } else {
913             experienceActivityTask.experienceActivity!!.get()
914           }
915         activity = weakActivity
916         if (weakActivity == null) {
917           // No activity, just force a reload
918           break
919         }
920         Exponent.getInstance().runOnUiThread { weakActivity.startLoading() }
921         break
922       }
923     }
924     activity?.let { killActivityStack(it) }
925     openManifestUrl(manifestUrl, null, true, forceCache)
926     return true
927   }
928 
929   override fun handleError(errorMessage: String) {
930     handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true)
931   }
932 
933   override fun handleError(exception: Exception) {
934     handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true)
935   }
936 
937   // TODO: probably need to call this from other places.
938   fun setHasError() {
939     hasError = true
940   }
941 
942   companion object {
943     private val TAG = Kernel::class.java.simpleName
944     private lateinit var instance: Kernel
945 
946     // Activities/Tasks
947     private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>()
948     private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>()
949     private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>()
950 
951     private fun addIntentDocumentFlags(intent: Intent) = intent.apply {
952       addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
953       addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
954       addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
955     }
956 
957     @JvmStatic
958     @DoNotStrip
959     fun reloadVisibleExperience(activityId: Int) {
960       val manifestUrl = getManifestUrlForActivityId(activityId)
961       if (manifestUrl != null) {
962         instance.reloadVisibleExperience(manifestUrl, false)
963       }
964     }
965 
966     // Called from DevServerHelper via ReactNativeStaticHelpers
967     @JvmStatic
968     @DoNotStrip
969     fun getManifestUrlForActivityId(activityId: Int): String? {
970       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl
971     }
972 
973     // Called from DevServerHelper via ReactNativeStaticHelpers
974     @JvmStatic
975     @DoNotStrip
976     fun getBundleUrlForActivityId(
977       activityId: Int,
978       host: String,
979       mainModuleId: String?,
980       bundleTypeId: String?,
981       devMode: Boolean,
982       jsMinify: Boolean
983     ): String? {
984       // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private
985       // BundleType enum for the possible values) but may need to
986       if (activityId == -1) {
987         // This is the kernel
988         return instance.bundleUrl
989       }
990       if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) {
991         return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId)
992       }
993       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
994     }
995 
996     // <= SDK 25
997     @DoNotStrip
998     fun getBundleUrlForActivityId(
999       activityId: Int,
1000       host: String,
1001       jsModulePath: String?,
1002       devMode: Boolean,
1003       jsMinify: Boolean
1004     ): String? {
1005       if (activityId == -1) {
1006         // This is the kernel
1007         return instance.bundleUrl
1008       }
1009       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
1010     }
1011 
1012     // <= SDK 21
1013     @DoNotStrip
1014     fun getBundleUrlForActivityId(
1015       activityId: Int,
1016       host: String,
1017       jsModulePath: String?,
1018       devMode: Boolean,
1019       hmr: Boolean,
1020       jsMinify: Boolean
1021     ): String? {
1022       if (activityId == -1) {
1023         // This is the kernel
1024         return instance.bundleUrl
1025       }
1026       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task ->
1027         var url = task.bundleUrl ?: return null
1028         if (hmr) {
1029           url = if (url.contains("hot=false")) {
1030             url.replace("hot=false", "hot=true")
1031           } else {
1032             "$url&hot=true"
1033           }
1034         }
1035         return url
1036       }
1037     }
1038 
1039     /*
1040    *
1041    * Error handling
1042    *
1043    */
1044     // Called using reflection from ReactAndroid.
1045     @DoNotStrip
1046     fun handleReactNativeError(
1047       errorMessage: String?,
1048       detailsUnversioned: Any?,
1049       exceptionId: Int?,
1050       isFatal: Boolean
1051     ) {
1052       handleReactNativeError(
1053         developerErrorMessage(errorMessage),
1054         detailsUnversioned,
1055         exceptionId,
1056         isFatal
1057       )
1058     }
1059 
1060     // Called using reflection from ReactAndroid.
1061     @DoNotStrip
1062     fun handleReactNativeError(
1063       throwable: Throwable?,
1064       errorMessage: String?,
1065       detailsUnversioned: Any?,
1066       exceptionId: Int?,
1067       isFatal: Boolean
1068     ) {
1069       handleReactNativeError(
1070         developerErrorMessage(errorMessage),
1071         detailsUnversioned,
1072         exceptionId,
1073         isFatal
1074       )
1075     }
1076 
1077     private fun handleReactNativeError(
1078       errorMessage: ExponentErrorMessage,
1079       detailsUnversioned: Any?,
1080       exceptionId: Int?,
1081       isFatal: Boolean
1082     ) {
1083       val stackList = ArrayList<Bundle>()
1084       if (detailsUnversioned != null) {
1085         val details = RNObject.wrap(detailsUnversioned)
1086         val arguments = RNObject("com.facebook.react.bridge.Arguments")
1087         arguments.loadVersion(details.version())
1088         for (i in 0 until details.call("size") as Int) {
1089           try {
1090             val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle
1091             stackList.add(bundle)
1092           } catch (e: Exception) {
1093             e.printStackTrace()
1094           }
1095         }
1096       } else if (BuildConfig.DEBUG) {
1097         val stackTraceElements = Thread.currentThread().stackTrace
1098         // stackTraceElements starts with a bunch of stuff we don't care about.
1099         for (i in 2 until stackTraceElements.size) {
1100           val element = stackTraceElements[i]
1101           if ((
1102             (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) &&
1103               ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError"))
1104             )
1105           ) {
1106             // Ignore these base error handling methods.
1107             continue
1108           }
1109           val bundle = Bundle().apply {
1110             putInt("column", 0)
1111             putInt("lineNumber", element.lineNumber)
1112             putString("methodName", element.methodName)
1113             putString("file", element.fileName)
1114           }
1115           stackList.add(bundle)
1116         }
1117       }
1118       val stack = stackList.toTypedArray()
1119       BaseExperienceActivity.addError(
1120         ExponentError(
1121           errorMessage, stack,
1122           getExceptionId(exceptionId), isFatal
1123         )
1124       )
1125     }
1126 
1127     private fun getExceptionId(originalId: Int?): Int {
1128       return if (originalId == null || originalId == -1) {
1129         (-(Math.random() * Int.MAX_VALUE)).toInt()
1130       } else originalId
1131     }
1132   }
1133 
1134   init {
1135     NativeModuleDepsProvider.getInstance().inject(Kernel::class.java, this)
1136     instance = this
1137     updateKernelRNOkHttp()
1138   }
1139 }
1140