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