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