Migrate to Kotlin - DevServerHelper (#50558)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50558

This diff migrates the following file to Kotlin - DevServerHelper
as part of our ongoing effort of migrating the codebase to Kotlin

Changelog:
[Internal] [Changed] - DevServerHelper to Kotlin.

Reviewed By: javache

Differential Revision: D72632266

fbshipit-source-id: 5dab7206dcf11de042d9e274e33a38a4491f1d17
This commit is contained in:
Nicola Corti
2025-04-09 03:39:19 -07:00
committed by Facebook GitHub Bot
parent be46391654
commit dd0cd07405
3 changed files with 428 additions and 522 deletions
@@ -1967,28 +1967,28 @@ public final class com/facebook/react/devsupport/DefaultDevLoadingViewImplementa
public final fun setDevLoadingEnabled (Z)V
}
public class com/facebook/react/devsupport/DevServerHelper {
public final class com/facebook/react/devsupport/DevServerHelper {
public fun <init> (Lcom/facebook/react/modules/debug/interfaces/DeveloperSettings;Landroid/content/Context;Lcom/facebook/react/packagerconnection/PackagerConnectionSettings;)V
public fun closeInspectorConnection ()V
public fun closePackagerConnection ()V
public fun disableDebugger ()V
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
public fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
public fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
public fun getDevServerBundleURL (Ljava/lang/String;)Ljava/lang/String;
public fun getDevServerSplitBundleURL (Ljava/lang/String;)Ljava/lang/String;
public fun getSourceMapUrl (Ljava/lang/String;)Ljava/lang/String;
public fun getSourceUrl (Ljava/lang/String;)Ljava/lang/String;
public fun getWebsocketProxyURL ()Ljava/lang/String;
public fun isPackagerRunning (Lcom/facebook/react/devsupport/interfaces/PackagerStatusCallback;)V
public fun openDebugger (Lcom/facebook/react/bridge/ReactContext;Ljava/lang/String;)V
public fun openInspectorConnection ()V
public fun openPackagerConnection (Ljava/lang/String;Lcom/facebook/react/devsupport/DevServerHelper$PackagerCommandListener;)V
public final fun closeInspectorConnection ()V
public final fun closePackagerConnection ()V
public final fun disableDebugger ()V
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;)V
public final fun downloadBundleFromURL (Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;)V
public static synthetic fun downloadBundleFromURL$default (Lcom/facebook/react/devsupport/DevServerHelper;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;Ljava/io/File;Ljava/lang/String;Lcom/facebook/react/devsupport/BundleDownloader$BundleInfo;Lokhttp3/Request$Builder;ILjava/lang/Object;)V
public final fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
public final fun getDevServerBundleURL (Ljava/lang/String;)Ljava/lang/String;
public final fun getDevServerSplitBundleURL (Ljava/lang/String;)Ljava/lang/String;
public final fun getSourceMapUrl (Ljava/lang/String;)Ljava/lang/String;
public final fun getSourceUrl (Ljava/lang/String;)Ljava/lang/String;
public final fun getWebsocketProxyURL ()Ljava/lang/String;
public final fun isPackagerRunning (Lcom/facebook/react/devsupport/interfaces/PackagerStatusCallback;)V
public final fun openDebugger (Lcom/facebook/react/bridge/ReactContext;Ljava/lang/String;)V
public final fun openInspectorConnection ()V
public final fun openPackagerConnection (Ljava/lang/String;Lcom/facebook/react/devsupport/DevServerHelper$PackagerCommandListener;)V
}
public abstract interface class com/facebook/react/devsupport/DevServerHelper$PackagerCommandListener {
public abstract fun customCommandHandlers ()Ljava/util/Map;
public fun onCaptureHeapCommand (Lcom/facebook/react/packagerconnection/Responder;)V
public abstract fun onPackagerConnected ()V
public abstract fun onPackagerDevMenuCommand ()V
public abstract fun onPackagerDisconnected ()V
@@ -1,505 +0,0 @@
/*
* 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.
*/
package com.facebook.react.devsupport;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.Settings.Secure;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.ReactConstants;
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;
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.ConnectionCallback;
import com.facebook.react.packagerconnection.RequestHandler;
import com.facebook.react.packagerconnection.Responder;
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.HashMap;
import java.util.Locale;
import java.util.Map;
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;
import okio.Sink;
/**
* Helper class for all things about the debug server running in the engineer's host machine.
*
* <p>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:
*
* <ul>
* <li>Android stock emulator with standard non-configurable local loopback alias: 10.0.2.2
* <li>Genymotion emulator with default settings: 10.0.3.2
* </ul>
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
public class DevServerHelper {
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
private static final String DEBUGGER_MSG_DISABLE = "{ \"id\":1,\"method\":\"Debugger.disable\" }";
public interface PackagerCommandListener {
void onPackagerConnected();
void onPackagerDisconnected();
void onPackagerReloadCommand();
void onPackagerDevMenuCommand();
@Deprecated(forRemoval = true)
default void onCaptureHeapCommand(final Responder responder) {}
// Allow apps to provide listeners for custom packager commands.
@Nullable
Map<String, RequestHandler> customCommandHandlers();
}
private enum BundleType {
BUNDLE("bundle"),
MAP("map");
private final String mTypeID;
BundleType(String typeID) {
mTypeID = typeID;
}
public String typeID() {
return mTypeID;
}
}
private final DeveloperSettings mSettings;
private final PackagerConnectionSettings mPackagerConnectionSettings;
private final OkHttpClient mClient;
private final BundleDownloader mBundleDownloader;
private final PackagerStatusCheck mPackagerStatusCheck;
private final Context mApplicationContext;
private final String mPackageName;
private @Nullable JSPackagerClient mPackagerClient;
private @Nullable IInspectorPackagerConnection mInspectorPackagerConnection;
public DevServerHelper(
DeveloperSettings developerSettings,
Context applicationContext,
PackagerConnectionSettings packagerConnectionSettings) {
mSettings = developerSettings;
mPackagerConnectionSettings = packagerConnectionSettings;
mClient =
new OkHttpClient.Builder()
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.build();
mBundleDownloader = new BundleDownloader(mClient);
mPackagerStatusCheck = new PackagerStatusCheck(mClient);
mApplicationContext = applicationContext;
mPackageName = applicationContext.getPackageName();
}
public void openPackagerConnection(
final String clientId, final PackagerCommandListener commandListener) {
if (mPackagerClient != null) {
FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.");
return;
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... backgroundParams) {
Map<String, RequestHandler> handlers = new HashMap<>();
handlers.put(
"reload",
new NotificationOnlyHandler() {
@Override
public void onNotification(@Nullable Object params) {
commandListener.onPackagerReloadCommand();
}
});
handlers.put(
"devMenu",
new NotificationOnlyHandler() {
@Override
public void onNotification(@Nullable Object params) {
commandListener.onPackagerDevMenuCommand();
}
});
Map<String, RequestHandler> customHandlers = commandListener.customCommandHandlers();
if (customHandlers != null) {
handlers.putAll(customHandlers);
}
handlers.putAll(new FileIoHandler().handlers());
ConnectionCallback onPackagerConnectedCallback =
new ConnectionCallback() {
@Override
public void onConnected() {
commandListener.onPackagerConnected();
}
@Override
public void onDisconnected() {
commandListener.onPackagerDisconnected();
}
};
mPackagerClient =
new JSPackagerClient(
clientId, mPackagerConnectionSettings, handlers, onPackagerConnectedCallback);
mPackagerClient.init();
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void closePackagerConnection() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (mPackagerClient != null) {
mPackagerClient.close();
mPackagerClient = null;
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void openInspectorConnection() {
if (mInspectorPackagerConnection != null) {
FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.");
return;
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Map<String, String> metadata =
AndroidInfoHelpers.getInspectorHostMetadata(mApplicationContext);
String deviceName = metadata.get("deviceName");
if (deviceName == null) {
FLog.w(ReactConstants.TAG, "Could not get device name from Inspector Host Metadata.");
return null;
}
mInspectorPackagerConnection =
new CxxInspectorPackagerConnection(getInspectorDeviceUrl(), deviceName, mPackageName);
mInspectorPackagerConnection.connect();
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void disableDebugger() {
if (mInspectorPackagerConnection != null) {
mInspectorPackagerConnection.sendEventToAllConnections(DEBUGGER_MSG_DISABLE);
}
}
public void closeInspectorConnection() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (mInspectorPackagerConnection != null) {
mInspectorPackagerConnection.closeQuietly();
mInspectorPackagerConnection = null;
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public String getWebsocketProxyURL() {
return String.format(
Locale.US,
"ws://%s/debugger-proxy?role=client",
mPackagerConnectionSettings.getDebugServerHost());
}
private static String getSHA256(String string) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Could not get standard SHA-256 algorithm", e);
}
digest.reset();
byte[] result;
try {
result = digest.digest(string.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new 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]);
}
// Returns an opaque ID which is stable for the current combination of device and app, stable
// across installs, and unique across devices.
private String getInspectorDeviceId() {
// 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]
String packageName = mPackageName;
// 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]
String androidId =
Secure.getString(mApplicationContext.getContentResolver(), Secure.ANDROID_ID);
String rawDeviceId =
String.format(
Locale.US,
"android-%s-%s-%s",
packageName,
androidId,
InspectorFlags.getFuseboxEnabled() ? "fusebox" : "legacy");
return getSHA256(rawDeviceId);
}
private String getInspectorDeviceUrl() {
return String.format(
Locale.US,
"http://%s/inspector/device?name=%s&app=%s&device=%s&profiling=%b",
mPackagerConnectionSettings.getDebugServerHost(),
Uri.encode(AndroidInfoHelpers.getFriendlyDeviceName()),
Uri.encode(mPackageName),
Uri.encode(getInspectorDeviceId()),
InspectorFlags.getIsProfilingBuild());
}
public void downloadBundleFromURL(
DevBundleDownloadListener callback,
File outputFile,
String bundleURL,
@Nullable BundleDownloader.BundleInfo bundleInfo) {
mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo);
}
public void downloadBundleFromURL(
DevBundleDownloadListener callback,
File outputFile,
String bundleURL,
@Nullable BundleDownloader.BundleInfo bundleInfo,
Request.Builder requestBuilder) {
mBundleDownloader.downloadBundleFromURL(
callback, outputFile, bundleURL, bundleInfo, requestBuilder);
}
/**
* @return the host to use when connecting to the bundle server from the host itself.
*/
private String getHostForJSProxy() {
// Use custom port if configured. Note that host stays "localhost".
String host = Assertions.assertNotNull(mPackagerConnectionSettings.getDebugServerHost());
int portOffset = host.lastIndexOf(':');
if (portOffset > -1) {
return "localhost" + host.substring(portOffset);
} else {
return AndroidInfoHelpers.DEVICE_LOCALHOST;
}
}
/**
* @return whether we should enable dev mode when requesting JS bundles.
*/
private boolean getDevMode() {
return mSettings.isJSDevModeEnabled();
}
/**
* @return whether we should request minified JS bundles.
*/
private boolean getJSMinifyMode() {
return mSettings.isJSMinifyEnabled();
}
private String createBundleURL(String mainModuleID, BundleType type, String host) {
return createBundleURL(mainModuleID, type, host, false, true);
}
private String createSplitBundleURL(String mainModuleID, String host) {
return createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false);
}
private String createBundleURL(
String mainModuleID, BundleType type, String host, boolean modulesOnly, boolean runModule) {
boolean dev = getDevMode();
StringBuilder additionalOptionsBuilder = new StringBuilder();
for (Map.Entry<String, String> entry :
mPackagerConnectionSettings.getAdditionalOptionsForPackager().entrySet()) {
if (entry.getValue().length() == 0) {
continue;
}
additionalOptionsBuilder.append("&" + entry.getKey() + "=" + Uri.encode(entry.getValue()));
}
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
getJSMinifyMode(),
mPackageName,
modulesOnly ? "true" : "false",
runModule ? "true" : "false")
+ (InspectorFlags.getFuseboxEnabled() ? "&excludeSource=true&sourcePaths=url-server" : "")
+ additionalOptionsBuilder.toString();
}
private String createBundleURL(String mainModuleID, BundleType type) {
return createBundleURL(mainModuleID, type, mPackagerConnectionSettings.getDebugServerHost());
}
private static String createResourceURL(String host, String resourcePath) {
// This is what we get for not using a proper URI library.
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);
}
public String getDevServerBundleURL(final String jsModulePath) {
return createBundleURL(
jsModulePath, BundleType.BUNDLE, mPackagerConnectionSettings.getDebugServerHost());
}
public String getDevServerSplitBundleURL(String jsModulePath) {
return createSplitBundleURL(jsModulePath, mPackagerConnectionSettings.getDebugServerHost());
}
public void isPackagerRunning(final PackagerStatusCallback callback) {
String host = mPackagerConnectionSettings.getDebugServerHost();
if (host == null) {
FLog.w(ReactConstants.TAG, "No packager host configured.");
callback.onPackagerStatusFetched(false);
} else {
mPackagerStatusCheck.run(host, callback);
}
}
public String getSourceMapUrl(String mainModuleName) {
return createBundleURL(mainModuleName, BundleType.MAP);
}
public String getSourceUrl(String mainModuleName) {
return 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 @Nullable File downloadBundleResourceFromUrlSync(
final String resourcePath, final File outputFile) {
final String resourceURL =
createResourceURL(mPackagerConnectionSettings.getDebugServerHost(), resourcePath);
final Request request = new Request.Builder().url(resourceURL).build();
try (Response response = mClient.newCall(request).execute()) {
if (!response.isSuccessful() || response.body() == null) {
return null;
}
try (Sink output = Okio.sink(outputFile)) {
Okio.buffer(response.body().source()).readAll(output);
}
return outputFile;
} catch (Exception ex) {
FLog.e(
ReactConstants.TAG,
"Failed to fetch resource synchronously - resourcePath: \"%s\", outputFile: \"%s\"",
resourcePath,
outputFile.getAbsolutePath(),
ex);
return null;
}
}
/** Attempt to open the JS debugger on the host machine (on-device CDP debugging). */
public void openDebugger(@Nullable final ReactContext context, final String errorMessage) {
// TODO(huntie): Requests to dev server should not assume 'http' URL scheme
String requestUrl =
String.format(
Locale.US,
"http://%s/open-debugger?device=%s",
mPackagerConnectionSettings.getDebugServerHost(),
Uri.encode(getInspectorDeviceId()));
Request request =
new Request.Builder().url(requestUrl).method("POST", RequestBody.create(null, "")).build();
mClient
.newCall(request)
.enqueue(
new Callback() {
@Override
public void onFailure(@NonNull Call _call, @NonNull IOException _e) {
RNLog.w(context, errorMessage);
}
@Override
public void onResponse(@NonNull Call _call, @NonNull Response _response) {}
});
}
}
@@ -0,0 +1,411 @@
/*
* 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 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<String, RequestHandler>?
}
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<Void, Void, Void>() {
@Deprecated("This needs to be rewritten to not use AsyncTasks")
override fun doInBackground(vararg backgroundParams: Void): Void? {
val handlers: MutableMap<String, RequestHandler> = 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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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<Void, Void, Void>() {
@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()
for ((key, value) in packagerConnectionSettings.additionalOptionsForPackager) {
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 fun getDevServerBundleURL(jsModulePath: String): String =
createBundleURL(jsModulePath, BundleType.BUNDLE, packagerConnectionSettings.debugServerHost)
public fun getDevServerSplitBundleURL(jsModulePath: String): String =
createSplitBundleURL(jsModulePath, packagerConnectionSettings.debugServerHost)
public fun isPackagerRunning(callback: PackagerStatusCallback) {
packagerStatusCheck.run(packagerConnectionSettings.debugServerHost, callback)
}
public fun getSourceMapUrl(mainModuleName: String): String =
createBundleURL(mainModuleName, BundleType.MAP)
public 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 -> response.body()?.source()?.buffer()?.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?) {
// TODO(huntie): Requests to dev server should not assume 'http' URL scheme
val requestUrl =
String.format(
Locale.US,
"http://%s/open-debugger?device=%s",
packagerConnectionSettings.debugServerHost,
Uri.encode(inspectorDeviceId))
val request =
Request.Builder().url(requestUrl).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)
}
}
}