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