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.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