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