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