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