<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