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