1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.experience
3 
4 import android.app.Activity
5 import android.content.Intent
6 import android.content.pm.PackageManager
7 import android.net.Uri
8 import android.os.Build
9 import android.os.Bundle
10 import android.os.Handler
11 import android.os.Process
12 import android.view.KeyEvent
13 import android.view.View
14 import android.view.ViewGroup
15 import android.widget.FrameLayout
16 import androidx.annotation.UiThread
17 import androidx.appcompat.app.AppCompatActivity
18 import androidx.core.content.ContextCompat
19 import com.facebook.infer.annotation.Assertions
20 import com.facebook.internal.BundleJSONConverter
21 import com.facebook.react.devsupport.DoubleTapReloadRecognizer
22 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
23 import com.facebook.react.modules.core.PermissionAwareActivity
24 import com.facebook.react.modules.core.PermissionListener
25 import de.greenrobot.event.EventBus
26 import expo.modules.core.interfaces.Package
27 import expo.modules.manifests.core.Manifest
28 import host.exp.exponent.Constants
29 import host.exp.exponent.ExponentManifest
30 import host.exp.exponent.RNObject
31 import host.exp.exponent.analytics.Analytics
32 import host.exp.exponent.analytics.EXL
33 import host.exp.exponent.di.NativeModuleDepsProvider
34 import host.exp.exponent.experience.BaseExperienceActivity.ExperienceContentLoaded
35 import host.exp.exponent.experience.splashscreen.LoadingView
36 import host.exp.exponent.kernel.*
37 import host.exp.exponent.kernel.KernelConstants.AddedExperienceEventEvent
38 import host.exp.exponent.kernel.services.ErrorRecoveryManager
39 import host.exp.exponent.kernel.services.ExpoKernelServiceRegistry
40 import host.exp.exponent.notifications.ExponentNotification
41 import host.exp.exponent.storage.ExponentSharedPreferences
42 import host.exp.exponent.utils.ExperienceActivityUtils
43 import host.exp.exponent.utils.ScopedPermissionsRequester
44 import host.exp.expoview.Exponent
45 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties
46 import host.exp.expoview.Exponent.StartReactInstanceDelegate
47 import host.exp.expoview.R
48 import org.json.JSONException
49 import org.json.JSONObject
50 import versioned.host.exp.exponent.ExponentPackage
51 import java.util.*
52 import javax.inject.Inject
53 
54 abstract class ReactNativeActivity :
55   AppCompatActivity(),
56   DefaultHardwareBackBtnHandler,
57   PermissionAwareActivity {
58 
59   class ExperienceDoneLoadingEvent internal constructor(val activity: Activity)
60 
61   open fun initialProps(expBundle: Bundle?): Bundle? {
62     return expBundle
63   }
64 
65   protected open fun onDoneLoading() {}
66 
67   // Will be called after waitForDrawOverOtherAppPermission
68   protected open fun startReactInstance() {}
69 
70   protected var reactInstanceManager: RNObject =
71     RNObject("com.facebook.react.ReactInstanceManager")
72   protected var isCrashed = false
73 
74   protected var manifestUrl: String? = null
75   var experienceKey: ExperienceKey? = null
76   protected var sdkVersion: String? = null
77   protected var activityId = 0
78 
79   // In detach we want UNVERSIONED most places. We still need the numbered sdk version
80   // when creating cache keys.
81   protected var detachSdkVersion: String? = null
82 
83   protected lateinit var reactRootView: RNObject
84   private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
85   var isLoading = true
86     protected set
87   protected var jsBundlePath: String? = null
88   protected var manifest: Manifest? = null
89   var isInForeground = false
90     protected set
91   private var scopedPermissionsRequester: ScopedPermissionsRequester? = null
92 
93   @Inject
94   protected lateinit var exponentSharedPreferences: ExponentSharedPreferences
95 
96   @Inject
97   lateinit var expoKernelServiceRegistry: ExpoKernelServiceRegistry
98 
99   private lateinit var containerView: FrameLayout
100 
101   /**
102    * This view is optional and available only when the app runs in Expo Go.
103    */
104   private var loadingView: LoadingView? = null
105   private lateinit var reactContainerView: FrameLayout
106   private val handler = Handler()
107 
108   protected open fun shouldCreateLoadingView(): Boolean {
109     return !Constants.isStandaloneApp() || Constants.SHOW_LOADING_VIEW_IN_SHELL_APP
110   }
111 
112   val rootView: View?
113     get() = reactRootView.get() as View?
114 
115   override fun onCreate(savedInstanceState: Bundle?) {
116     super.onCreate(null)
117 
118     containerView = FrameLayout(this)
119     setContentView(containerView)
120 
121     reactContainerView = FrameLayout(this)
122     containerView.addView(reactContainerView)
123 
124     if (shouldCreateLoadingView()) {
125       containerView.setBackgroundColor(
126         ContextCompat.getColor(
127           this,
128           R.color.splashscreen_background
129         )
130       )
131       loadingView = LoadingView(this)
132       loadingView!!.show()
133       containerView.addView(loadingView)
134     }
135 
136     doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
137     Exponent.initialize(this, application)
138     NativeModuleDepsProvider.instance.inject(ReactNativeActivity::class.java, this)
139 
140     // Can't call this here because subclasses need to do other initialization
141     // before their listener methods are called.
142     // EventBus.getDefault().registerSticky(this);
143   }
144 
145   protected fun setReactRootView(reactRootView: View) {
146     reactContainerView.removeAllViews()
147     addReactViewToContentContainer(reactRootView)
148   }
149 
150   fun addReactViewToContentContainer(reactView: View) {
151     if (reactView.parent != null) {
152       (reactView.parent as ViewGroup).removeView(reactView)
153     }
154     reactContainerView.addView(reactView)
155   }
156 
157   fun hasReactView(reactView: View): Boolean {
158     return reactView.parent === reactContainerView
159   }
160 
161   protected fun hideLoadingView() {
162     loadingView?.let {
163       val viewGroup = it.parent as ViewGroup?
164       viewGroup?.removeView(it)
165       it.hide()
166     }
167     loadingView = null
168   }
169 
170   protected fun removeAllViewsFromContainer() {
171     containerView.removeAllViews()
172   }
173   // region Loading
174   /**
175    * Successfully finished loading
176    */
177   @UiThread
178   protected fun finishLoading() {
179     waitForReactAndFinishLoading()
180   }
181 
182   /**
183    * There was an error during loading phase
184    */
185   protected fun interruptLoading() {
186     handler.removeCallbacksAndMessages(null)
187   }
188 
189   // Loop until a view is added to the ReactRootView and once it happens run callback
190   private fun waitForReactRootViewToHaveChildrenAndRunCallback(callback: Runnable) {
191     if (reactRootView.isNull) {
192       return
193     }
194 
195     if (reactRootView.call("getChildCount") as Int > 0) {
196       callback.run()
197     } else {
198       handler.postDelayed(
199         { waitForReactRootViewToHaveChildrenAndRunCallback(callback) },
200         VIEW_TEST_INTERVAL_MS
201       )
202     }
203   }
204 
205   /**
206    * Waits for JS side of React to be launched and then performs final launching actions.
207    */
208   private fun waitForReactAndFinishLoading() {
209     if (Constants.isStandaloneApp() && Constants.SHOW_LOADING_VIEW_IN_SHELL_APP) {
210       val layoutParams = containerView.layoutParams
211       layoutParams.height = FrameLayout.LayoutParams.MATCH_PARENT
212       containerView.layoutParams = layoutParams
213     }
214     waitForReactRootViewToHaveChildrenAndRunCallback {
215       onDoneLoading()
216       try {
217         ExperienceActivityUtils.setRootViewBackgroundColor(manifest!!, rootView!!)
218       } catch (e: Exception) {
219         EXL.e(TAG, e)
220       }
221       ErrorRecoveryManager.getInstance(experienceKey!!).markExperienceLoaded()
222       pollForEventsToSendToRN()
223       EventBus.getDefault().post(ExperienceDoneLoadingEvent(this))
224       isLoading = false
225     }
226   }
227   // endregion
228   // region SplashScreen
229   /**
230    * Get what version (among versioned classes) of ReactRootView.class SplashScreen module should be looking for.
231    */
232   protected fun getRootViewClass(manifest: Manifest): Class<out ViewGroup> {
233     val reactRootViewRNClass = reactRootView.rnClass()
234     if (reactRootViewRNClass != null) {
235       return reactRootViewRNClass as Class<out ViewGroup>
236     }
237     var sdkVersion = manifest.getSDKVersion()
238     if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == this.sdkVersion) {
239       sdkVersion = RNObject.UNVERSIONED
240     }
241     sdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion
242     return RNObject("com.facebook.react.ReactRootView").loadVersion(sdkVersion!!).rnClass() as Class<out ViewGroup>
243   }
244 
245   // endregion
246   override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
247     if (reactInstanceManager.isNotNull && !isCrashed) {
248       if (devSupportManager.call("getDevSupportEnabled") as Boolean) {
249         val didDoubleTapR = Assertions.assertNotNull(doubleTapReloadRecognizer)
250           .didDoubleTapR(keyCode, currentFocus)
251         if (didDoubleTapR) {
252           devSupportManager.call("reloadExpoApp")
253           return true
254         }
255       }
256     }
257     return super.onKeyUp(keyCode, event)
258   }
259 
260   override fun onBackPressed() {
261     if (reactInstanceManager.isNotNull && !isCrashed) {
262       reactInstanceManager.call("onBackPressed")
263     } else {
264       super.onBackPressed()
265     }
266   }
267 
268   override fun invokeDefaultOnBackPressed() {
269     super.onBackPressed()
270   }
271 
272   override fun onPause() {
273     super.onPause()
274     if (reactInstanceManager.isNotNull && !isCrashed) {
275       reactInstanceManager.onHostPause()
276       // TODO: use onHostPause(activity)
277     }
278   }
279 
280   override fun onResume() {
281     super.onResume()
282     if (reactInstanceManager.isNotNull && !isCrashed) {
283       reactInstanceManager.onHostResume(this, this)
284     }
285   }
286 
287   override fun onDestroy() {
288     super.onDestroy()
289     destroyReactInstanceManager()
290     handler.removeCallbacksAndMessages(null)
291     EventBus.getDefault().unregister(this)
292   }
293 
294   public override fun onNewIntent(intent: Intent) {
295     if (reactInstanceManager.isNotNull && !isCrashed) {
296       try {
297         reactInstanceManager.call("onNewIntent", intent)
298       } catch (e: Throwable) {
299         EXL.e(TAG, e.toString())
300         super.onNewIntent(intent)
301       }
302     } else {
303       super.onNewIntent(intent)
304     }
305   }
306 
307   open val isDebugModeEnabled: Boolean
308     get() = manifest?.isDevelopmentMode() ?: false
309 
310   protected open fun destroyReactInstanceManager() {
311     if (reactInstanceManager.isNotNull && !isCrashed) {
312       reactInstanceManager.call("destroy")
313     }
314   }
315 
316   public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
317     super.onActivityResult(requestCode, resultCode, data)
318 
319     Exponent.instance.onActivityResult(requestCode, resultCode, data)
320 
321     if (reactInstanceManager.isNotNull && !isCrashed) {
322       reactInstanceManager.call("onActivityResult", this, requestCode, resultCode, data)
323     }
324 
325     // Have permission to draw over other apps. Resume loading.
326     if (requestCode == KernelConstants.OVERLAY_PERMISSION_REQUEST_CODE) {
327       // startReactInstance() checks isInForeground and onActivityResult is called before onResume,
328       // so manually set this here.
329       isInForeground = true
330       startReactInstance()
331     }
332   }
333 
334   fun startReactInstance(
335     delegate: StartReactInstanceDelegate,
336     intentUri: String?,
337     sdkVersion: String?,
338     notification: ExponentNotification?,
339     isShellApp: Boolean,
340     extraNativeModules: List<Any>?,
341     extraExpoPackages: List<Package>?,
342     progressListener: DevBundleDownloadProgressListener
343   ): RNObject {
344     if (isCrashed || !delegate.isInForeground) {
345       // Can sometimes get here after an error has occurred. Return early or else we'll hit
346       // a null pointer at mReactRootView.startReactApplication
347       return RNObject("com.facebook.react.ReactInstanceManager")
348     }
349 
350     val experienceProperties = mapOf<String, Any?>(
351       KernelConstants.MANIFEST_URL_KEY to manifestUrl,
352       KernelConstants.LINKING_URI_KEY to linkingUri,
353       KernelConstants.INTENT_URI_KEY to intentUri,
354       KernelConstants.IS_HEADLESS_KEY to false
355     )
356 
357     val instanceManagerBuilderProperties = InstanceManagerBuilderProperties(
358       application = application,
359       jsBundlePath = jsBundlePath,
360       experienceProperties = experienceProperties,
361       expoPackages = extraExpoPackages,
362       exponentPackageDelegate = delegate.exponentPackageDelegate,
363       manifest = manifest!!,
364       singletonModules = ExponentPackage.getOrCreateSingletonModules(applicationContext, manifest, extraExpoPackages)
365     )
366 
367     val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(sdkVersion!!)
368     val builder = versionedUtils.callRecursive(
369       "getReactInstanceManagerBuilder",
370       instanceManagerBuilderProperties
371     )!!
372 
373     builder.call("setCurrentActivity", this)
374 
375     // ReactNativeInstance is considered to be resumed when it has its activity attached, which is expected to be the case here
376     builder.call(
377       "setInitialLifecycleState",
378       RNObject.versionedEnum(sdkVersion, "com.facebook.react.common.LifecycleState", "RESUMED")
379     )
380 
381     if (extraNativeModules != null) {
382       for (nativeModule in extraNativeModules) {
383         builder.call("addPackage", nativeModule)
384       }
385     }
386 
387     if (delegate.isDebugModeEnabled) {
388       val debuggerHost = manifest!!.getDebuggerHost()
389       val mainModuleName = manifest!!.getMainModuleName()
390       Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder)
391 
392       val devLoadingView =
393         RNObject("com.facebook.react.devsupport.DevLoadingViewController").loadVersion(sdkVersion)
394       devLoadingView.callRecursive("setDevLoadingEnabled", false)
395 
396       val devBundleDownloadListener =
397         RNObject("host.exp.exponent.ExponentDevBundleDownloadListener")
398           .loadVersion(sdkVersion)
399           .construct(progressListener)
400       builder.callRecursive("setDevBundleDownloadListener", devBundleDownloadListener.get())
401     } else {
402       waitForReactAndFinishLoading()
403     }
404 
405     val bundle = Bundle()
406     val exponentProps = JSONObject()
407     if (notification != null) {
408       bundle.putString("notification", notification.body) // Deprecated
409       try {
410         exponentProps.put("notification", notification.toJSONObject("selected"))
411       } catch (e: JSONException) {
412         e.printStackTrace()
413       }
414     }
415 
416     try {
417       exponentProps.put("manifestString", manifest.toString())
418       exponentProps.put("shell", isShellApp)
419       exponentProps.put("initialUri", intentUri)
420     } catch (e: JSONException) {
421       EXL.e(TAG, e)
422     }
423 
424     val metadata = exponentSharedPreferences.getExperienceMetadata(experienceKey!!)
425     if (metadata != null) {
426       // TODO: fix this. this is the only place that EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS is sent to the experience,
427       // we need to send them with the standard notification events so that you can get all the unread notification through an event
428       // Copy unreadNotifications into exponentProps
429       if (metadata.has(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)) {
430         try {
431           val unreadNotifications =
432             metadata.getJSONArray(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
433           delegate.handleUnreadNotifications(unreadNotifications)
434         } catch (e: JSONException) {
435           e.printStackTrace()
436         }
437         metadata.remove(ExponentSharedPreferences.EXPERIENCE_METADATA_UNREAD_REMOTE_NOTIFICATIONS)
438       }
439       exponentSharedPreferences.updateExperienceMetadata(experienceKey!!, metadata)
440     }
441 
442     try {
443       bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps))
444     } catch (e: JSONException) {
445       throw Error("JSONObject failed to be converted to Bundle", e)
446     }
447 
448     if (!delegate.isInForeground) {
449       return RNObject("com.facebook.react.ReactInstanceManager")
450     }
451 
452     Analytics.markEvent(Analytics.TimedEvent.STARTED_LOADING_REACT_NATIVE)
453     val mReactInstanceManager = builder.callRecursive("build")!!
454     val devSettings =
455       mReactInstanceManager.callRecursive("getDevSupportManager")!!.callRecursive("getDevSettings")
456     if (devSettings != null) {
457       devSettings.setField("exponentActivityId", activityId)
458       if (devSettings.call("isRemoteJSDebugEnabled") as Boolean) {
459         waitForReactAndFinishLoading()
460       }
461     }
462 
463     mReactInstanceManager.onHostResume(this, this)
464     val appKey = manifest!!.getAppKey()
465     reactRootView.call(
466       "startReactApplication",
467       mReactInstanceManager.get(),
468       appKey ?: KernelConstants.DEFAULT_APPLICATION_KEY,
469       initialProps(bundle)
470     )
471 
472     // Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager}
473     // Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}.
474     // Originally react-native will automatically attach after `startReactApplication`.
475     // After https://github.com/facebook/react-native/commit/2c896d35782cd04c8,
476     // the only remaining path is by `onMeasure`.
477     reactRootView.call("requestLayout")
478 
479     return mReactInstanceManager
480   }
481 
482   protected fun shouldShowErrorScreen(errorMessage: ExponentErrorMessage): Boolean {
483     if (isLoading) {
484       // Don't hit ErrorRecoveryManager until bridge is initialized.
485       // This is the same on iOS.
486       return true
487     }
488     val errorRecoveryManager = ErrorRecoveryManager.getInstance(experienceKey!!)
489     errorRecoveryManager.markErrored()
490 
491     if (!errorRecoveryManager.shouldReloadOnError()) {
492       return true
493     }
494 
495     if (!KernelProvider.instance.reloadVisibleExperience(manifestUrl!!)) {
496       // Kernel couldn't reload, show error screen
497       return true
498     }
499 
500     errorQueue.clear()
501     try {
502       val eventProperties = JSONObject().apply {
503         put(Analytics.USER_ERROR_MESSAGE, errorMessage.userErrorMessage())
504         put(Analytics.DEVELOPER_ERROR_MESSAGE, errorMessage.developerErrorMessage())
505         put(Analytics.MANIFEST_URL, manifestUrl)
506       }
507       Analytics.logEvent(Analytics.AnalyticsEvent.ERROR_RELOADED, eventProperties)
508     } catch (e: Exception) {
509       EXL.e(TAG, e.message)
510     }
511 
512     return false
513   }
514 
515   fun onEventMainThread(event: AddedExperienceEventEvent) {
516     if (manifestUrl != null && manifestUrl == event.manifestUrl) {
517       pollForEventsToSendToRN()
518     }
519   }
520 
521   fun onEvent(event: ExperienceContentLoaded?) {}
522 
523   private fun pollForEventsToSendToRN() {
524     if (manifestUrl == null) {
525       return
526     }
527 
528     try {
529       val rctDeviceEventEmitter =
530         RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter")
531       rctDeviceEventEmitter.loadVersion(detachSdkVersion!!)
532       val existingEmitter = reactInstanceManager.callRecursive("getCurrentReactContext")!!
533         .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())
534       if (existingEmitter != null) {
535         val events = KernelProvider.instance.consumeExperienceEvents(manifestUrl!!)
536         for ((eventName, eventPayload) in events) {
537           existingEmitter.call("emit", eventName, eventPayload)
538         }
539       }
540     } catch (e: Throwable) {
541       EXL.e(TAG, e)
542     }
543   }
544 
545   // for getting global permission
546   override fun checkSelfPermission(permission: String): Int {
547     return super.checkPermission(permission, Process.myPid(), Process.myUid())
548   }
549 
550   override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
551     // in scoped application we don't have `don't ask again` button
552     return if (!Constants.isStandaloneApp() && checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
553       true
554     } else super.shouldShowRequestPermissionRationale(permission)
555   }
556 
557   override fun requestPermissions(
558     permissions: Array<String>,
559     requestCode: Int,
560     listener: PermissionListener
561   ) {
562     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
563       val name = manifest!!.getName()
564       scopedPermissionsRequester = ScopedPermissionsRequester(experienceKey!!)
565       scopedPermissionsRequester!!.requestPermissions(this, name ?: "", permissions, listener)
566     } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
567       super.requestPermissions(permissions, requestCode)
568     }
569   }
570 
571   override fun onRequestPermissionsResult(
572     requestCode: Int,
573     permissions: Array<String>,
574     grantResults: IntArray
575   ) {
576     if (requestCode == ScopedPermissionsRequester.EXPONENT_PERMISSIONS_REQUEST) {
577       if (permissions.isNotEmpty() && grantResults.size == permissions.size && scopedPermissionsRequester != null) {
578         if (scopedPermissionsRequester!!.onRequestPermissionsResult(permissions, grantResults)) {
579           scopedPermissionsRequester = null
580         }
581       }
582     } else {
583       super.onRequestPermissionsResult(requestCode, permissions, grantResults)
584     }
585   }
586 
587   // for getting scoped permission
588   override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
589     val globalResult = super.checkPermission(permission, pid, uid)
590     return expoKernelServiceRegistry.permissionsKernelService.getPermissions(
591       globalResult,
592       packageManager,
593       permission,
594       experienceKey!!
595     )
596   }
597 
598   val devSupportManager: RNObject
599     get() = reactInstanceManager.callRecursive("getDevSupportManager")!!
600 
601   // deprecated in favor of Expo.Linking.makeUrl
602   // TODO: remove this
603   private val linkingUri: String?
604     get() = if (Constants.SHELL_APP_SCHEME != null) {
605       Constants.SHELL_APP_SCHEME + "://"
606     } else {
607       val uri = Uri.parse(manifestUrl)
608       val host = uri.host
609       if (host != null && (
610         host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" ||
611           host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith(
612             ".expo.test"
613           )
614         )
615       ) {
616         val pathSegments = uri.pathSegments
617         val builder = uri.buildUpon()
618         builder.path(null)
619         for (segment in pathSegments) {
620           if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) {
621             break
622           }
623           builder.appendEncodedPath(segment)
624         }
625         builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build()
626           .toString()
627       } else {
628         manifestUrl
629       }
630     }
631 
632   companion object {
633     private val TAG = ReactNativeActivity::class.java.simpleName
634     private const val VIEW_TEST_INTERVAL_MS: Long = 20
635     @JvmStatic protected var errorQueue: Queue<ExponentError> = LinkedList()
636   }
637 }
638