Release React Native for Android

This is an early release and there are several things that are known
not to work if you're porting your iOS app to Android.

See the Known Issues guide on the website.

We will work with the community to reach platform parity with iOS.
This commit is contained in:
Martin Konicek
2015-09-14 15:35:58 +01:00
parent c372dab213
commit 42eb5464fd
571 changed files with 44550 additions and 116 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.react.devsupport">
<application>
<!-- This activity will be removed for release builds -->
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"
android:exported="false"
/>
</application>
</manifest>

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.facebook.react.bridge.ReactContext;
/**
* Helper class for controlling overlay view with FPS and JS FPS info
* that gets added directly to @{link WindowManager} instance.
*/
/* package */ class DebugOverlayController {
private final WindowManager mWindowManager;
private final ReactContext mReactContext;
private @Nullable FrameLayout mFPSDebugViewContainer;
public DebugOverlayController(ReactContext reactContext) {
mReactContext = reactContext;
mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
}
public void setFpsDebugViewVisible(boolean fpsDebugViewVisible) {
if (fpsDebugViewVisible && mFPSDebugViewContainer == null) {
mFPSDebugViewContainer = new FpsView(mReactContext);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT);
mWindowManager.addView(mFPSDebugViewContainer, params);
} else if (!fpsDebugViewVisible && mFPSDebugViewContainer != null) {
mFPSDebugViewContainer.removeAllViews();
mWindowManager.removeView(mFPSDebugViewContainer);
mFPSDebugViewContainer = null;
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.io.IOException;
import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import org.json.JSONException;
import org.json.JSONObject;
/**
* The debug server returns errors as json objects. This exception represents that error.
*/
public class DebugServerException extends IOException {
public final String description;
public final String fileName;
public final int lineNumber;
public final int column;
private DebugServerException(String description, String fileName, int lineNumber, int column) {
this.description = description;
this.fileName = fileName;
this.lineNumber = lineNumber;
this.column = column;
}
public String toReadableMessage() {
return description + "\n at " + fileName + ":" + lineNumber + ":" + column;
}
/**
* Parse a DebugServerException from the server json string.
* @param str json string returned by the debug server
* @return A DebugServerException or null if the string is not of proper form.
*/
@Nullable public static DebugServerException parse(String str) {
if (TextUtils.isEmpty(str)) {
return null;
}
try {
JSONObject jsonObject = new JSONObject(str);
String fullFileName = jsonObject.getString("filename");
return new DebugServerException(
jsonObject.getString("description"),
shortenFileName(fullFileName),
jsonObject.getInt("lineNumber"),
jsonObject.getInt("column"));
} catch (JSONException e) {
// I'm not sure how strict this format is for returned errors, or what other errors there can
// be, so this may end up being spammy. Can remove it later if necessary.
FLog.w(ReactConstants.TAG, "Could not parse DebugServerException from: " + str, e);
return null;
}
}
private static String shortenFileName(String fullFileName) {
String[] parts = fullFileName.split("/");
return parts[parts.length - 1];
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.modules.debug.DeveloperSettings;
/**
* Helper class for accessing developers settings that should not be accessed outside of the package
* {@link com.facebook.react.devsupport}. For accessing some of the settings by external modules
* this class implements an external interface {@link DeveloperSettings}.
*/
@VisibleForTesting
public class DevInternalSettings implements
DeveloperSettings,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String PREFS_FPS_DEBUG_KEY = "fps_debug";
private static final String PREFS_DEBUG_SERVER_HOST_KEY = "debug_http_host";
private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug";
private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change";
private final SharedPreferences mPreferences;
private final DevSupportManager mDebugManager;
public DevInternalSettings(
Context applicationContext,
DevSupportManager debugManager) {
mDebugManager = debugManager;
mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext);
mPreferences.registerOnSharedPreferenceChangeListener(this);
}
@Override
public boolean isFpsDebugEnabled() {
return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false);
}
@Override
public boolean isAnimationFpsDebugEnabled() {
return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false);
}
public @Nullable String getDebugServerHost() {
return mPreferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PREFS_FPS_DEBUG_KEY.equals(key) || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key)) {
mDebugManager.reloadSettings();
}
}
public boolean isReloadOnJSChangeEnabled() {
return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, false);
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
/**
* Callback class for custom options that may appear in {@link DevSupportManager} developer
* options menu. In case when option registered for this handler is selected from the menu, the
* instance method {@link #onOptionSelected} will be triggered.
*/
public interface DevOptionHandler {
/**
* Triggered in case when user select custom developer option from the developers options menu
* displayed with {@link DevSupportManager}.
*/
public void onOptionSelected();
}

View File

@@ -0,0 +1,307 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import okio.Okio;
import okio.Sink;
/**
* 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 connectiong 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
*/
/* package */ class DevServerHelper {
public static final String RELOAD_APP_EXTRA_JS_PROXY = "jsproxy";
private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION";
private static final String EMULATOR_LOCALHOST = "10.0.2.2";
private static final String GENYMOTION_LOCALHOST = "10.0.3.2";
private static final String DEVICE_LOCALHOST = "localhost";
private static final String BUNDLE_URL_FORMAT =
"http://%s:8081/%s.bundle?platform=android";
private static final String SOURCE_MAP_URL_FORMAT =
BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map");
private static final String LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT =
"http://%s:8081/launch-chrome-devtools";
private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
"http://%s:8081/onchange";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s:8081/debugger-proxy";
private static final int LONG_POLL_KEEP_ALIVE_DURATION_MS = 2 * 60 * 1000; // 2 mins
private static final int LONG_POLL_FAILURE_DELAY_MS = 5000;
private static final int HTTP_CONNECT_TIMEOUT_MS = 5000;
public interface BundleDownloadCallback {
void onSuccess();
void onFailure(Exception cause);
}
public interface OnServerContentChangeListener {
void onServerContentChanged();
}
private final DevInternalSettings mSettings;
private final OkHttpClient mClient;
private boolean mOnChangePollingEnabled;
private @Nullable OkHttpClient mOnChangePollingClient;
private @Nullable Handler mRestartOnChangePollingHandler;
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
public DevServerHelper(DevInternalSettings settings) {
mSettings = settings;
mClient = new OkHttpClient();
mClient.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
// No read or write timeouts by default
mClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
mClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
}
/** Intent action for reloading the JS */
public static String getReloadAppAction(Context context) {
return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX;
}
public String getWebsocketProxyURL() {
return String.format(Locale.US, WEBSOCKET_PROXY_URL_FORMAT, getDebugServerHost());
}
/**
* @return the host to use when connecting to the bundle server from the host itself.
*/
private static String getHostForJSProxy() {
return "localhost";
}
/**
* @return the host to use when connecting to the bundle server.
*/
private String getDebugServerHost() {
// Check debug server host setting first. If empty try to detect emulator type and use default
// hostname for those
String hostFromSettings = mSettings.getDebugServerHost();
if (!TextUtils.isEmpty(hostFromSettings)) {
return Assertions.assertNotNull(hostFromSettings);
}
// Since genymotion runs in vbox it use different hostname to refer to adb host.
// We detect whether app runs on genymotion and replace js bundle server hostname accordingly
if (isRunningOnGenymotion()) {
return GENYMOTION_LOCALHOST;
}
if (isRunningOnStockEmulator()) {
return EMULATOR_LOCALHOST;
}
FLog.w(
ReactConstants.TAG,
"You seem to be running on device. Run 'adb reverse tcp:8081 tcp:8081' " +
"to forward the debug server's port to the device.");
return DEVICE_LOCALHOST;
}
private boolean isRunningOnGenymotion() {
return Build.FINGERPRINT.contains("vbox");
}
private boolean isRunningOnStockEmulator() {
return Build.FINGERPRINT.contains("generic");
}
private String createBundleURL(String host, String jsModulePath) {
return String.format(BUNDLE_URL_FORMAT, host, jsModulePath);
}
public void downloadBundleFromURL(
final BundleDownloadCallback callback,
final String jsModulePath,
final File outputFile) {
final String bundleURL = createBundleURL(getDebugServerHost(), jsModulePath);
Request request = new Request.Builder()
.url(bundleURL)
.build();
Call call = mClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
callback.onFailure(e);
}
@Override
public void onResponse(Response response) throws IOException {
// Check for server errors. If the server error has the expected form, fail with more info.
if (!response.isSuccessful()) {
String body = response.body().string();
DebugServerException debugServerException = DebugServerException.parse(body);
if (debugServerException != null) {
callback.onFailure(debugServerException);
} else {
callback.onFailure(new IOException("Unexpected response code: " + response.code()));
}
return;
}
Sink output = null;
try {
output = Okio.sink(outputFile);
Okio.buffer(response.body().source()).readAll(output);
callback.onSuccess();
} finally {
if (output != null) {
output.close();
}
}
}
});
}
public void stopPollingOnChangeEndpoint() {
mOnChangePollingEnabled = false;
if (mRestartOnChangePollingHandler != null) {
mRestartOnChangePollingHandler.removeCallbacksAndMessages(null);
mRestartOnChangePollingHandler = null;
}
if (mOnChangePollingClient != null) {
mOnChangePollingClient.cancel(this);
mOnChangePollingClient = null;
}
mOnServerContentChangeListener = null;
}
public void startPollingOnChangeEndpoint(
OnServerContentChangeListener onServerContentChangeListener) {
if (mOnChangePollingEnabled) {
// polling already enabled
return;
}
mOnChangePollingEnabled = true;
mOnServerContentChangeListener = onServerContentChangeListener;
mOnChangePollingClient = new OkHttpClient();
mOnChangePollingClient
.setConnectionPool(new ConnectionPool(1, LONG_POLL_KEEP_ALIVE_DURATION_MS))
.setConnectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
mRestartOnChangePollingHandler = new Handler();
enqueueOnChangeEndpointLongPolling();
}
private void handleOnChangePollingResponse(boolean didServerContentChanged) {
if (mOnChangePollingEnabled) {
if (didServerContentChanged) {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mOnServerContentChangeListener != null) {
mOnServerContentChangeListener.onServerContentChanged();
}
}
});
}
enqueueOnChangeEndpointLongPolling();
}
}
private void enqueueOnChangeEndpointLongPolling() {
Request request = new Request.Builder().url(createOnChangeEndpointUrl()).tag(this).build();
Assertions.assertNotNull(mOnChangePollingClient).newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
if (mOnChangePollingEnabled) {
// this runnable is used by onchange endpoint poller to delay subsequent requests in case
// of a failure, so that we don't flood network queue with frequent requests in case when
// dev server is down
FLog.d(ReactConstants.TAG, "Error while requesting /onchange endpoint", e);
Assertions.assertNotNull(mRestartOnChangePollingHandler).postDelayed(
new Runnable() {
@Override
public void run() {
handleOnChangePollingResponse(false);
}
},
LONG_POLL_FAILURE_DELAY_MS);
}
}
@Override
public void onResponse(Response response) throws IOException {
handleOnChangePollingResponse(response.code() == 205);
}
});
}
private String createOnChangeEndpointUrl() {
return String.format(Locale.US, ONCHANGE_ENDPOINT_URL_FORMAT, getDebugServerHost());
}
private String createLaunchChromeDevtoolsCommandUrl() {
return String.format(LAUNCH_CHROME_DEVTOOLS_COMMAND_URL_FORMAT, getDebugServerHost());
}
public void launchChromeDevtools() {
Request request = new Request.Builder()
.url(createLaunchChromeDevtoolsCommandUrl())
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
// ignore HTTP call response, this is just to open a debugger page and there is no reason
// to report failures from here
}
@Override
public void onResponse(Response response) throws IOException {
// ignore HTTP call response - see above
}
});
}
public String getSourceMapUrl(String mainModuleName) {
return String.format(Locale.US, SOURCE_MAP_URL_FORMAT, getDebugServerHost(), mainModuleName);
}
public String getSourceUrl(String mainModuleName) {
return String.format(Locale.US, BUNDLE_URL_FORMAT, getDebugServerHost(), mainModuleName);
}
public String getJSBundleURLForRemoteDebugging(String mainModuleName) {
// The host IP we use when connecting to the JS bundle server from the emulator is not the
// same as the one needed to connect to the same server from the Chrome proxy running on the
// host itself.
return createBundleURL(getHostForJSProxy(), mainModuleName);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import com.facebook.react.R;
/**
* Activity that display developers settings. Should be added to the debug manifest of the app. Can
* be triggered through the developers option menu displayed by {@link DevSupportManager}.
*/
public class DevSettingsActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.catalyst_settings_title);
addPreferencesFromResource(R.xml.preferences);
}
}

View File

@@ -0,0 +1,629 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Locale;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.hardware.SensorManager;
import android.os.Environment;
import android.view.WindowManager;
import android.widget.Toast;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WebsocketJavaScriptExecutor;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ShakeDetector;
import com.facebook.react.modules.debug.DeveloperSettings;
/**
* Interface for accessing and interacting with development features. Following features
* are supported through this manager class:
* 1) Displaying JS errors (aka RedBox)
* 2) Displaying developers menu (Reload JS, Debug JS)
* 3) Communication with developer server in order to download updated JS bundle
* 4) Starting/stopping broadcast receiver for js reload signals
* 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may
* trigger developers menu.
* 6) Launching developers settings view
*
* This class automatically monitors the state of registered views and activities to which they are
* bound to make sure that we don't display overlay or that we we don't listen for sensor events
* when app is backgrounded.
*
* {@link ReactInstanceDevCommandsHandler} implementation is responsible for instantiating this
* instance and for populating with an instance of {@link CatalystInstance} whenever instance
* manager recreates it (through {@link #onNewCatalystContextCreated}). Also, instance manager is
* responsible for enabling/disabling dev support in case when app is backgrounded or when all the
* views has been detached from the instance (through {@link #setDevSupportEnabled} method).
*
* IMPORTANT: In order for developer support to work correctly it is required that the
* manifest of your application contain the following entries:
* {@code <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>}
* {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>}
*/
public class DevSupportManager implements NativeModuleCallExceptionHandler {
private static final int JAVA_ERROR_COOKIE = -1;
private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js";
private static final String EXOPACKAGE_LOCATION_FORMAT
= "/data/local/tmp/exopackage/%s//secondary-dex";
private final Context mApplicationContext;
private final ShakeDetector mShakeDetector;
private final BroadcastReceiver mReloadAppBroadcastReceiver;
private final DevServerHelper mDevServerHelper;
private final LinkedHashMap<String, DevOptionHandler> mCustomDevOptions =
new LinkedHashMap<>();
private final ReactInstanceDevCommandsHandler mReactInstanceCommandsHandler;
private final @Nullable String mJSAppBundleName;
private final File mJSBundleTempFile;
private @Nullable RedBoxDialog mRedBoxDialog;
private @Nullable AlertDialog mDevOptionsDialog;
private @Nullable DebugOverlayController mDebugOverlayController;
private @Nullable ReactContext mCurrentContext;
private DevInternalSettings mDevSettings;
private boolean mIsUsingJSProxy = false;
private boolean mIsReceiverRegistered = false;
private boolean mIsShakeDetectorStarted = false;
private boolean mIsDevSupportEnabled = false;
private boolean mIsCurrentlyProfiling = false;
private int mProfileIndex = 0;
public DevSupportManager(
Context applicationContext,
ReactInstanceDevCommandsHandler reactInstanceCommandsHandler,
@Nullable String packagerPathForJSBundleName,
boolean enableOnCreate) {
mReactInstanceCommandsHandler = reactInstanceCommandsHandler;
mApplicationContext = applicationContext;
mJSAppBundleName = packagerPathForJSBundleName;
mDevSettings = new DevInternalSettings(applicationContext, this);
mDevServerHelper = new DevServerHelper(mDevSettings);
// Prepare shake gesture detector (will be started/stopped from #reload)
mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() {
@Override
public void onShake() {
showDevOptionsDialog();
}
});
// Prepare reload APP broadcast receiver (will be registered/unregistered from #reload)
mReloadAppBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DevServerHelper.getReloadAppAction(context).equals(action)) {
if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) {
mIsUsingJSProxy = true;
mDevServerHelper.launchChromeDevtools();
} else {
mIsUsingJSProxy = false;
}
handleReloadJS();
}
}
};
// We store JS bundle loaded from dev server in a single destination in app's data dir.
// In case when someone schedule 2 subsequent reloads it may happen that JS thread will
// start reading first reload output while the second reload starts writing to the same
// file. As this should only be the case in dev mode we leave it as it is.
// TODO(6418010): Fix readers-writers problem in debug reload from HTTP server
mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);
setDevSupportEnabled(enableOnCreate);
}
@Override
public void handleException(Exception e) {
if (mIsDevSupportEnabled) {
FLog.e(ReactConstants.TAG, "Exception in native call from JS", e);
CharSequence details = ExceptionFormatterHelper.javaStackTraceToHtml(e.getStackTrace());
showNewError(e.getMessage(), details, JAVA_ERROR_COOKIE);
} else {
if (e instanceof RuntimeException) {
// Because we are rethrowing the original exception, the original stacktrace will be
// preserved
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
}
/**
* Add option item to dev settings dialog displayed by this manager. In the case user select given
* option from that dialog, the appropriate handler passed as {@param optionHandler} will be
* called.
*/
public void addCustomDevOption(
String optionName,
DevOptionHandler optionHandler) {
mCustomDevOptions.put(optionName, optionHandler);
}
public void showNewJSError(String message, ReadableArray details, int errorCookie) {
showNewError(message, ExceptionFormatterHelper.jsStackTraceToHtml(details), errorCookie);
}
public void updateJSError(
final String message,
final ReadableArray details,
final int errorCookie) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
// Since we only show the first JS error in a succession of JS errors, make sure we only
// update the error message for that error message. This assumes that updateJSError
// belongs to the most recent showNewJSError
if (mRedBoxDialog == null ||
!mRedBoxDialog.isShowing() ||
errorCookie != mRedBoxDialog.getErrorCookie()) {
return;
}
mRedBoxDialog.setTitle(message);
mRedBoxDialog.setDetails(ExceptionFormatterHelper.jsStackTraceToHtml(details));
mRedBoxDialog.show();
}
});
}
private void showNewError(
final String message,
final CharSequence details,
final int errorCookie) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mRedBoxDialog == null) {
mRedBoxDialog = new RedBoxDialog(mApplicationContext, DevSupportManager.this);
mRedBoxDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
if (mRedBoxDialog.isShowing()) {
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
// show the first and most actionable one.
return;
}
mRedBoxDialog.setTitle(message);
mRedBoxDialog.setDetails(details);
mRedBoxDialog.setErrorCookie(errorCookie);
mRedBoxDialog.show();
}
});
}
public void showDevOptionsDialog() {
if (mDevOptionsDialog != null || !mIsDevSupportEnabled) {
return;
}
LinkedHashMap<String, DevOptionHandler> options = new LinkedHashMap<>();
/* register standard options */
options.put(
mApplicationContext.getString(R.string.catalyst_reloadjs), new DevOptionHandler() {
@Override
public void onOptionSelected() {
handleReloadJS();
}
});
options.put(
mIsUsingJSProxy ?
mApplicationContext.getString(R.string.catalyst_debugjs_off) :
mApplicationContext.getString(R.string.catalyst_debugjs),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
mIsUsingJSProxy = !mIsUsingJSProxy;
handleReloadJS();
}
});
options.put(
mApplicationContext.getString(R.string.catalyst_settings), new DevOptionHandler() {
@Override
public void onOptionSelected() {
Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mApplicationContext.startActivity(intent);
}
});
options.put(
mApplicationContext.getString(R.string.catalyst_inspect_element),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
mReactInstanceCommandsHandler.toggleElementInspector();
}
});
if (mCurrentContext != null &&
mCurrentContext.getCatalystInstance() != null &&
mCurrentContext.getCatalystInstance().getBridge() != null &&
mCurrentContext.getCatalystInstance().getBridge().supportsProfiling()) {
options.put(
mApplicationContext.getString(
mIsCurrentlyProfiling ? R.string.catalyst_stop_profile :
R.string.catalyst_start_profile),
new DevOptionHandler() {
@Override
public void onOptionSelected() {
if (mCurrentContext != null && mCurrentContext.hasActiveCatalystInstance()) {
if (mIsCurrentlyProfiling) {
mIsCurrentlyProfiling = false;
String profileName = (Environment.getExternalStorageDirectory().getPath() +
"/profile_" + mProfileIndex + ".json");
mProfileIndex++;
mCurrentContext.getCatalystInstance()
.getBridge()
.stopProfiler("profile", profileName);
Toast.makeText(
mCurrentContext,
"Profile output to " + profileName,
Toast.LENGTH_LONG).show();
} else {
mIsCurrentlyProfiling = true;
mCurrentContext.getCatalystInstance().getBridge().startProfiler("profile");
}
}
}
});
}
if (mCustomDevOptions.size() > 0) {
options.putAll(mCustomDevOptions);
}
final DevOptionHandler[] optionHandlers = options.values().toArray(new DevOptionHandler[0]);
mDevOptionsDialog = new AlertDialog.Builder(mApplicationContext)
.setItems(options.keySet().toArray(new String[0]), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
optionHandlers[which].onOptionSelected();
mDevOptionsDialog = null;
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mDevOptionsDialog = null;
}
})
.create();
mDevOptionsDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
mDevOptionsDialog.show();
}
/**
* {@link ReactInstanceDevCommandsHandler} is responsible for
* enabling/disabling dev support when a React view is attached/detached
* or when application state changes (e.g. the application is backgrounded).
*/
public void setDevSupportEnabled(boolean isDevSupportEnabled) {
mIsDevSupportEnabled = isDevSupportEnabled;
reload();
}
public boolean getDevSupportEnabled() {
return mIsDevSupportEnabled;
}
public DeveloperSettings getDevSettings() {
return mDevSettings;
}
public void onNewReactContextCreated(ReactContext reactContext) {
resetCurrentContext(reactContext);
}
public void onReactInstanceDestroyed(ReactContext reactContext) {
if (reactContext == mCurrentContext) {
// only call reset context when the destroyed context matches the one that is currently set
// for this manager
resetCurrentContext(null);
}
}
public String getSourceMapUrl() {
return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName));
}
public String getSourceUrl() {
return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName));
}
public String getJSBundleURLForRemoteDebugging() {
return mDevServerHelper.getJSBundleURLForRemoteDebugging(
Assertions.assertNotNull(mJSAppBundleName));
}
public String getDownloadedJSBundleFile() {
return mJSBundleTempFile.getAbsolutePath();
}
/**
* @return {@code true} if {@link ReactInstanceManager} should use downloaded JS bundle file
* instead of using JS file from assets. This may happen when app has not been updated since
* the last time we fetched the bundle.
*/
public boolean hasUpToDateJSBundleInCache() {
if (mIsDevSupportEnabled && mJSBundleTempFile.exists()) {
try {
String packageName = mApplicationContext.getPackageName();
PackageInfo thisPackage = mApplicationContext.getPackageManager()
.getPackageInfo(packageName, 0);
if (mJSBundleTempFile.lastModified() > thisPackage.lastUpdateTime) {
// Base APK has not been updated since we donwloaded JS, but if app is using exopackage
// it may only be a single dex that has been updated. We check for exopackage dir update
// time in that case.
File exopackageDir = new File(
String.format(Locale.US, EXOPACKAGE_LOCATION_FORMAT, packageName));
if (exopackageDir.exists()) {
return mJSBundleTempFile.lastModified() > exopackageDir.lastModified();
}
return true;
}
} catch (PackageManager.NameNotFoundException e) {
// Ignore this error and just fallback to loading JS from assets
FLog.e(ReactConstants.TAG, "DevSupport is unable to get current app info");
}
}
return false;
}
/**
* @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case
* {@link ReactInstanceManager} should use that file from assets instead of downloading bundle
* from dev server
*/
public boolean hasBundleInAssets(String bundleAssetName) {
try {
String[] assets = mApplicationContext.getAssets().list("");
for (int i = 0; i < assets.length; i++) {
if (assets[i].equals(bundleAssetName)) {
return true;
}
}
} catch (IOException e) {
// Ignore this error and just fallback to downloading JS from devserver
FLog.e(ReactConstants.TAG, "Error while loading assets list");
}
return false;
}
private void resetCurrentContext(@Nullable ReactContext reactContext) {
if (mCurrentContext == reactContext) {
// new context is the same as the old one - do nothing
return;
}
// if currently profiling stop and write the profile file
if (mIsCurrentlyProfiling) {
mIsCurrentlyProfiling = false;
String profileName = (Environment.getExternalStorageDirectory().getPath() +
"/profile_" + mProfileIndex + ".json");
mProfileIndex++;
mCurrentContext.getCatalystInstance().getBridge().stopProfiler("profile", profileName);
}
mCurrentContext = reactContext;
// Recreate debug overlay controller with new CatalystInstance object
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(false);
}
if (reactContext != null) {
mDebugOverlayController = new DebugOverlayController(reactContext);
}
reloadSettings();
}
/* package */ void reloadSettings() {
reload();
}
public void handleReloadJS() {
// dismiss redbox if exists
if (mRedBoxDialog != null) {
mRedBoxDialog.dismiss();
}
ProgressDialog progressDialog = new ProgressDialog(mApplicationContext);
progressDialog.setTitle(R.string.catalyst_jsload_title);
progressDialog.setMessage(mApplicationContext.getString(
mIsUsingJSProxy ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
progressDialog.show();
if (mIsUsingJSProxy) {
reloadJSInProxyMode(progressDialog);
} else {
reloadJSFromServer(progressDialog);
}
}
private void reloadJSInProxyMode(final ProgressDialog progressDialog) {
// When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
// anyway
mDevServerHelper.launchChromeDevtools();
final WebsocketJavaScriptExecutor webSocketJSExecutor = new WebsocketJavaScriptExecutor();
webSocketJSExecutor.connect(
mDevServerHelper.getWebsocketProxyURL(),
new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() {
@Override
public void onSuccess() {
progressDialog.dismiss();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mReactInstanceCommandsHandler.onReloadWithJSDebugger(
new ProxyJavaScriptExecutor(webSocketJSExecutor));
}
});
}
@Override
public void onFailure(final Throwable cause) {
progressDialog.dismiss();
FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
showNewError(
mApplicationContext.getString(R.string.catalyst_remotedbg_error),
ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
JAVA_ERROR_COOKIE);
}
});
}
});
}
private void reloadJSFromServer(final ProgressDialog progressDialog) {
mDevServerHelper.downloadBundleFromURL(
new DevServerHelper.BundleDownloadCallback() {
@Override
public void onSuccess() {
progressDialog.dismiss();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mReactInstanceCommandsHandler.onJSBundleLoadedFromServer();
}
});
}
@Override
public void onFailure(final Exception cause) {
progressDialog.dismiss();
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (cause instanceof DebugServerException) {
DebugServerException debugServerException = (DebugServerException) cause;
showNewError(
debugServerException.description,
ExceptionFormatterHelper.debugServerExcStackTraceToHtml(
(DebugServerException) cause),
JAVA_ERROR_COOKIE);
} else {
showNewError(
mApplicationContext.getString(R.string.catalyst_jsload_error),
ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
JAVA_ERROR_COOKIE);
}
}
});
}
},
Assertions.assertNotNull(mJSAppBundleName),
mJSBundleTempFile);
}
private void reload() {
// reload settings, show/hide debug overlay if required & start/stop shake detector
if (mIsDevSupportEnabled) {
// update visibility of FPS debug overlay depending on the settings
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(mDevSettings.isFpsDebugEnabled());
}
// start shake gesture detector
if (!mIsShakeDetectorStarted) {
mShakeDetector.start(
(SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE));
mIsShakeDetectorStarted = true;
}
// register reload app broadcast receiver
if (!mIsReceiverRegistered) {
IntentFilter filter = new IntentFilter();
filter.addAction(DevServerHelper.getReloadAppAction(mApplicationContext));
mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter);
mIsReceiverRegistered = true;
}
if (mDevSettings.isReloadOnJSChangeEnabled()) {
mDevServerHelper.startPollingOnChangeEndpoint(
new DevServerHelper.OnServerContentChangeListener() {
@Override
public void onServerContentChanged() {
handleReloadJS();
}
});
} else {
mDevServerHelper.stopPollingOnChangeEndpoint();
}
} else {
// hide FPS debug overlay
if (mDebugOverlayController != null) {
mDebugOverlayController.setFpsDebugViewVisible(false);
}
// stop shake gesture detector
if (mIsShakeDetectorStarted) {
mShakeDetector.stop();
mIsShakeDetectorStarted = false;
}
// unregister app reload broadcast receiver
if (mIsReceiverRegistered) {
mApplicationContext.unregisterReceiver(mReloadAppBroadcastReceiver);
mIsReceiverRegistered = false;
}
// hide redbox dialog
if (mRedBoxDialog != null) {
mRedBoxDialog.dismiss();
}
// hide dev options dialog
if (mDevOptionsDialog != null) {
mDevOptionsDialog.dismiss();
}
mDevServerHelper.stopPollingOnChangeEndpoint();
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import java.io.File;
import android.text.Html;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
/**
* Helper class for displaying errors in an eye-catching form (red box).
*/
/* package */ class ExceptionFormatterHelper {
private static String getStackTraceHtmlComponent(
String methodName, String filename, int lineNumber, int columnNumber) {
StringBuilder stringBuilder = new StringBuilder();
methodName = methodName.replace("<", "&lt;").replace(">", "&gt;");
stringBuilder.append("<font color=#FDE5E5>")
.append(methodName)
.append("</font><br /><font color=#F9B3B3>")
.append(filename)
.append(":")
.append(lineNumber);
if (columnNumber != -1) {
stringBuilder
.append(":")
.append(columnNumber);
}
stringBuilder.append("</font><br /><br />");
return stringBuilder.toString();
}
public static CharSequence jsStackTraceToHtml(ReadableArray stack) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < stack.size(); i++) {
ReadableMap frame = stack.getMap(i);
String methodName = frame.getString("methodName");
String fileName = new File(frame.getString("file")).getName();
int lineNumber = frame.getInt("lineNumber");
int columnNumber = -1;
if (frame.hasKey("column") && !frame.isNull("column")) {
columnNumber = frame.getInt("column");
}
stringBuilder.append(getStackTraceHtmlComponent(
methodName, fileName, lineNumber, columnNumber));
}
return Html.fromHtml(stringBuilder.toString());
}
public static CharSequence javaStackTraceToHtml(StackTraceElement[] stack) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i< stack.length; i++) {
stringBuilder.append(getStackTraceHtmlComponent(
stack[i].getMethodName(), stack[i].getFileName(), stack[i].getLineNumber(), -1));
}
return Html.fromHtml(stringBuilder.toString());
}
public static CharSequence debugServerExcStackTraceToHtml(DebugServerException e) {
String s = getStackTraceHtmlComponent("", e.fileName, e.lineNumber, e.column);
return Html.fromHtml(s);
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import java.util.Locale;
import android.annotation.TargetApi;
import android.view.Choreographer;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.facebook.common.logging.FLog;
import com.facebook.react.R;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.modules.debug.FpsDebugFrameCallback;
/**
* View that automatically monitors and displays the current app frame rate. Also logs the current
* FPS to logcat while active.
*
* NB: Requires API 16 for use of FpsDebugFrameCallback.
*/
@TargetApi(16)
public class FpsView extends FrameLayout {
private static final int UPDATE_INTERVAL_MS = 500;
private final TextView mTextView;
private final FpsDebugFrameCallback mFrameCallback;
private final FPSMonitorRunnable mFPSMonitorRunnable;
public FpsView(ReactContext reactContext) {
super(reactContext);
inflate(reactContext, R.layout.fps_view, this);
mTextView = (TextView) findViewById(R.id.fps_text);
mFrameCallback = new FpsDebugFrameCallback(Choreographer.getInstance(), reactContext);
mFPSMonitorRunnable = new FPSMonitorRunnable();
setCurrentFPS(0, 0);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFrameCallback.reset();
mFrameCallback.start();
mFPSMonitorRunnable.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFrameCallback.stop();
mFPSMonitorRunnable.stop();
}
private void setCurrentFPS(double currentFPS, double currentJSFPS) {
String fpsString = String.format(
Locale.US,
"UI FPS: %.1f\nJS FPS: %.1f",
currentFPS,
currentJSFPS);
mTextView.setText(fpsString);
FLog.d(ReactConstants.TAG, fpsString);
}
/**
* Timer that runs every UPDATE_INTERVAL_MS ms and updates the currently displayed FPS.
*/
private class FPSMonitorRunnable implements Runnable {
private boolean mShouldStop = false;
@Override
public void run() {
if (mShouldStop) {
return;
}
setCurrentFPS(mFrameCallback.getFPS(), mFrameCallback.getJSFPS());
mFrameCallback.reset();
postDelayed(this, UPDATE_INTERVAL_MS);
}
public void start() {
mShouldStop = false;
post(this);
}
public void stop() {
mShouldStop = true;
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import com.facebook.react.bridge.ProxyJavaScriptExecutor;
/**
* Interface used by {@link DevSupportManager} for requesting React instance recreation
* based on the option that user select in developers menu.
*/
public interface ReactInstanceDevCommandsHandler {
/**
* Request react instance recreation with JS debugging enabled.
*/
void onReloadWithJSDebugger(ProxyJavaScriptExecutor proxyExecutor);
/**
* Notify react instance manager about new JS bundle version downloaded from the server.
*/
void onJSBundleLoadedFromServer();
/**
* Request to toggle the react element inspector.
*/
void toggleElementInspector();
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.devsupport;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Typeface;
import android.text.method.ScrollingMovementMethod;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import com.facebook.react.R;
/**
* Dialog for displaying JS errors in an eye-catching form (red box).
*/
/* package */ class RedBoxDialog extends Dialog {
private final DevSupportManager mDevSupportManager;
private TextView mTitle;
private TextView mDetails;
private Button mReloadJs;
private int mCookie = 0;
protected RedBoxDialog(Context context, DevSupportManager devSupportManager) {
super(context, R.style.Theme_Catalyst_RedBox);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.redbox_view);
mDevSupportManager = devSupportManager;
mTitle = (TextView) findViewById(R.id.catalyst_redbox_title);
mDetails = (TextView) findViewById(R.id.catalyst_redbox_details);
mDetails.setTypeface(Typeface.MONOSPACE);
mDetails.setHorizontallyScrolling(true);
mDetails.setMovementMethod(new ScrollingMovementMethod());
mReloadJs = (Button) findViewById(R.id.catalyst_redbox_reloadjs);
mReloadJs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDevSupportManager.handleReloadJS();
}
});
}
public void setTitle(String title) {
mTitle.setText(title);
}
public void setDetails(CharSequence details) {
mDetails.setText(details);
}
public void setErrorCookie(int cookie) {
mCookie = cookie;
}
public int getErrorCookie() {
return mCookie;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
mDevSupportManager.showDevOptionsDialog();
return true;
}
return super.onKeyUp(keyCode, event);
}
}