1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.experience
3 
4 import android.content.Intent
5 import android.content.res.Configuration
6 import android.os.Bundle
7 import com.facebook.drawee.backends.pipeline.Fresco
8 import de.greenrobot.event.EventBus
9 import host.exp.exponent.Constants
10 import host.exp.exponent.RNObject
11 import host.exp.exponent.di.NativeModuleDepsProvider
12 import host.exp.exponent.kernel.*
13 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage
14 import host.exp.exponent.utils.AsyncCondition
15 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
16 import host.exp.expoview.Exponent
17 import javax.inject.Inject
18 
19 abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
20   abstract class ExperienceEvent internal constructor(val experienceKey: ExperienceKey)
21 
22   class ExperienceForegroundedEvent internal constructor(experienceKey: ExperienceKey) :
23     ExperienceEvent(experienceKey)
24 
25   class ExperienceBackgroundedEvent internal constructor(experienceKey: ExperienceKey) :
26     ExperienceEvent(experienceKey)
27 
28   class ExperienceContentLoaded(experienceKey: ExperienceKey) : ExperienceEvent(experienceKey)
29 
30   @Inject
31   protected lateinit var kernel: Kernel
32 
33   private var onResumeTime: Long = 0
34 
onCreatenull35   override fun onCreate(savedInstanceState: Bundle?) {
36     super.onCreate(savedInstanceState)
37     isInForeground = true
38     reactRootView = RNObject("com.facebook.react.ReactRootView")
39     NativeModuleDepsProvider.instance.inject(BaseExperienceActivity::class.java, this)
40   }
41 
onResumenull42   override fun onResume() {
43     super.onResume()
44     kernel.activityContext = this
45     Exponent.instance.currentActivity = this
46     visibleActivity = this
47 
48     // Consume any errors that happened before onResume
49     consumeErrorQueue()
50     isInForeground = true
51     onResumeTime = System.currentTimeMillis()
52     AsyncCondition.wait(
53       KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY,
54       object : AsyncConditionListener {
55         override fun isReady(): Boolean {
56           return experienceKey != null || this@BaseExperienceActivity is HomeActivity
57         }
58 
59         override fun execute() {
60           EventBus.getDefault().post(
61             ExperienceForegroundedEvent(
62               experienceKey!!
63             )
64           )
65         }
66       }
67     )
68   }
69 
onPausenull70   override fun onPause() {
71     if (experienceKey != null) {
72       EventBus.getDefault().post(ExperienceBackgroundedEvent(experienceKey!!))
73     }
74     super.onPause()
75 
76     // For some reason onPause sometimes gets called soon after onResume.
77     // One symptom of this is that ReactNativeActivity.startReactInstance will
78     // see isInForeground == false and not start the app.
79     // 500ms should be very safe. The average time between onResume and
80     // onPause when the bug happens is around 10ms.
81     // This seems to happen when foregrounding the app after pressing on a notification.
82     // Unclear if this is because of something we're doing during the initialization process
83     // or just an OS quirk.
84     val timeSinceOnResume = System.currentTimeMillis() - onResumeTime
85     if (timeSinceOnResume > 500) {
86       isInForeground = false
87       if (visibleActivity === this) {
88         visibleActivity = null
89       }
90     }
91   }
92 
onBackPressednull93   override fun onBackPressed() {
94     if (reactInstanceManager.isNotNull && !isCrashed) {
95       reactInstanceManager.call("onBackPressed")
96     } else {
97       moveTaskToBack(true)
98     }
99   }
100 
invokeDefaultOnBackPressednull101   override fun invokeDefaultOnBackPressed() {
102     moveTaskToBack(true)
103   }
104 
onDestroynull105   override fun onDestroy() {
106     super.onDestroy()
107     if (this is HomeActivity) {
108       // Don't want to trash the kernel instance
109       return
110     }
111 
112     if (reactInstanceManager.isNotNull) {
113       reactInstanceManager.onHostDestroy()
114       reactInstanceManager.assign(null)
115     }
116     reactRootView.assign(null)
117 
118     // Fresco leaks ReactApplicationContext
119     Fresco.initialize(applicationContext)
120 
121     // TODO: OkHttpClientProvider leaks Activity. Clean it up.
122   }
123 
onConfigurationChangednull124   override fun onConfigurationChanged(newConfig: Configuration) {
125     super.onConfigurationChanged(newConfig)
126     if (reactInstanceManager.isNotNull && !isCrashed) {
127       reactInstanceManager.call("onConfigurationChanged", this, newConfig)
128     }
129   }
130 
consumeErrorQueuenull131   protected fun consumeErrorQueue() {
132     if (errorQueue.isEmpty()) {
133       return
134     }
135     runOnUiThread {
136       if (errorQueue.isEmpty()) {
137         return@runOnUiThread
138       }
139       val (isFatal, errorMessage, errorHeader) = sendErrorsToErrorActivity()
140       if (!shouldShowErrorScreen(errorMessage)) {
141         return@runOnUiThread
142       }
143       if (!isFatal) {
144         return@runOnUiThread
145       }
146 
147       // we don't ever want to show any Expo UI in a production standalone app
148       // so hard crash in this case
149       if (Constants.isStandaloneApp() && !isDebugModeEnabled) {
150         throw RuntimeException("Expo encountered a fatal error: " + errorMessage.developerErrorMessage())
151       }
152       if (!isDebugModeEnabled) {
153         removeAllViewsFromContainer()
154         reactInstanceManager.assign(null)
155         reactRootView.assign(null)
156       }
157       isCrashed = true
158       isLoading = false
159       val intent = Intent(this@BaseExperienceActivity, ErrorActivity::class.java).apply {
160         addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
161       }
162       onError(intent)
163       intent.apply {
164         putExtra(ErrorActivity.DEBUG_MODE_KEY, isDebugModeEnabled)
165         putExtra(ErrorActivity.ERROR_HEADER_KEY, errorHeader)
166         putExtra(ErrorActivity.USER_ERROR_MESSAGE_KEY, errorMessage.userErrorMessage())
167         putExtra(
168           ErrorActivity.DEVELOPER_ERROR_MESSAGE_KEY,
169           errorMessage.developerErrorMessage()
170         )
171       }
172       startActivity(intent)
173       EventBus.getDefault().post(ExperienceDoneLoadingEvent(this))
174     }
175   }
176 
177   // Override
178   override val isDebugModeEnabled: Boolean = false
179 
180   // Override
onErrornull181   protected open fun onError(intent: Intent) {
182     // Modify intent used to start ErrorActivity
183   }
184 
185   companion object {
186     private val TAG = BaseExperienceActivity::class.java.simpleName
187 
188     // TODO: kill. just use Exponent class's activity
189     var visibleActivity: BaseExperienceActivity? = null
190       private set
191 
addErrornull192     fun addError(error: ExponentError) {
193       errorQueue.add(error)
194       if (visibleActivity != null) {
195         visibleActivity!!.consumeErrorQueue()
196       } else if (ErrorActivity.visibleActivity != null) {
197         // If ErrorActivity is already started and we get another error from RN.
198         sendErrorsToErrorActivity()
199       }
200       // Otherwise onResume will consumeErrorQueue
201     }
202 
sendErrorsToErrorActivitynull203     private fun sendErrorsToErrorActivity(): Triple<Boolean, ExponentErrorMessage, String?> {
204       var isFatal = false
205       var errorMessage = developerErrorMessage("")
206       var errorHeader: String? = null
207       synchronized(errorQueue) {
208         while (!errorQueue.isEmpty()) {
209           val error = errorQueue.remove()
210           ErrorActivity.addError(error)
211 
212           // Just use the last error message for now, is there a better way to do this?
213           errorMessage = error.errorMessage
214           errorHeader = error.errorHeader
215           if (error.isFatal) {
216             isFatal = true
217           }
218         }
219       }
220       return Triple(isFatal, errorMessage, errorHeader)
221     }
222   }
223 }
224