Skip to content

Commit

Permalink
Use currentActivity to display redbox, loading view and dev menu
Browse files Browse the repository at this point in the history
Summary:
This change aims at replacing SYSTEM_ALERT_WINDOW/OVERLAY API being used for rendering dev support related views on Android (redbox, dev menu, green loading view) with API that does not require any special permission. The permission is still used for displaying perf monitor, although it is no longer requested at app startup but only when perf monitor gets enabled.

This change should not affect the way react native apps work in production environment as in release mode all dev support functionality is disabled.

There are two main reasons why requiring SYSTEM_ALERT/OVERLAY permission for displaying basic dev related windows is problematic:
 1) On Android >=6 devices it is required that overlay permission is granted in device settings for apps being side loaded (not installed via play store which is usually the case for apps being developed). Although this setting is not available on some Android devices including Google's stock Android TV version. On such devices App cannot be granted rights to draw in system alert window which cases the app to crash (instead of showing a redbox or dev menu dialog)
 2) Some Android device vendors have issues with implementation of `Settings.canDrawOverlays` that always return false (I've seen it on Xiaomi Redmi 4A with Android 6.1). This issue because of the following code in [ReactActivityDelegate.java#L90](https://github.com/facebook/react-native/blob/1e8f3b11027fe0a7514b4fc97d0798d3c64bc895/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java#L90), results in the overlay permission settings screen popping up every time the app is launched even though the permission has been perviously granted which is extremely annoying. Since this change only require overlay permission for displaying perf monitor we no longer ask for it on startup but only when user switches perf monitor ON.

Test need to be performed on pre Android 6 and post Android 6 devices.

1. Run app with devserver off -> should result in redbox
2. Start packager with --reset-cache flag for the loading bar to be visible for some longer period of time. Then restart the app and see the loading bar show up
3. While the app is running, open dev menu, navigate to "dev settings", test "reload"
4. Modify JS app such that the app crashes, see it display redbox properly. Check if "reload" button works well from the redbox
5. Verify that "Show Perf Monitor" option works as expected. On Android >=6 re-install the app to see it ask for overlay permission at the moment when perf monitor option gets selected.

 - SYSTEM_ALERT_WINDOW permission will no longer be required on Android to display Redbox

 This change can break things for framework users who provide custom implementation of DevSupportManager interface on Android:

- **Who does this affect**: Owners of apps that use custom implementation of DevSupportManager interface on Android.

- **How to migrate**: Update `create` method of your `DevSupportManager`'s factory to take `ReactInstanceManagerDevHelper` type as a second argument instead of `ReactInstanceDevCommandsHandler`. The interface `ReactInstanceDevCommandsHandler` has been renamed to `ReactInstanceManagerDevHelper` but kept all the methods the same (new method got added). If you were calling one of three methods from `ReactInstanceDevCommandsHandler` interface (`onReloadWithJSDebugger`, `onJSBundleLoadedFromServer` and `toggleElementInspector`) you can call exact same methods directly on `ReactInstanceManagerDevHelper` instance that is being provided in exchange for `ReactInstanceManagerDevHelper `.

- **Why make this breaking change**:
This PR adds a new method to `ReactInstanceManagerDevHelper` called `getCurrentActivity`. In which case the prev name can no longer be justified. The activity is required for some of the DevSupportManager methods in order to start new dialogs and popups so that overlay permission isn't necessary.

- **Severity (number of people affected x effort)**:
Relatively small (perhaps Fb internally is using DevSupportManager abstraction to provide an alternative implementation but since it isn't documented I doubt anyone else uses it). Effort it very low as it boils down to updating uses of interface `ReactInstanceDevCommandsHandler` with `ReactInstanceManagerDevHelper` (all the methods in `ReactInstanceDevCommandsHandler` stays the same)
Closes #16596

Differential Revision: D6256999

Pulled By: achen1

fbshipit-source-id: 551d449e831da3de466726ead172608527fcfbb4
  • Loading branch information
kmagiera authored and facebook-github-bot committed Dec 8, 2017
1 parent f59140e commit d19afc7
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,13 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import android.widget.Toast;

import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;
Expand All @@ -31,12 +26,6 @@
*/
public class ReactActivityDelegate {

private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
"Overlay permissions have been granted.";
private static final String REDBOX_PERMISSION_MESSAGE =
"Overlay permissions needs to be granted in order for react native apps to run in dev mode";

private final @Nullable Activity mActivity;
private final @Nullable FragmentActivity mFragmentActivity;
private final @Nullable String mMainComponentName;
Expand Down Expand Up @@ -84,19 +73,7 @@ public ReactInstanceManager getReactInstanceManager() {
}

protected void onCreate(Bundle savedInstanceState) {
boolean needsOverlayPermission = false;
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(getContext())) {
needsOverlayPermission = true;
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
}
}

if (mMainComponentName != null && !needsOverlayPermission) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
Expand Down Expand Up @@ -147,16 +124,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
} else {
// Did we request overlay permissions?
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(getContext())) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Process;
import android.support.v4.view.ViewCompat;
import android.util.Log;
import android.view.View;
import com.facebook.common.logging.FLog;
Expand Down Expand Up @@ -65,7 +66,7 @@
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.devsupport.DevSupportManagerFactory;
import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler;
import com.facebook.react.devsupport.ReactInstanceManagerDevHelper;
import com.facebook.react.devsupport.RedBoxHandler;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
Expand Down Expand Up @@ -221,7 +222,7 @@ public static ReactInstanceManagerBuilder builder() {
mDevSupportManager =
DevSupportManagerFactory.create(
applicationContext,
createDevInterface(),
createDevHelperInterface(),
mJSMainModulePath,
useDeveloperSupport,
redBoxHandler,
Expand Down Expand Up @@ -261,8 +262,8 @@ public void invokeDefaultOnBackPressed() {
}
}

private ReactInstanceDevCommandsHandler createDevInterface() {
return new ReactInstanceDevCommandsHandler() {
private ReactInstanceManagerDevHelper createDevHelperInterface() {
return new ReactInstanceManagerDevHelper() {
@Override
public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
ReactInstanceManager.this.onReloadWithJSDebugger(jsExecutorFactory);
Expand All @@ -277,6 +278,11 @@ public void onJSBundleLoadedFromServer() {
public void toggleElementInspector() {
ReactInstanceManager.this.toggleElementInspector();
}

@Override
public @Nullable Activity getCurrentActivity() {
return ReactInstanceManager.this.mCurrentActivity;
}
};
}

Expand Down Expand Up @@ -563,11 +569,40 @@ public void onHostResume(Activity activity, DefaultHardwareBackBtnHandler defaul
UiThreadUtil.assertOnUiThread();

mDefaultBackButtonImpl = defaultBackButtonImpl;
mCurrentActivity = activity;

if (mUseDeveloperSupport) {
mDevSupportManager.setDevSupportEnabled(true);
// Resume can be called from one of two different states:
// a) when activity was paused
// b) when activity has just been created
// In case of (a) the activity is attached to window and it is ok to add new views to it or
// open dialogs. In case of (b) there is often a slight delay before such a thing happens.
// As dev support manager can add views or open dialogs immediately after it gets enabled
// (e.g. in the case when JS bundle is being fetched in background) we only want to enable
// it once we know for sure the current activity is attached.

// We check if activity is attached to window by checking if decor view is attached
final View decorView = mCurrentActivity.getWindow().getDecorView();
if (!ViewCompat.isAttachedToWindow(decorView)) {
decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
// we can drop listener now that we know the view is attached
decorView.removeOnAttachStateChangeListener(this);
mDevSupportManager.setDevSupportEnabled(true);
}

@Override
public void onViewDetachedFromWindow(View v) {
// do nothing
}
});
} else {
// activity is attached to window, we can enable dev support immediately
mDevSupportManager.setDevSupportEnabled(true);
}
}

mCurrentActivity = activity;
moveToResumedLifecycleState(false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,83 @@

package com.facebook.react.devsupport;

import javax.annotation.Nullable;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.view.WindowManager;
import android.widget.FrameLayout;

import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.ReactConstants;

import javax.annotation.Nullable;

/**
* Helper class for controlling overlay view with FPS and JS FPS info
* that gets added directly to @{link WindowManager} instance.
*/
/* package */ class DebugOverlayController {

public static void requestPermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show debug overlay in dev builds.
if (!Settings.canDrawOverlays(context)) {
Intent intent = new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.getPackageName()));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
FLog.w(ReactConstants.TAG, "Overlay permissions needs to be granted in order for react native apps to run in dev mode");
if (canHandleIntent(context, intent)) {
context.startActivity(intent);
}
}
}
}

private static boolean permissionCheck(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show debug overlay in dev builds.
if (!Settings.canDrawOverlays(context)) {
// overlay permission not yet granted
return false;
} else {
return true;
}
}
// on pre-M devices permission needs to be specified in manifest
return hasPermission(context, Manifest.permission.SYSTEM_ALERT_WINDOW);
}

private static boolean hasPermission(Context context, String permission) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(),
PackageManager.GET_PERMISSIONS);
if (info.requestedPermissions != null) {
for (String p : info.requestedPermissions) {
if (p.equals(permission)) {
return true;
}
}
}
} catch (PackageManager.NameNotFoundException e) {
FLog.e(ReactConstants.TAG, "Error while retrieving package info", e);
}
return false;
}

private static boolean canHandleIntent(Context context, Intent intent) {
PackageManager packageManager = context.getPackageManager();
return intent.resolveActivity(packageManager) != null;
}

private final WindowManager mWindowManager;
private final ReactContext mReactContext;

Expand All @@ -36,6 +98,10 @@ public DebugOverlayController(ReactContext reactContext) {

public void setFpsDebugViewVisible(boolean fpsDebugViewVisible) {
if (fpsDebugViewVisible && mFPSDebugViewContainer == null) {
if (!permissionCheck(mReactContext)) {
FLog.d(ReactConstants.TAG, "Wait for overlay permission to be set");
return;
}
mFPSDebugViewContainer = new FpsView(mReactContext);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
Expand Down
Loading

0 comments on commit d19afc7

Please sign in to comment.