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.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
17 import com.facebook.imagepipeline.producers.HttpUrlConnectionNetworkFetcher
18 import com.raizlabs.android.dbflow.config.DatabaseConfig
19 import com.raizlabs.android.dbflow.config.FlowConfig
20 import com.raizlabs.android.dbflow.config.FlowManager
21 import expo.modules.core.interfaces.Package
22 import expo.modules.core.interfaces.SingletonModule
23 import expo.modules.manifests.core.Manifest
24 import host.exp.exponent.*
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.kernel.KernelNetworkInterceptor
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 java.util.concurrent.TimeUnit
47 import javax.inject.Inject
48 
49 class Exponent private constructor(val context: Context, val application: Application) {
50   var currentActivity: Activity? = null
51 
52   private val bundleStrings = mutableMapOf<String, String>()
53 
54   fun getBundleSource(path: String): String? {
55     synchronized(bundleStrings) {
56       return bundleStrings.remove(path)
57     }
58   }
59 
60   @Inject
61   lateinit var exponentNetwork: ExponentNetwork
62 
63   @Inject
64   lateinit var exponentManifest: ExponentManifest
65 
66   @Inject
67   lateinit var exponentSharedPreferences: ExponentSharedPreferences
68 
69   @Inject
70   lateinit var expoHandler: ExpoHandler
71 
72   fun runOnUiThread(action: Runnable) {
73     if (Thread.currentThread() !== Looper.getMainLooper().thread) {
74       Handler(context.mainLooper).post(action)
75     } else {
76       action.run()
77     }
78   }
79 
80   private val activityResultListeners = CopyOnWriteArrayList<ActivityResultListener>()
81 
82   data class InstanceManagerBuilderProperties(
83     var application: Application?,
84     var jsBundlePath: String?,
85     var experienceProperties: Map<String, Any?>,
86     var expoPackages: List<Package>?,
87     var exponentPackageDelegate: ExponentPackageDelegate?,
88     var manifest: Manifest,
89     var singletonModules: List<SingletonModule>,
90   )
91 
92   fun addActivityResultListener(listener: ActivityResultListener) {
93     activityResultListeners.add(listener)
94   }
95 
96   fun removeActivityResultListener(listener: ActivityResultListener) {
97     activityResultListeners.remove(listener)
98   }
99 
100   fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
101     for (listener in activityResultListeners) {
102       listener.onActivityResult(requestCode, resultCode, data)
103     }
104   }
105 
106   /*
107    * Bundle loading
108    */
109   interface BundleListener {
110     fun onBundleLoaded(localBundlePath: String)
111     fun onError(e: Exception)
112   }
113 
114   // `id` must be URL encoded. Returns true if found cached bundle.
115   @JvmOverloads
116   fun loadJSBundle(
117     manifest: Manifest?,
118     urlString: String,
119     id: String,
120     abiVersion: String,
121     bundleListener: BundleListener,
122     shouldForceNetworkArg: Boolean = false,
123     shouldForceCache: Boolean = false
124   ): Boolean {
125     var shouldForceNetwork = shouldForceNetworkArg
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           try {
183             val sourceFile = File(directory, fileName)
184 
185             var hasCachedSourceFile = false
186             val networkResponse = response.networkResponse()
187             if (networkResponse == null || networkResponse.code() == KernelConstants.HTTP_NOT_MODIFIED) {
188               // If we're getting a cached response don't rewrite the file to disk.
189               EXL.d(TAG, "Got cached OkHttp response for $urlString")
190               if (sourceFile.exists()) {
191                 hasCachedSourceFile = true
192                 EXL.d(TAG, "Have cached source file for $urlString")
193               }
194             }
195 
196             if (!hasCachedSourceFile) {
197               var inputStream: InputStream? = null
198               var fileOutputStream: FileOutputStream? = null
199               var byteArrayOutputStream: ByteArrayOutputStream? = null
200               var teeOutputStream: TeeOutputStream? = null
201               try {
202                 EXL.d(TAG, "Do not have cached source file for $urlString")
203                 inputStream = response.body().byteStream()
204                 fileOutputStream = FileOutputStream(sourceFile)
205                 byteArrayOutputStream = ByteArrayOutputStream()
206 
207                 // Multiplex the stream. Write both to file and string.
208                 teeOutputStream = TeeOutputStream(fileOutputStream, byteArrayOutputStream)
209                 ByteStreams.copy(inputStream, teeOutputStream)
210                 teeOutputStream.flush()
211                 bundleStrings[sourceFile.absolutePath] = byteArrayOutputStream.toString()
212                 fileOutputStream.flush()
213                 fileOutputStream.fd.sync()
214               } finally {
215                 IOUtils.closeQuietly(teeOutputStream)
216                 IOUtils.closeQuietly(fileOutputStream)
217                 IOUtils.closeQuietly(byteArrayOutputStream)
218                 IOUtils.closeQuietly(inputStream)
219               }
220             }
221 
222             if (Constants.WRITE_BUNDLE_TO_LOG) {
223               printSourceFile(sourceFile.absolutePath)
224             }
225 
226             expoHandler.post { bundleListener.onBundleLoaded(sourceFile.absolutePath) }
227           } catch (e: Exception) {
228             bundleListener.onError(e)
229           }
230         }
231 
232         override fun onCachedResponse(response: ExpoResponse, isEmbedded: Boolean) {
233           EXL.d(TAG, "Using cached or embedded response.")
234           onResponse(response)
235         }
236       }
237 
238       exponentNetwork.longTimeoutClient.apply {
239         when {
240           shouldForceCache -> tryForcedCachedResponse(
241             request.url.toString(),
242             request,
243             callback,
244             null,
245             null
246           )
247           shouldForceNetwork -> callSafe(request, callback)
248           else -> callDefaultCache(request, callback)
249         }
250       }
251     } catch (e: Exception) {
252       bundleListener.onError(e)
253     }
254 
255     // Guess whether we'll use the cache based on whether the source file is saved.
256     val sourceFile = File(directory, fileName)
257     return sourceFile.exists()
258   }
259 
260   private fun printSourceFile(path: String) {
261     EXL.d(KernelConstants.BUNDLE_TAG, "Printing bundle:")
262     val inputStream = try {
263       FileInputStream(path)
264     } catch (e: Exception) {
265       EXL.e(KernelConstants.BUNDLE_TAG, e.toString())
266       return
267     }
268     inputStream.bufferedReader().useLines { lines ->
269       lines.forEach { line -> EXL.d(KernelConstants.BUNDLE_TAG, line) }
270     }
271   }
272 
273   interface PackagerStatusCallback {
274     fun onSuccess()
275     fun onFailure(errorMessage: String)
276   }
277 
278   fun testPackagerStatus(
279     isDebug: Boolean,
280     mManifest: Manifest,
281     callback: PackagerStatusCallback
282   ) {
283     if (!isDebug) {
284       callback.onSuccess()
285       return
286     }
287 
288     val debuggerHost = mManifest.getDebuggerHost()
289     exponentNetwork.noCacheClient.newCall(
290       Request.Builder().url("http://$debuggerHost/status").build()
291     ).enqueue(object : Callback {
292       override fun onFailure(call: Call, e: IOException) {
293         EXL.d(TAG, e.toString())
294         callback.onFailure("Packager is not running at http://$debuggerHost")
295       }
296 
297       @Throws(IOException::class)
298       override fun onResponse(call: Call, response: Response) {
299         val responseString = response.body!!.string()
300         if (responseString.contains(PACKAGER_RUNNING)) {
301           runOnUiThread { callback.onSuccess() }
302         } else {
303           callback.onFailure("Packager is not running at http://$debuggerHost")
304         }
305       }
306     })
307   }
308 
309   interface StartReactInstanceDelegate {
310     val isDebugModeEnabled: Boolean
311     val isInForeground: Boolean
312     val exponentPackageDelegate: ExponentPackageDelegate?
313     fun handleUnreadNotifications(unreadNotifications: JSONArray)
314   }
315 
316   companion object {
317     private val TAG = Exponent::class.java.simpleName
318 
319     private const val PACKAGER_RUNNING = "running"
320 
321     @JvmStatic lateinit var instance: Exponent
322       private set
323     private var hasBeenInitialized = false
324 
325     @JvmStatic fun initialize(context: Context, application: Application) {
326       if (!hasBeenInitialized) {
327         hasBeenInitialized = true
328         Exponent(context, application)
329       }
330     }
331 
332     @Throws(UnsupportedEncodingException::class)
333     fun encodeExperienceId(manifestId: String): String {
334       return URLEncoder.encode("experience-$manifestId", "UTF-8")
335     }
336 
337     fun getPort(urlArg: String): Int {
338       var url = urlArg
339       if (!url.contains("://")) {
340         url = "http://$url"
341       }
342       val uri = Uri.parse(url)
343       val port = uri.port
344       return if (port == -1) {
345         80
346       } else {
347         port
348       }
349     }
350 
351     fun getHostname(urlArg: String): String? {
352       var url = urlArg
353       if (!url.contains("://")) {
354         url = "http://$url"
355       }
356       val uri = Uri.parse(url)
357       return uri.host
358     }
359 
360     @JvmStatic fun enableDeveloperSupport(
361       debuggerHost: String,
362       mainModuleName: String,
363       builder: RNObject
364     ) {
365       if (debuggerHost.isEmpty() || mainModuleName.isEmpty()) {
366         return
367       }
368 
369       try {
370         val fieldObject = RNObject("com.facebook.react.modules.systeminfo.AndroidInfoHelpers")
371         fieldObject.loadVersion(builder.version())
372 
373         val debuggerHostHostname = getHostname(debuggerHost)
374         val debuggerHostPort = getPort(debuggerHost)
375 
376         val deviceField = fieldObject.rnClass()!!.getDeclaredField("DEVICE_LOCALHOST")
377         deviceField.isAccessible = true
378         deviceField[null] = debuggerHostHostname
379 
380         val genymotionField = fieldObject.rnClass()!!.getDeclaredField("GENYMOTION_LOCALHOST")
381         genymotionField.isAccessible = true
382         genymotionField[null] = debuggerHostHostname
383 
384         val emulatorField = fieldObject.rnClass()!!.getDeclaredField("EMULATOR_LOCALHOST")
385         emulatorField.isAccessible = true
386         emulatorField[null] = debuggerHostHostname
387 
388         fieldObject.callStatic("setDevServerPort", debuggerHostPort)
389         fieldObject.callStatic("setInspectorProxyPort", debuggerHostPort)
390 
391         builder.callRecursive("setUseDeveloperSupport", true)
392         builder.callRecursive("setJSMainModulePath", mainModuleName)
393       } catch (e: IllegalAccessException) {
394         e.printStackTrace()
395       } catch (e: NoSuchFieldException) {
396         e.printStackTrace()
397       }
398     }
399   }
400 
401   init {
402     instance = this
403     NativeModuleDepsProvider.initialize(application)
404     NativeModuleDepsProvider.instance.inject(Exponent::class.java, this)
405 
406     // Fixes Android memory leak
407     try {
408       UserManager::class.java.getMethod("get", Context::class.java).invoke(null, context)
409     } catch (e: Throwable) {
410       EXL.testError(e)
411     }
412 
413     try {
414       val okHttpClient = OkHttpClient.Builder()
415         .connectTimeout(HttpUrlConnectionNetworkFetcher.HTTP_DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
416         .readTimeout(0, TimeUnit.MILLISECONDS)
417         .writeTimeout(0, TimeUnit.MILLISECONDS)
418         .addInterceptor(KernelNetworkInterceptor.okhttpAppInterceptorProxy)
419         .addNetworkInterceptor(KernelNetworkInterceptor.okhttpNetworkInterceptorProxy)
420         .build()
421       val imagePipelineConfig = OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient).build()
422       Fresco.initialize(context, imagePipelineConfig)
423     } catch (e: RuntimeException) {
424       EXL.testError(e)
425     }
426 
427     // TODO: profile this
428     FlowManager.init(
429       FlowConfig.builder(context)
430         .addDatabaseConfig(
431           DatabaseConfig.builder(SchedulersDatabase::class.java)
432             .databaseName(SchedulersDatabase.NAME)
433             .build()
434         )
435         .addDatabaseConfig(
436           DatabaseConfig.builder(ActionDatabase::class.java)
437             .databaseName(ActionDatabase.NAME)
438             .build()
439         )
440         .addDatabaseConfig(
441           DatabaseConfig.builder(ExponentDB::class.java)
442             .databaseName(ExponentDB.NAME)
443             .build()
444         )
445         .build()
446     )
447 
448     if (!ExpoViewBuildConfig.DEBUG) {
449       // There are a few places in RN code that throw NetworkOnMainThreadException.
450       // WebsocketJavaScriptExecutor.connectInternal closes a websocket on the main thread.
451       // Shouldn't actually block the ui since it's fire and forget so not high priority to fix the root cause.
452       val policy = ThreadPolicy.Builder().permitAll().build()
453       StrictMode.setThreadPolicy(policy)
454     }
455   }
456 }
457