1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.expoview
3 
4 import android.app.Activity
5 import android.app.Application
6 import android.content.Context
7 import android.content.Intent
8 import android.net.Uri
9 import android.os.Handler
10 import android.os.Looper
11 import android.os.StrictMode
12 import android.os.StrictMode.ThreadPolicy
13 import android.os.UserManager
14 import com.facebook.common.internal.ByteStreams
15 import com.facebook.drawee.backends.pipeline.Fresco
16 import com.facebook.stetho.Stetho
17 import com.raizlabs.android.dbflow.config.DatabaseConfig
18 import com.raizlabs.android.dbflow.config.FlowConfig
19 import com.raizlabs.android.dbflow.config.FlowManager
20 import expo.modules.core.interfaces.Package
21 import expo.modules.core.interfaces.SingletonModule
22 import expo.modules.manifests.core.Manifest
23 import host.exp.exponent.*
24 import host.exp.exponent.analytics.Analytics
25 import host.exp.exponent.analytics.EXL
26 import host.exp.exponent.di.NativeModuleDepsProvider
27 import host.exp.exponent.kernel.ExponentUrls
28 import host.exp.exponent.kernel.KernelConstants
29 import host.exp.exponent.network.ExpoHttpCallback
30 import host.exp.exponent.network.ExpoResponse
31 import host.exp.exponent.network.ExponentHttpClient.SafeCallback
32 import host.exp.exponent.network.ExponentNetwork
33 import host.exp.exponent.notifications.ActionDatabase
34 import host.exp.exponent.notifications.managers.SchedulersDatabase
35 import host.exp.exponent.storage.ExponentDB
36 import host.exp.exponent.storage.ExponentSharedPreferences
37 import okhttp3.*
38 import org.apache.commons.io.IOUtils
39 import org.apache.commons.io.output.ByteArrayOutputStream
40 import org.apache.commons.io.output.TeeOutputStream
41 import org.json.JSONArray
42 import versioned.host.exp.exponent.ExponentPackageDelegate
43 import java.io.*
44 import java.net.URLEncoder
45 import java.util.concurrent.CopyOnWriteArrayList
46 import javax.inject.Inject
47 
48 class Exponent private constructor(val context: Context, val application: Application) {
49   var currentActivity: Activity? = null
50 
51   private val bundleStrings = mutableMapOf<String, String>()
52 
53   fun getBundleSource(path: String): String? {
54     synchronized(bundleStrings) {
55       return bundleStrings.remove(path)
56     }
57   }
58 
59   @Inject
60   lateinit var exponentNetwork: ExponentNetwork
61 
62   @Inject
63   lateinit var exponentManifest: ExponentManifest
64 
65   @Inject
66   lateinit var exponentSharedPreferences: ExponentSharedPreferences
67 
68   @Inject
69   lateinit var expoHandler: ExpoHandler
70 
71   fun runOnUiThread(action: Runnable) {
72     if (Thread.currentThread() !== Looper.getMainLooper().thread) {
73       Handler(context.mainLooper).post(action)
74     } else {
75       action.run()
76     }
77   }
78 
79   private val activityResultListeners = CopyOnWriteArrayList<ActivityResultListener>()
80 
81   data class InstanceManagerBuilderProperties(
82     var application: Application?,
83     var jsBundlePath: String?,
84     var experienceProperties: Map<String, Any?>,
85     var expoPackages: List<Package>?,
86     var exponentPackageDelegate: ExponentPackageDelegate?,
87     var manifest: Manifest,
88     var singletonModules: List<SingletonModule>,
89   )
90 
91   fun addActivityResultListener(listener: ActivityResultListener) {
92     activityResultListeners.add(listener)
93   }
94 
95   fun removeActivityResultListener(listener: ActivityResultListener) {
96     activityResultListeners.remove(listener)
97   }
98 
99   fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
100     for (listener in activityResultListeners) {
101       listener.onActivityResult(requestCode, resultCode, data)
102     }
103   }
104 
105   /*
106    * Bundle loading
107    */
108   interface BundleListener {
109     fun onBundleLoaded(localBundlePath: String)
110     fun onError(e: Exception)
111   }
112 
113   // `id` must be URL encoded. Returns true if found cached bundle.
114   @JvmOverloads
115   fun loadJSBundle(
116     manifest: Manifest?,
117     urlString: String,
118     id: String,
119     abiVersion: String,
120     bundleListener: BundleListener,
121     shouldForceNetworkArg: Boolean = false,
122     shouldForceCache: Boolean = false
123   ): Boolean {
124     var shouldForceNetwork = shouldForceNetworkArg
125     if (id != KernelConstants.KERNEL_BUNDLE_ID) {
126       Analytics.markEvent(Analytics.TimedEvent.STARTED_FETCHING_BUNDLE)
127     }
128     val isDeveloping = manifest?.isDevelopmentMode() ?: false
129     if (isDeveloping) {
130       // This is important for running locally with no-dev
131       shouldForceNetwork = true
132     }
133 
134     // The bundle is cached in two places:
135     //   1. The OkHttp cache (which lives in internal storage)
136     //   2. Written to our own file (in cache dir)
137     // Ideally we'd take the OkHttp response and send the InputStream directly to RN but RN doesn't
138     // support that right now so we need to write the response to a file.
139     // getCacheDir() doesn't work here! Some phones clean the file up in between when we check
140     // file.exists() and when we feed it into React Native!
141     // TODO: clean up files here!
142     val fileName =
143       KernelConstants.BUNDLE_FILE_PREFIX + id + urlString.hashCode().toString() + '-' + abiVersion
144     val directory = File(context.filesDir, abiVersion)
145     if (!directory.exists()) {
146       directory.mkdir()
147     }
148 
149     try {
150       val requestBuilder = if (KernelConstants.KERNEL_BUNDLE_ID == id) {
151         // TODO(eric): remove once home bundle is loaded normally
152         ExponentUrls.addExponentHeadersToUrl(urlString)
153       } else {
154         Request.Builder().url(urlString)
155       }
156       if (shouldForceNetwork) {
157         requestBuilder.cacheControl(CacheControl.FORCE_NETWORK)
158       }
159       val request = requestBuilder.build()
160 
161       // Use OkHttpClient with long read timeout for dev bundles
162       val callback: SafeCallback = object : SafeCallback {
163         override fun onFailure(e: IOException) {
164           bundleListener.onError(e)
165         }
166 
167         override fun onResponse(response: ExpoResponse) {
168           if (!response.isSuccessful) {
169             var body = "(could not render body)"
170             try {
171               body = response.body().string()
172             } catch (e: IOException) {
173               EXL.e(TAG, e)
174             }
175             bundleListener.onError(
176               Exception(
177                 "Bundle return code: " + response.code() +
178                   ". With body: " + body
179               )
180             )
181             return
182           }
183 
184           if (id != KernelConstants.KERNEL_BUNDLE_ID) {
185             Analytics.markEvent(Analytics.TimedEvent.FINISHED_FETCHING_BUNDLE)
186           }
187 
188           try {
189             if (id != KernelConstants.KERNEL_BUNDLE_ID) {
190               Analytics.markEvent(Analytics.TimedEvent.STARTED_WRITING_BUNDLE)
191             }
192 
193             val sourceFile = File(directory, fileName)
194 
195             var hasCachedSourceFile = false
196             val networkResponse = response.networkResponse()
197             if (networkResponse == null || networkResponse.code() == KernelConstants.HTTP_NOT_MODIFIED) {
198               // If we're getting a cached response don't rewrite the file to disk.
199               EXL.d(TAG, "Got cached OkHttp response for $urlString")
200               if (sourceFile.exists()) {
201                 hasCachedSourceFile = true
202                 EXL.d(TAG, "Have cached source file for $urlString")
203               }
204             }
205 
206             if (!hasCachedSourceFile) {
207               var inputStream: InputStream? = null
208               var fileOutputStream: FileOutputStream? = null
209               var byteArrayOutputStream: ByteArrayOutputStream? = null
210               var teeOutputStream: TeeOutputStream? = null
211               try {
212                 EXL.d(TAG, "Do not have cached source file for $urlString")
213                 inputStream = response.body().byteStream()
214                 fileOutputStream = FileOutputStream(sourceFile)
215                 byteArrayOutputStream = ByteArrayOutputStream()
216 
217                 // Multiplex the stream. Write both to file and string.
218                 teeOutputStream = TeeOutputStream(fileOutputStream, byteArrayOutputStream)
219                 ByteStreams.copy(inputStream, teeOutputStream)
220                 teeOutputStream.flush()
221                 bundleStrings[sourceFile.absolutePath] = byteArrayOutputStream.toString()
222                 fileOutputStream.flush()
223                 fileOutputStream.fd.sync()
224               } finally {
225                 IOUtils.closeQuietly(teeOutputStream)
226                 IOUtils.closeQuietly(fileOutputStream)
227                 IOUtils.closeQuietly(byteArrayOutputStream)
228                 IOUtils.closeQuietly(inputStream)
229               }
230             }
231 
232             if (id != KernelConstants.KERNEL_BUNDLE_ID) {
233               Analytics.markEvent(Analytics.TimedEvent.FINISHED_WRITING_BUNDLE)
234             }
235 
236             if (Constants.WRITE_BUNDLE_TO_LOG) {
237               printSourceFile(sourceFile.absolutePath)
238             }
239 
240             expoHandler.post { bundleListener.onBundleLoaded(sourceFile.absolutePath) }
241           } catch (e: Exception) {
242             bundleListener.onError(e)
243           }
244         }
245 
246         override fun onCachedResponse(response: ExpoResponse, isEmbedded: Boolean) {
247           EXL.d(TAG, "Using cached or embedded response.")
248           onResponse(response)
249         }
250       }
251 
252       exponentNetwork.longTimeoutClient.apply {
253         when {
254           shouldForceCache -> tryForcedCachedResponse(
255             request.url().toString(),
256             request,
257             callback,
258             null,
259             null
260           )
261           shouldForceNetwork -> callSafe(request, callback)
262           else -> callDefaultCache(request, callback)
263         }
264       }
265     } catch (e: Exception) {
266       bundleListener.onError(e)
267     }
268 
269     // Guess whether we'll use the cache based on whether the source file is saved.
270     val sourceFile = File(directory, fileName)
271     return sourceFile.exists()
272   }
273 
274   private fun printSourceFile(path: String) {
275     EXL.d(KernelConstants.BUNDLE_TAG, "Printing bundle:")
276     val inputStream = try {
277       FileInputStream(path)
278     } catch (e: Exception) {
279       EXL.e(KernelConstants.BUNDLE_TAG, e.toString())
280       return
281     }
282     inputStream.bufferedReader().useLines { lines ->
283       lines.forEach { line -> EXL.d(KernelConstants.BUNDLE_TAG, line) }
284     }
285   }
286 
287   interface PackagerStatusCallback {
288     fun onSuccess()
289     fun onFailure(errorMessage: String)
290   }
291 
292   fun testPackagerStatus(
293     isDebug: Boolean,
294     mManifest: Manifest,
295     callback: PackagerStatusCallback
296   ) {
297     if (!isDebug) {
298       callback.onSuccess()
299       return
300     }
301 
302     val debuggerHost = mManifest.getDebuggerHost()
303     exponentNetwork.noCacheClient.newCall(
304       Request.Builder().url("http://$debuggerHost/status").build()
305     ).enqueue(object : Callback {
306       override fun onFailure(call: Call, e: IOException) {
307         EXL.d(TAG, e.toString())
308         callback.onFailure("Packager is not running at http://$debuggerHost")
309       }
310 
311       @Throws(IOException::class)
312       override fun onResponse(call: Call, response: Response) {
313         val responseString = response.body()!!.string()
314         if (responseString.contains(PACKAGER_RUNNING)) {
315           runOnUiThread { callback.onSuccess() }
316         } else {
317           callback.onFailure("Packager is not running at http://$debuggerHost")
318         }
319       }
320     })
321   }
322 
323   interface StartReactInstanceDelegate {
324     val isDebugModeEnabled: Boolean
325     val isInForeground: Boolean
326     val exponentPackageDelegate: ExponentPackageDelegate?
327     fun handleUnreadNotifications(unreadNotifications: JSONArray)
328   }
329 
330   companion object {
331     private val TAG = Exponent::class.java.simpleName
332 
333     private const val PACKAGER_RUNNING = "running"
334 
335     @JvmStatic lateinit var instance: Exponent
336       private set
337     private var hasBeenInitialized = false
338 
339     @JvmStatic fun initialize(context: Context, application: Application) {
340       if (!hasBeenInitialized) {
341         hasBeenInitialized = true
342         Exponent(context, application)
343       }
344     }
345 
346     @Throws(UnsupportedEncodingException::class)
347     fun encodeExperienceId(manifestId: String): String {
348       return URLEncoder.encode("experience-$manifestId", "UTF-8")
349     }
350 
351     fun getPort(urlArg: String): Int {
352       var url = urlArg
353       if (!url.contains("://")) {
354         url = "http://$url"
355       }
356       val uri = Uri.parse(url)
357       val port = uri.port
358       return if (port == -1) {
359         80
360       } else {
361         port
362       }
363     }
364 
365     fun getHostname(urlArg: String): String? {
366       var url = urlArg
367       if (!url.contains("://")) {
368         url = "http://$url"
369       }
370       val uri = Uri.parse(url)
371       return uri.host
372     }
373 
374     @JvmStatic fun enableDeveloperSupport(
375       debuggerHost: String,
376       mainModuleName: String,
377       builder: RNObject
378     ) {
379       if (debuggerHost.isEmpty() || mainModuleName.isEmpty()) {
380         return
381       }
382 
383       try {
384         val fieldObject = RNObject("com.facebook.react.modules.systeminfo.AndroidInfoHelpers")
385         fieldObject.loadVersion(builder.version())
386 
387         val debuggerHostHostname = getHostname(debuggerHost)
388         val debuggerHostPort = getPort(debuggerHost)
389 
390         val deviceField = fieldObject.rnClass()!!.getDeclaredField("DEVICE_LOCALHOST")
391         deviceField.isAccessible = true
392         deviceField[null] = debuggerHostHostname
393 
394         val genymotionField = fieldObject.rnClass()!!.getDeclaredField("GENYMOTION_LOCALHOST")
395         genymotionField.isAccessible = true
396         genymotionField[null] = debuggerHostHostname
397 
398         val emulatorField = fieldObject.rnClass()!!.getDeclaredField("EMULATOR_LOCALHOST")
399         emulatorField.isAccessible = true
400         emulatorField[null] = debuggerHostHostname
401 
402         fieldObject.callStatic("setDevServerPort", debuggerHostPort)
403         fieldObject.callStatic("setInspectorProxyPort", debuggerHostPort)
404 
405         builder.callRecursive("setUseDeveloperSupport", true)
406         builder.callRecursive("setJSMainModulePath", mainModuleName)
407       } catch (e: IllegalAccessException) {
408         e.printStackTrace()
409       } catch (e: NoSuchFieldException) {
410         e.printStackTrace()
411       }
412     }
413   }
414 
415   init {
416     instance = this
417     NativeModuleDepsProvider.initialize(application)
418     NativeModuleDepsProvider.instance.inject(Exponent::class.java, this)
419 
420     // Verifying SSL certs is slow on Android, so send an HTTPS request to our server as early as possible.
421     // This speeds up the manifest request in a shell app from ~500ms to ~250ms.
422     try {
423       exponentNetwork.client.call(
424         Request.Builder().url(Constants.API_HOST + "/status").build(),
425         object : ExpoHttpCallback {
426           override fun onFailure(e: IOException) {
427             EXL.d(TAG, e.toString())
428           }
429 
430           @Throws(IOException::class)
431           override fun onResponse(response: ExpoResponse) {
432             ExponentNetwork.flushResponse(response)
433             EXL.d(TAG, "Loaded exp.host status page.")
434           }
435         }
436       )
437     } catch (e: Throwable) {
438       EXL.e(TAG, e)
439     }
440 
441     // Fixes Android memory leak
442     try {
443       UserManager::class.java.getMethod("get", Context::class.java).invoke(null, context)
444     } catch (e: Throwable) {
445       EXL.testError(e)
446     }
447 
448     try {
449       Fresco.initialize(context)
450     } catch (e: RuntimeException) {
451       EXL.testError(e)
452     }
453 
454     // Amplitude
455     Analytics.initializeAmplitude(context, application)
456 
457     // TODO: profile this
458     FlowManager.init(
459       FlowConfig.builder(context)
460         .addDatabaseConfig(
461           DatabaseConfig.builder(SchedulersDatabase::class.java)
462             .databaseName(SchedulersDatabase.NAME)
463             .build()
464         )
465         .addDatabaseConfig(
466           DatabaseConfig.builder(ActionDatabase::class.java)
467             .databaseName(ActionDatabase.NAME)
468             .build()
469         )
470         .addDatabaseConfig(
471           DatabaseConfig.builder(ExponentDB::class.java)
472             .databaseName(ExponentDB.NAME)
473             .build()
474         )
475         .build()
476     )
477 
478     if (ExpoViewBuildConfig.DEBUG) {
479       Stetho.initializeWithDefaults(context)
480     }
481 
482     if (!ExpoViewBuildConfig.DEBUG) {
483       // There are a few places in RN code that throw NetworkOnMainThreadException.
484       // WebsocketJavaScriptExecutor.connectInternal closes a websocket on the main thread.
485       // Shouldn't actually block the ui since it's fire and forget so not high priority to fix the root cause.
486       val policy = ThreadPolicy.Builder().permitAll().build()
487       StrictMode.setThreadPolicy(policy)
488     }
489   }
490 }
491