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