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