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