1 package host.exp.exponent.headless 2 3 import android.app.Application 4 import android.content.Context 5 import android.net.Uri 6 import android.util.SparseArray 7 import com.facebook.react.ReactPackage 8 import com.facebook.react.bridge.UiThreadUtil 9 import com.facebook.soloader.SoLoader 10 import expo.modules.adapters.react.ReactModuleRegistryProvider 11 import expo.modules.apploader.AppLoaderPackagesProviderInterface 12 import expo.modules.apploader.AppLoaderProvider 13 import expo.modules.core.interfaces.Package 14 import expo.modules.core.interfaces.SingletonModule 15 import expo.modules.manifests.core.Manifest 16 import host.exp.exponent.Constants 17 import host.exp.exponent.ExpoUpdatesAppLoader 18 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 19 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 20 import host.exp.exponent.ExponentManifest 21 import host.exp.exponent.RNObject 22 import host.exp.exponent.experience.DetachedModuleRegistryAdapter 23 import host.exp.exponent.kernel.ExponentUrls 24 import host.exp.exponent.kernel.KernelConstants 25 import host.exp.exponent.storage.ExponentDB 26 import host.exp.exponent.storage.ExponentDBObject 27 import host.exp.exponent.taskManager.AppLoaderInterface 28 import host.exp.exponent.taskManager.AppRecordInterface 29 import host.exp.exponent.utils.AsyncCondition 30 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener 31 import host.exp.exponent.utils.ExpoActivityIds 32 import host.exp.expoview.Exponent 33 import host.exp.expoview.Exponent.InstanceManagerBuilderProperties 34 import host.exp.expoview.Exponent.StartReactInstanceDelegate 35 import org.json.JSONArray 36 import org.json.JSONException 37 import org.json.JSONObject 38 import versioned.host.exp.exponent.ExponentPackage 39 import versioned.host.exp.exponent.ExponentPackageDelegate 40 import versioned.host.exp.exponent.modules.universal.ExpoModuleRegistryAdapter 41 42 // @tsapeta: Most parts of this class was just copied from ReactNativeActivity and ExperienceActivity, 43 // however it allows launching apps in the background, without the activity. 44 // I've found it pretty hard to make just one implementation that can be used in both cases, 45 // so I decided to go with a copy until we refactor these activity classes. 46 47 class InternalHeadlessAppLoader(private val context: Context) : 48 AppLoaderInterface, 49 StartReactInstanceDelegate, 50 ExponentPackageDelegate { 51 52 private var manifest: Manifest? = null 53 private var manifestUrl: String? = null 54 private var sdkVersion: String? = null 55 private var detachSdkVersion: String? = null 56 private var reactInstanceManager: RNObject? = RNObject("com.facebook.react.ReactInstanceManager") 57 private val intentUri: String? = null 58 private var isReadyForBundle = false 59 private var jsBundlePath: String? = null 60 private var appRecord: HeadlessAppRecord? = null 61 private var callback: AppLoaderProvider.Callback? = null 62 private var activityId = 0 63 loadAppnull64 override fun loadApp( 65 appUrl: String, 66 options: Map<String, Any>, 67 callback: AppLoaderProvider.Callback 68 ): AppRecordInterface { 69 manifestUrl = appUrl 70 appRecord = HeadlessAppRecord() 71 this.callback = callback 72 activityId = ExpoActivityIds.getNextHeadlessActivityId() 73 74 ExpoUpdatesAppLoader( 75 manifestUrl!!, 76 object : AppLoaderCallback { 77 override fun onOptimisticManifest(optimisticManifest: Manifest) {} 78 override fun onManifestCompleted(manifest: Manifest) { 79 Exponent.instance.runOnUiThread { 80 try { 81 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 82 activityIdToBundleUrl.put(activityId, bundleUrl) 83 setManifest(manifestUrl!!, manifest, bundleUrl) 84 } catch (e: JSONException) { 85 this@InternalHeadlessAppLoader.callback!!.onComplete(false, Exception(e.message)) 86 } 87 } 88 } 89 90 override fun onBundleCompleted(localBundlePath: String) { 91 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 92 } 93 94 override fun emitEvent(params: JSONObject) {} 95 override fun updateStatus(status: AppLoaderStatus) {} 96 override fun onError(e: Exception) { 97 Exponent.instance.runOnUiThread { this@InternalHeadlessAppLoader.callback!!.onComplete(false, Exception(e.message)) } 98 } 99 }, 100 true 101 ).start(context) 102 103 return appRecord!! 104 } 105 setManifestnull106 private fun setManifest(manifestUrl: String, manifest: Manifest, bundleUrl: String?) { 107 this.manifestUrl = manifestUrl 108 this.manifest = manifest 109 sdkVersion = manifest.getExpoGoSDKVersion() 110 111 // Notifications logic uses this to determine which experience to route a notification to 112 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl!!)) 113 114 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 115 // to point to the unversioned code in ReactAndroid. 116 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 117 sdkVersion = RNObject.UNVERSIONED 118 } 119 120 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 121 122 if (RNObject.UNVERSIONED != sdkVersion) { 123 var isValidVersion = false 124 for (version in Constants.SDK_VERSIONS_LIST) { 125 if (version == sdkVersion) { 126 isValidVersion = true 127 break 128 } 129 } 130 if (!isValidVersion) { 131 callback!!.onComplete(false, Exception("$sdkVersion is not a valid SDK version.")) 132 return 133 } 134 } 135 136 soLoaderInit() 137 138 UiThreadUtil.runOnUiThread { 139 if (reactInstanceManager!!.isNotNull) { 140 reactInstanceManager!!.onHostDestroy() 141 reactInstanceManager!!.assign(null) 142 } 143 if (isDebugModeEnabled) { 144 jsBundlePath = "" 145 startReactInstance() 146 } else { 147 isReadyForBundle = true 148 AsyncCondition.notify(READY_FOR_BUNDLE) 149 } 150 } 151 } 152 setBundlenull153 private fun setBundle(localBundlePath: String) { 154 if (!isDebugModeEnabled) { 155 AsyncCondition.wait( 156 READY_FOR_BUNDLE, 157 object : AsyncConditionListener { 158 override fun isReady(): Boolean { 159 return isReadyForBundle 160 } 161 162 override fun execute() { 163 jsBundlePath = localBundlePath 164 startReactInstance() 165 AsyncCondition.remove(READY_FOR_BUNDLE) 166 } 167 } 168 ) 169 } 170 } 171 172 override val isDebugModeEnabled: Boolean 173 get() = manifest?.isDevelopmentMode() ?: false 174 soLoaderInitnull175 private fun soLoaderInit() { 176 if (detachSdkVersion != null) { 177 SoLoader.init(context, false) 178 } 179 } 180 181 // Override reactPackagesnull182 private fun reactPackages(): List<ReactPackage?>? { 183 return if (!Constants.isStandaloneApp()) { 184 // Pass null if it's on Expo Go. In that case packages from ExperiencePackagePicker will be used instead. 185 null 186 } else try { 187 (context.applicationContext as AppLoaderPackagesProviderInterface<ReactPackage?>).packages 188 } catch (e: ClassCastException) { 189 e.printStackTrace() 190 null 191 } 192 } 193 194 // Override expoPackagesnull195 fun expoPackages(): List<Package>? { 196 return if (!Constants.isStandaloneApp()) { 197 // Pass null if it's on Expo Go. In that case packages from ExperiencePackagePicker will be used instead. 198 null 199 } else try { 200 (context.applicationContext as AppLoaderPackagesProviderInterface<*>).expoPackages 201 } catch (e: ClassCastException) { 202 e.printStackTrace() 203 null 204 } 205 } 206 207 //region StartReactInstanceDelegate 208 override val isInForeground: Boolean = false 209 override val exponentPackageDelegate: ExponentPackageDelegate = this 210 handleUnreadNotificationsnull211 override fun handleUnreadNotifications(unreadNotifications: JSONArray) {} 212 213 //endregion startReactInstancenull214 private fun startReactInstance() { 215 Exponent.instance.testPackagerStatus( 216 isDebugModeEnabled, 217 manifest!!, 218 object : Exponent.PackagerStatusCallback { 219 override fun onSuccess() { 220 reactInstanceManager = startReactInstance( 221 this@InternalHeadlessAppLoader, 222 intentUri, 223 detachSdkVersion, 224 reactPackages(), 225 expoPackages() 226 ) 227 } 228 229 override fun onFailure(errorMessage: String) { 230 callback!!.onComplete(false, Exception(errorMessage)) 231 } 232 } 233 ) 234 } 235 startReactInstancenull236 private fun startReactInstance( 237 delegate: StartReactInstanceDelegate, 238 mIntentUri: String?, 239 mSDKVersion: String?, 240 extraNativeModules: List<ReactPackage?>?, 241 extraExpoPackages: List<Package>? 242 ): RNObject? { 243 val experienceProperties = mapOf( 244 KernelConstants.MANIFEST_URL_KEY to manifestUrl, 245 KernelConstants.LINKING_URI_KEY to linkingUri, 246 KernelConstants.INTENT_URI_KEY to mIntentUri 247 ) 248 val instanceManagerBuilderProperties = InstanceManagerBuilderProperties( 249 application = context as Application, 250 jsBundlePath = jsBundlePath, 251 experienceProperties = experienceProperties, 252 expoPackages = extraExpoPackages, 253 exponentPackageDelegate = delegate.exponentPackageDelegate, 254 manifest = manifest!!, 255 singletonModules = ExponentPackage.getOrCreateSingletonModules(context, manifest, extraExpoPackages), 256 ) 257 258 val versionedUtils = RNObject("host.exp.exponent.VersionedUtils").loadVersion(mSDKVersion!!) 259 val builder = versionedUtils.callRecursive( 260 "getReactInstanceManagerBuilder", 261 instanceManagerBuilderProperties 262 )!! 263 264 // Since there is no activity to be attached, we cannot set ReactInstanceManager state to RESUMED, so we opt to BEFORE_RESUME 265 builder.call( 266 "setInitialLifecycleState", 267 RNObject.versionedEnum( 268 mSDKVersion, 269 "com.facebook.react.common.LifecycleState", 270 "BEFORE_RESUME" 271 ) 272 ) 273 274 if (extraNativeModules != null) { 275 for (nativeModule in extraNativeModules) { 276 builder.call("addPackage", nativeModule) 277 } 278 } 279 280 if (delegate.isDebugModeEnabled) { 281 val debuggerHost = manifest!!.getDebuggerHost() 282 val mainModuleName = manifest!!.getMainModuleName() 283 Exponent.enableDeveloperSupport(debuggerHost, mainModuleName, builder) 284 } 285 286 val reactInstanceManager = builder.callRecursive("build") 287 val devSupportManager = reactInstanceManager!!.callRecursive("getDevSupportManager") 288 if (devSupportManager != null) { 289 val devSettings = devSupportManager.callRecursive("getDevSettings") 290 devSettings?.setField("exponentActivityId", activityId) 291 } 292 reactInstanceManager?.call("createReactContextInBackground") 293 294 // keep a reference in app record, so it can be invalidated through AppRecord.invalidate() 295 appRecord!!.setReactInstanceManager(reactInstanceManager) 296 callback!!.onComplete(true, null) 297 298 return reactInstanceManager 299 } 300 301 // deprecated in favor of Expo.Linking.makeUrl 302 // TODO: remove this 303 private val linkingUri: String? 304 get() = if (Constants.SHELL_APP_SCHEME != null) { 305 Constants.SHELL_APP_SCHEME + "://" 306 } else { 307 val uri = Uri.parse(manifestUrl) 308 val host = uri.host 309 if (host != null && ( 310 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 311 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 312 ".expo.test" 313 ) 314 ) 315 ) { 316 val pathSegments = uri.pathSegments 317 val builder = uri.buildUpon() 318 builder.path(null) 319 for (segment in pathSegments) { 320 if (ExponentManifest.DEEP_LINK_SEPARATOR == segment) { 321 break 322 } 323 builder.appendEncodedPath(segment) 324 } 325 builder.appendEncodedPath(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH).build() 326 .toString() 327 } else { 328 manifestUrl 329 } 330 } 331 getScopedModuleRegistryAdapterForPackagesnull332 override fun getScopedModuleRegistryAdapterForPackages( 333 packages: List<Package>, 334 singletonModules: List<SingletonModule> 335 ): ExpoModuleRegistryAdapter? { 336 return if (Constants.isStandaloneApp()) { 337 DetachedModuleRegistryAdapter( 338 ReactModuleRegistryProvider( 339 packages, 340 singletonModules 341 ) 342 ) 343 } else { 344 null 345 } 346 } 347 348 companion object { 349 private const val READY_FOR_BUNDLE = "headlessAppReadyForBundle" 350 351 private val activityIdToBundleUrl = SparseArray<String>() 352 hasBundleUrlForActivityIdnull353 fun hasBundleUrlForActivityId(activityId: Int): Boolean { 354 return activityId < -1 && activityIdToBundleUrl[activityId] != null 355 } 356 getBundleUrlForActivityIdnull357 fun getBundleUrlForActivityId(activityId: Int): String? { 358 return activityIdToBundleUrl[activityId] 359 } 360 } 361 } 362