/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // Conflicting okhttp versions + This class needs to be rewritten to don't // use AsyncTasks @file:Suppress("DEPRECATION", "DEPRECATION_ERROR") package com.facebook.react.devsupport import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.os.AsyncTask import android.provider.Settings.Secure import com.facebook.common.logging.FLog import com.facebook.react.bridge.ReactContext import com.facebook.react.common.ReactConstants import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled import com.facebook.react.devsupport.InspectorFlags.getIsProfilingBuild import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener import com.facebook.react.devsupport.interfaces.PackagerStatusCallback import com.facebook.react.modules.debug.interfaces.DeveloperSettings import com.facebook.react.modules.systeminfo.AndroidInfoHelpers.getFriendlyDeviceName import com.facebook.react.modules.systeminfo.AndroidInfoHelpers.getInspectorHostMetadata import com.facebook.react.packagerconnection.FileIoHandler import com.facebook.react.packagerconnection.JSPackagerClient import com.facebook.react.packagerconnection.NotificationOnlyHandler import com.facebook.react.packagerconnection.PackagerConnectionSettings import com.facebook.react.packagerconnection.ReconnectingWebSocket import com.facebook.react.packagerconnection.RequestHandler import com.facebook.react.util.RNLog import java.io.File import java.io.IOException import java.io.UnsupportedEncodingException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.Locale import java.util.concurrent.TimeUnit import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response import okio.Okio /** * Helper class for all things about the debug server running in the engineer's host machine. * * One can use 'debug_http_host' shared preferences key to provide a host name for the debug server. * If the setting is empty we support and detect two basic configuration that works well for android * emulators connection to debug server running on emulator's host: * - Android stock emulator with standard non-configurable local loopback alias: 10.0.2.2 * - Genymotion emulator with default settings: 10.0.3.2 */ @SuppressLint( "StaticFieldLeak" ) // TODO: This entire class should be rewritten to don't use AsyncTask public open class DevServerHelper( private val settings: DeveloperSettings, private val applicationContext: Context, private val packagerConnectionSettings: PackagerConnectionSettings, ) { public interface PackagerCommandListener { public fun onPackagerConnected() public fun onPackagerDisconnected() public fun onPackagerReloadCommand() public fun onPackagerDevMenuCommand() // Allow apps to provide listeners for custom packager commands. public fun customCommandHandlers(): Map? } public val websocketProxyURL: String get() = "ws://${packagerConnectionSettings.debugServerHost}/debugger-proxy?role=client" private enum class BundleType(val typeID: String) { BUNDLE("bundle"), MAP("map"), } private val client: OkHttpClient = OkHttpClient.Builder() .connectTimeout(HTTP_CONNECT_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS) .readTimeout(0, TimeUnit.MILLISECONDS) .writeTimeout(0, TimeUnit.MILLISECONDS) .build() private val bundleDownloader: BundleDownloader = BundleDownloader(client) private val packagerStatusCheck: PackagerStatusCheck = PackagerStatusCheck(client) private val packageName: String = applicationContext.packageName private var packagerClient: JSPackagerClient? = null private var inspectorPackagerConnection: IInspectorPackagerConnection? = null /** Returns an opaque ID which is stable for the current combination of device and app, stable */ private val inspectorDeviceId: String get() { // Every Android app has a unique application ID that looks like a Java or Kotlin package // name, // such as com.example.myapp. This ID uniquely identifies your app on the device and in the // Google Play Store. [Source: Android docs] val packageName = packageName // A 64-bit number expressed as a hexadecimal string, which is either: // * unique to each combination of app-signing key, user, and device (API level >= 26), or // * randomly generated when the user first sets up the device and should remain constant for // the lifetime of the user's device (API level < 26). // [Source: Android docs] val androidId = Secure.getString(applicationContext.contentResolver, Secure.ANDROID_ID) val rawDeviceId = String.format( Locale.US, "android-%s-%s-%s", packageName, androidId, if (getFuseboxEnabled()) "fusebox" else "legacy", ) return getSHA256(rawDeviceId) } private val inspectorDeviceUrl: String get() = String.format( Locale.US, "http://%s/inspector/device?name=%s&app=%s&device=%s&profiling=%b", packagerConnectionSettings.debugServerHost, Uri.encode(getFriendlyDeviceName()), Uri.encode(packageName), Uri.encode(inspectorDeviceId), getIsProfilingBuild(), ) /** Whether we should enable dev mode when requesting JS bundles. */ private val devMode: Boolean get() = settings.isJSDevModeEnabled /** Whether we should request minified JS bundles. */ private val jSMinifyMode: Boolean get() = settings.isJSMinifyEnabled public fun openPackagerConnection(clientId: String?, commandListener: PackagerCommandListener) { if (packagerClient != null) { FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.") return } object : AsyncTask() { @Deprecated("This needs to be rewritten to not use AsyncTasks") override fun doInBackground(vararg backgroundParams: Void): Void? { val handlers: MutableMap = mutableMapOf() handlers["reload"] = object : NotificationOnlyHandler() { override fun onNotification(params: Any?) { commandListener.onPackagerReloadCommand() } } handlers["devMenu"] = object : NotificationOnlyHandler() { override fun onNotification(params: Any?) { commandListener.onPackagerDevMenuCommand() } } commandListener.customCommandHandlers()?.let { handlers.putAll(it) } handlers.putAll(FileIoHandler().handlers()) val onPackagerConnectedCallback: ReconnectingWebSocket.ConnectionCallback = object : ReconnectingWebSocket.ConnectionCallback { override fun onConnected() { commandListener.onPackagerConnected() } override fun onDisconnected() { commandListener.onPackagerDisconnected() } } checkNotNull(clientId) packagerClient = JSPackagerClient( clientId, packagerConnectionSettings, handlers, onPackagerConnectedCallback, ) .apply { init() } return null } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } public fun closePackagerConnection() { object : AsyncTask() { @Deprecated("This class needs to be rewritten to don't use AsyncTasks") override fun doInBackground(vararg params: Void): Void? { packagerClient?.close() packagerClient = null return null } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } public fun openInspectorConnection() { if (inspectorPackagerConnection != null) { FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.") return } object : AsyncTask() { @Deprecated("This class needs to be rewritten to don't use AsyncTasks") override fun doInBackground(vararg params: Void): Void? { val metadata = getInspectorHostMetadata(applicationContext) val deviceName = metadata["deviceName"] if (deviceName == null) { FLog.w(ReactConstants.TAG, "Could not get device name from Inspector Host Metadata.") return null } inspectorPackagerConnection = CxxInspectorPackagerConnection( this@DevServerHelper.inspectorDeviceUrl, deviceName, packageName, ) .apply { connect() } return null } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } public fun disableDebugger() { inspectorPackagerConnection?.sendEventToAllConnections(DEBUGGER_MSG_DISABLE) } public fun closeInspectorConnection() { object : AsyncTask() { @Deprecated("This class needs to be rewritten to don't use AsyncTasks") override fun doInBackground(vararg params: Void): Void? { inspectorPackagerConnection?.closeQuietly() inspectorPackagerConnection = null return null } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } @JvmOverloads public fun downloadBundleFromURL( callback: DevBundleDownloadListener, outputFile: File, bundleURL: String?, bundleInfo: BundleDownloader.BundleInfo?, requestBuilder: Request.Builder = Request.Builder(), ) { bundleDownloader.downloadBundleFromURL( callback, outputFile, bundleURL, bundleInfo, requestBuilder, ) } private fun createSplitBundleURL(mainModuleID: String, host: String): String = createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false) private fun createBundleURL( mainModuleID: String, type: BundleType, host: String = packagerConnectionSettings.debugServerHost, modulesOnly: Boolean = false, runModule: Boolean = true, ): String { val dev = devMode val additionalOptionsBuilder = StringBuilder() val packagerOptions = packagerConnectionSettings.updatePackagerOptions( packagerConnectionSettings.additionalOptionsForPackager ) for ((key, value) in packagerOptions) { if (value.isEmpty()) { continue } additionalOptionsBuilder.append("&" + key + "=" + Uri.encode(value)) } return (String.format( Locale.US, "http://%s/%s.%s?platform=android&dev=%s&lazy=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s", host, mainModuleID, type.typeID, dev, // dev dev, // lazy jSMinifyMode, packageName, if (modulesOnly) "true" else "false", if (runModule) "true" else "false", ) + (if (getFuseboxEnabled()) "&excludeSource=true&sourcePaths=url-server" else "") + additionalOptionsBuilder.toString()) } public open fun getDevServerBundleURL(jsModulePath: String): String = createBundleURL(jsModulePath, BundleType.BUNDLE, packagerConnectionSettings.debugServerHost) public open fun getDevServerSplitBundleURL(jsModulePath: String): String = createSplitBundleURL(jsModulePath, packagerConnectionSettings.debugServerHost) public open fun isPackagerRunning(callback: PackagerStatusCallback) { packagerStatusCheck.run(packagerConnectionSettings.debugServerHost, callback) } public open fun getSourceMapUrl(mainModuleName: String): String = createBundleURL(mainModuleName, BundleType.MAP) public open fun getSourceUrl(mainModuleName: String): String = createBundleURL(mainModuleName, BundleType.BUNDLE) /** * This is a debug-only utility to allow fetching a file via packager. It's made synchronous for * simplicity, but should only be used if it's absolutely necessary. * * @return the file with the fetched content, or null if there's any failure. */ public fun downloadBundleResourceFromUrlSync(resourcePath: String, outputFile: File): File? { val resourceURL = createResourceURL(packagerConnectionSettings.debugServerHost, resourcePath) val request = Request.Builder().url(resourceURL).build() try { client.newCall(request).execute().use { response -> if (!response.isSuccessful || response.body() == null) { return null } Okio.sink(outputFile).use { output -> Okio.buffer(response.body()?.source()!!).readAll(output) } return outputFile } } catch (e: Exception) { FLog.e( ReactConstants.TAG, "Failed to fetch resource synchronously - resourcePath: \"%s\", outputFile: \"%s\"", resourcePath, outputFile.absolutePath, e, ) return null } } /** Attempt to open the JS debugger on the host machine (on-device CDP debugging). */ public fun openDebugger( context: ReactContext?, errorMessage: String?, panel: String?, ) { // TODO(huntie): Requests to dev server should not assume 'http' URL scheme val requestUrlBuilder = StringBuilder() requestUrlBuilder.append( String.format( Locale.US, "http://%s/open-debugger?device=%s", packagerConnectionSettings.debugServerHost, Uri.encode(inspectorDeviceId), ) ) if (panel != null) { requestUrlBuilder.append("&panel=" + Uri.encode(panel)) } val request = Request.Builder() .url(requestUrlBuilder.toString()) .method("POST", RequestBody.create(null, "")) .build() client .newCall(request) .enqueue( object : Callback { override fun onFailure(call: Call, e: IOException) { RNLog.w(context, errorMessage ?: "openDebugger error") } override fun onResponse(call: Call, response: Response) = Unit } ) } private companion object { private const val HTTP_CONNECT_TIMEOUT_MS = 5000 private const val DEBUGGER_MSG_DISABLE = "{ \"id\":1,\"method\":\"Debugger.disable\" }" private fun getSHA256(string: String): String { val digest = try { MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { throw AssertionError("Could not get standard SHA-256 algorithm", e) } digest.reset() val result = try { digest.digest(string.toByteArray(charset("UTF-8"))) } catch (e: UnsupportedEncodingException) { throw AssertionError("This environment doesn't support UTF-8 encoding", e) } return String.format( "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15], result[16], result[17], result[18], result[19], ) } private fun createResourceURL(host: String, resourcePathParam: String): String { // This is what we get for not using a proper URI library. var resourcePath = resourcePathParam if (resourcePath.startsWith("/")) { FLog.w(ReactConstants.TAG, "Resource path should not begin with `/`, removing it.") resourcePath = resourcePath.substring(1) } return String.format(Locale.US, "http://%s/%s", host, resourcePath) } } }