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.kernelManifest
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.kernelManifest,
269                 HomeActivity.homeExpoPackages(),
270                 initialURL
271               )
272             )
273             .addPackage(
274               ExpoTurboPackage.kernelExpoTurboPackage(
275                 exponentManifest.kernelManifest, 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.kernelManifest.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.kernelManifest.getDebuggerHost()
320   private val kernelMainModuleName: String
321     get() = exponentManifest.kernelManifest.getMainModuleName()
322   private val bundleUrl: String?
323     get() {
324       return try {
325         exponentManifest.kernelManifest.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.kernelManifest.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 =
532       ScopedNotificationsUtils.getExperienceScopeKey(response) ?: return false
533     val experience = ExponentDB.experienceScopeKeyToExperienceSync(experienceScopeKey)
534     if (experience == null) {
535       Log.w("expo-notifications", "Couldn't find experience from scopeKey: $experienceScopeKey")
536       return false
537     }
538     val manifestUrl = experience.manifestUrl
539     openExperience(ExperienceOptions(manifestUrl, manifestUrl, null))
540     return true
541   }
542 
543   private fun openDefaultUrl() {
544     val defaultUrl =
545       if (Constants.INITIAL_URL == null) KernelConstants.HOME_MANIFEST_URL else Constants.INITIAL_URL
546     openExperience(ExperienceOptions(defaultUrl, defaultUrl, null))
547   }
548 
549   override fun openExperience(options: ExperienceOptions) {
550     openManifestUrl(getManifestUrlFromFullUri(options.manifestUri), options, true)
551   }
552 
553   private fun getManifestUrlFromFullUri(uriString: String?): String? {
554     if (uriString == null) {
555       return null
556     }
557 
558     val uri = Uri.parse(uriString)
559     val builder = uri.buildUpon()
560     val deepLinkPositionDashes =
561       uriString.indexOf(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH)
562     if (deepLinkPositionDashes >= 0) {
563       // do this safely so we preserve any query string
564       val pathSegments = uri.pathSegments
565       builder.path(null)
566       for (segment: String in pathSegments) {
567         if ((ExponentManifest.DEEP_LINK_SEPARATOR == segment)) {
568           break
569         }
570         builder.appendEncodedPath(segment)
571       }
572     }
573 
574     // transfer the release-channel param to the built URL as this will cause Expo Go to treat
575     // this as a different project
576     var releaseChannel = uri.getQueryParameter(ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL)
577     builder.query(null)
578     if (releaseChannel != null) {
579       // release channels cannot contain the ' ' character, so if this is present,
580       // it must be an encoded form of '+' which indicated a deep link in SDK <27.
581       // therefore, nothing after this is part of the release channel name so we should strip it.
582       // TODO: remove this check once SDK 26 and below are no longer supported
583       val releaseChannelDeepLinkPosition = releaseChannel.indexOf(' ')
584       if (releaseChannelDeepLinkPosition > -1) {
585         releaseChannel = releaseChannel.substring(0, releaseChannelDeepLinkPosition)
586       }
587       builder.appendQueryParameter(
588         ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL,
589         releaseChannel
590       )
591     }
592 
593     // transfer the expo-updates query params: runtime-version, channel-name
594     val expoUpdatesQueryParameters = listOf(
595       ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION,
596       ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME
597     )
598     for (queryParameter: String in expoUpdatesQueryParameters) {
599       val queryParameterValue = uri.getQueryParameter(queryParameter)
600       if (queryParameterValue != null) {
601         builder.appendQueryParameter(queryParameter, queryParameterValue)
602       }
603     }
604 
605     // ignore fragments as well (e.g. those added by auth-session)
606     builder.fragment(null)
607     var newUriString = builder.build().toString()
608     val deepLinkPositionPlus = newUriString.indexOf('+')
609     if (deepLinkPositionPlus >= 0 && deepLinkPositionDashes < 0) {
610       // need to keep this for backwards compatibility
611       newUriString = newUriString.substring(0, deepLinkPositionPlus)
612     }
613 
614     // manifest url doesn't have a trailing slash
615     if (newUriString.isNotEmpty()) {
616       val lastUrlChar = newUriString[newUriString.length - 1]
617       if (lastUrlChar == '/') {
618         newUriString = newUriString.substring(0, newUriString.length - 1)
619       }
620     }
621     return newUriString
622   }
623 
624   private fun openManifestUrl(
625     manifestUrl: String?,
626     options: ExperienceOptions?,
627     isOptimistic: Boolean,
628     forceCache: Boolean = false
629   ) {
630     SoLoader.init(context, false)
631     if (options == null) {
632       manifestUrlToOptions.remove(manifestUrl)
633     } else {
634       manifestUrlToOptions[manifestUrl] = options
635     }
636     if (manifestUrl == null || (manifestUrl == KernelConstants.HOME_MANIFEST_URL)) {
637       openHomeActivity()
638       return
639     }
640     if (Constants.isStandaloneApp()) {
641       openShellAppActivity(forceCache)
642       return
643     }
644     ErrorActivity.clearErrorList()
645     val tasks: List<AppTask> = experienceActivityTasks
646     var existingTask: AppTask? = null
647     for (i in tasks.indices) {
648       val task = tasks[i]
649       val baseIntent = task.taskInfo.baseIntent
650       if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && (
651         baseIntent.getStringExtra(
652             KernelConstants.MANIFEST_URL_KEY
653           ) == manifestUrl
654         )
655       ) {
656         existingTask = task
657         break
658       }
659     }
660     if (isOptimistic && existingTask == null) {
661       openOptimisticExperienceActivity(manifestUrl)
662     }
663     if (existingTask != null) {
664       try {
665         moveTaskToFront(existingTask.taskInfo.id)
666       } catch (e: IllegalArgumentException) {
667         // Sometimes task can't be found.
668         existingTask = null
669         openOptimisticExperienceActivity(manifestUrl)
670       }
671     }
672     val finalExistingTask = existingTask
673     if (existingTask == null) {
674       ExpoUpdatesAppLoader(
675         manifestUrl,
676         object : AppLoaderCallback {
677           override fun onOptimisticManifest(optimisticManifest: RawManifest) {
678             Exponent.getInstance()
679               .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) }
680           }
681 
682           override fun onManifestCompleted(manifest: RawManifest) {
683             Exponent.getInstance().runOnUiThread {
684               try {
685                 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask)
686               } catch (e: JSONException) {
687                 handleError(e)
688               }
689             }
690           }
691 
692           override fun onBundleCompleted(localBundlePath: String) {
693             Exponent.getInstance().runOnUiThread { sendBundleToExperienceActivity(localBundlePath) }
694           }
695 
696           override fun emitEvent(params: JSONObject) {
697             val task = manifestUrlToExperienceActivityTask[manifestUrl]
698             if (task != null) {
699               val experienceActivity = task.experienceActivity!!.get()
700               experienceActivity?.emitUpdatesEvent(params)
701             }
702           }
703 
704           override fun updateStatus(status: AppLoaderStatus) {
705             if (optimisticActivity != null) {
706               optimisticActivity!!.setLoadingProgressStatusIfEnabled(status)
707             }
708           }
709 
710           override fun onError(e: Exception) {
711             Exponent.getInstance().runOnUiThread { handleError(e) }
712           }
713         },
714         forceCache
715       ).start(context)
716     }
717   }
718 
719   @Throws(JSONException::class)
720   private fun openManifestUrlStep2(
721     manifestUrl: String,
722     manifest: RawManifest,
723     existingTask: AppTask?
724   ) {
725     val bundleUrl = toHttp(manifest.getBundleURL())
726     val task = getExperienceActivityTask(manifestUrl)
727     task.bundleUrl = bundleUrl
728     ExponentManifest.normalizeRawManifestInPlace(manifest, manifestUrl)
729     val opts = JSONObject()
730     if (existingTask == null) {
731       sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl, opts)
732     }
733     val params = Arguments.createMap().apply {
734       putString("manifestUrl", manifestUrl)
735       putString("manifestString", manifest.toString())
736     }
737     queueEvent(
738       "ExponentKernel.addHistoryItem", params,
739       object : KernelEventCallback {
740         override fun onEventSuccess(result: ReadableMap) {
741           EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.")
742         }
743 
744         override fun onEventFailure(errorMessage: String?) {
745           EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage")
746         }
747       }
748     )
749     killOrphanedLauncherActivities()
750   }
751 
752   /*
753    *
754    * Optimistic experiences
755    *
756    */
757   private fun openOptimisticExperienceActivity(manifestUrl: String?) {
758     try {
759       val intent = Intent(activityContext, ExperienceActivity::class.java).apply {
760         addIntentDocumentFlags(this)
761         putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl)
762         putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true)
763       }
764       activityContext!!.startActivity(intent)
765     } catch (e: Throwable) {
766       EXL.e(TAG, e)
767     }
768   }
769 
770   fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) {
771     optimisticActivity = experienceActivity
772     optimisticTaskId = taskId
773     AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY)
774     AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY)
775   }
776 
777   fun sendOptimisticManifestToExperienceActivity(optimisticManifest: RawManifest?) {
778     AsyncCondition.wait(
779       KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY,
780       object : AsyncConditionListener {
781         override fun isReady(): Boolean {
782           return optimisticActivity != null && optimisticTaskId != null
783         }
784 
785         override fun execute() {
786           optimisticActivity!!.setOptimisticManifest(optimisticManifest)
787         }
788       }
789     )
790   }
791 
792   private fun sendManifestToExperienceActivity(
793     manifestUrl: String?,
794     manifest: RawManifest?,
795     bundleUrl: String?,
796     kernelOptions: JSONObject?
797   ) {
798     AsyncCondition.wait(
799       KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY,
800       object : AsyncConditionListener {
801         override fun isReady(): Boolean {
802           return optimisticActivity != null && optimisticTaskId != null
803         }
804 
805         override fun execute() {
806           optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl, kernelOptions)
807           AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY)
808         }
809       }
810     )
811   }
812 
813   fun sendBundleToExperienceActivity(localBundlePath: String?) {
814     AsyncCondition.wait(
815       KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY,
816       object : AsyncConditionListener {
817         override fun isReady(): Boolean {
818           return optimisticActivity != null && optimisticTaskId != null
819         }
820 
821         override fun execute() {
822           optimisticActivity!!.setBundle(localBundlePath)
823           optimisticActivity = null
824           optimisticTaskId = null
825         }
826       }
827     )
828   }
829 
830   /*
831    *
832    * Tasks
833    *
834    */
835   val tasks: List<AppTask>
836     get() {
837       val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
838       return manager.appTasks
839     }
840 
841   // Get list of tasks in our format.
842   val experienceActivityTasks: List<AppTask>
843     get() = tasks
844 
845   // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly.
846   // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem.
847   // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing
848   // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind
849   // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out
850   // the root cause.
851   private fun killOrphanedLauncherActivities() {
852     try {
853       // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities
854       RecentTaskInfo::class.java.getDeclaredField("numActivities")
855       for (task: AppTask in tasks) {
856         val taskInfo = task.taskInfo
857         if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) {
858           task.finishAndRemoveTask()
859           return
860         }
861         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
862           if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) {
863             task.finishAndRemoveTask()
864             return
865           }
866         }
867       }
868     } catch (e: NoSuchFieldException) {
869       // Don't EXL here because this isn't actually a problem
870       Log.e(TAG, e.toString())
871     } catch (e: Throwable) {
872       EXL.e(TAG, e)
873     }
874   }
875 
876   fun moveTaskToFront(taskId: Int) {
877     tasks.find { it.taskInfo.id == taskId }?.also { task ->
878       // If we have the task in memory, tell the ExperienceActivity to check for new options.
879       // Otherwise options will be added in initialProps when the Experience starts.
880       val exponentTask = experienceActivityTaskForTaskId(taskId)
881       if (exponentTask != null) {
882         val experienceActivity = exponentTask.experienceActivity!!.get()
883         experienceActivity?.shouldCheckOptions()
884       }
885       task.moveToFront()
886     }
887   }
888 
889   fun killActivityStack(activity: Activity) {
890     val exponentTask = experienceActivityTaskForTaskId(activity.taskId)
891     if (exponentTask != null) {
892       removeExperienceActivityTask(exponentTask.manifestUrl)
893     }
894 
895     // Kill the current task.
896     val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
897     manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() }
898   }
899 
900   override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean {
901     var activity: ExperienceActivity? = null
902     for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) {
903       if (manifestUrl == experienceActivityTask.manifestUrl) {
904         val weakActivity =
905           if (experienceActivityTask.experienceActivity == null) {
906             null
907           } else {
908             experienceActivityTask.experienceActivity!!.get()
909           }
910         activity = weakActivity
911         if (weakActivity == null) {
912           // No activity, just force a reload
913           break
914         }
915         Exponent.getInstance().runOnUiThread { weakActivity.startLoading() }
916         break
917       }
918     }
919     activity?.let { killActivityStack(it) }
920     openManifestUrl(manifestUrl, null, true, forceCache)
921     return true
922   }
923 
924   override fun handleError(errorMessage: String) {
925     handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true)
926   }
927 
928   override fun handleError(exception: Exception) {
929     handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true)
930   }
931 
932   // TODO: probably need to call this from other places.
933   fun setHasError() {
934     hasError = true
935   }
936 
937   companion object {
938     private val TAG = Kernel::class.java.simpleName
939     private lateinit var instance: Kernel
940 
941     // Activities/Tasks
942     private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>()
943     private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>()
944     private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>()
945 
946     private fun addIntentDocumentFlags(intent: Intent) = intent.apply {
947       addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
948       addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
949       addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
950     }
951 
952     @JvmStatic
953     @DoNotStrip
954     fun reloadVisibleExperience(activityId: Int) {
955       val manifestUrl = getManifestUrlForActivityId(activityId)
956       if (manifestUrl != null) {
957         instance.reloadVisibleExperience(manifestUrl, false)
958       }
959     }
960 
961     // Called from DevServerHelper via ReactNativeStaticHelpers
962     @JvmStatic
963     @DoNotStrip
964     fun getManifestUrlForActivityId(activityId: Int): String? {
965       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl
966     }
967 
968     // Called from DevServerHelper via ReactNativeStaticHelpers
969     @JvmStatic
970     @DoNotStrip
971     fun getBundleUrlForActivityId(
972       activityId: Int,
973       host: String,
974       mainModuleId: String?,
975       bundleTypeId: String?,
976       devMode: Boolean,
977       jsMinify: Boolean
978     ): String? {
979       // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private
980       // BundleType enum for the possible values) but may need to
981       if (activityId == -1) {
982         // This is the kernel
983         return instance.bundleUrl
984       }
985       if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) {
986         return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId)
987       }
988       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
989     }
990 
991     // <= SDK 25
992     @DoNotStrip
993     fun getBundleUrlForActivityId(
994       activityId: Int,
995       host: String,
996       jsModulePath: String?,
997       devMode: Boolean,
998       jsMinify: Boolean
999     ): String? {
1000       if (activityId == -1) {
1001         // This is the kernel
1002         return instance.bundleUrl
1003       }
1004       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl
1005     }
1006 
1007     // <= SDK 21
1008     @DoNotStrip
1009     fun getBundleUrlForActivityId(
1010       activityId: Int,
1011       host: String,
1012       jsModulePath: String?,
1013       devMode: Boolean,
1014       hmr: Boolean,
1015       jsMinify: Boolean
1016     ): String? {
1017       if (activityId == -1) {
1018         // This is the kernel
1019         return instance.bundleUrl
1020       }
1021       return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task ->
1022         var url = task.bundleUrl ?: return null
1023         if (hmr) {
1024           url = if (url.contains("hot=false")) {
1025             url.replace("hot=false", "hot=true")
1026           } else {
1027             "$url&hot=true"
1028           }
1029         }
1030         return url
1031       }
1032     }
1033 
1034     /*
1035    *
1036    * Error handling
1037    *
1038    */
1039     // Called using reflection from ReactAndroid.
1040     @DoNotStrip
1041     fun handleReactNativeError(
1042       errorMessage: String?,
1043       detailsUnversioned: Any?,
1044       exceptionId: Int?,
1045       isFatal: Boolean
1046     ) {
1047       handleReactNativeError(
1048         developerErrorMessage(errorMessage),
1049         detailsUnversioned,
1050         exceptionId,
1051         isFatal
1052       )
1053     }
1054 
1055     // Called using reflection from ReactAndroid.
1056     @DoNotStrip
1057     fun handleReactNativeError(
1058       throwable: Throwable?,
1059       errorMessage: String?,
1060       detailsUnversioned: Any?,
1061       exceptionId: Int?,
1062       isFatal: Boolean
1063     ) {
1064       handleReactNativeError(
1065         developerErrorMessage(errorMessage),
1066         detailsUnversioned,
1067         exceptionId,
1068         isFatal
1069       )
1070     }
1071 
1072     private fun handleReactNativeError(
1073       errorMessage: ExponentErrorMessage,
1074       detailsUnversioned: Any?,
1075       exceptionId: Int?,
1076       isFatal: Boolean
1077     ) {
1078       val stackList = ArrayList<Bundle>()
1079       if (detailsUnversioned != null) {
1080         val details = RNObject.wrap(detailsUnversioned)
1081         val arguments = RNObject("com.facebook.react.bridge.Arguments")
1082         arguments.loadVersion(details.version())
1083         for (i in 0 until details.call("size") as Int) {
1084           try {
1085             val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle
1086             stackList.add(bundle)
1087           } catch (e: Exception) {
1088             e.printStackTrace()
1089           }
1090         }
1091       } else if (BuildConfig.DEBUG) {
1092         val stackTraceElements = Thread.currentThread().stackTrace
1093         // stackTraceElements starts with a bunch of stuff we don't care about.
1094         for (i in 2 until stackTraceElements.size) {
1095           val element = stackTraceElements[i]
1096           if ((
1097             (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) &&
1098               ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError"))
1099             )
1100           ) {
1101             // Ignore these base error handling methods.
1102             continue
1103           }
1104           val bundle = Bundle().apply {
1105             putInt("column", 0)
1106             putInt("lineNumber", element.lineNumber)
1107             putString("methodName", element.methodName)
1108             putString("file", element.fileName)
1109           }
1110           stackList.add(bundle)
1111         }
1112       }
1113       val stack = stackList.toTypedArray()
1114       BaseExperienceActivity.addError(
1115         ExponentError(
1116           errorMessage, stack,
1117           getExceptionId(exceptionId), isFatal
1118         )
1119       )
1120     }
1121 
1122     private fun getExceptionId(originalId: Int?): Int {
1123       return if (originalId == null || originalId == -1) {
1124         (-(Math.random() * Int.MAX_VALUE)).toInt()
1125       } else originalId
1126     }
1127   }
1128 
1129   init {
1130     NativeModuleDepsProvider.getInstance().inject(Kernel::class.java, this)
1131     instance = this
1132     updateKernelRNOkHttp()
1133   }
1134 }
1135