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