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