Skip to content

Commit d19afc7

Browse files
kmagierafacebook-github-bot
authored andcommitted
Use currentActivity to display redbox, loading view and dev menu
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
1 parent f59140e commit d19afc7

7 files changed

+229
-110
lines changed

ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java

+1-34
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@
66
import android.app.Activity;
77
import android.content.Context;
88
import android.content.Intent;
9-
import android.net.Uri;
109
import android.os.Build;
1110
import android.os.Bundle;
12-
import android.provider.Settings;
1311
import android.support.v4.app.FragmentActivity;
1412
import android.view.KeyEvent;
15-
import android.widget.Toast;
1613

17-
import com.facebook.common.logging.FLog;
1814
import com.facebook.infer.annotation.Assertions;
1915
import com.facebook.react.bridge.Callback;
20-
import com.facebook.react.common.ReactConstants;
2116
import com.facebook.react.devsupport.DoubleTapReloadRecognizer;
2217
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
2318
import com.facebook.react.modules.core.PermissionListener;
@@ -31,12 +26,6 @@
3126
*/
3227
public class ReactActivityDelegate {
3328

34-
private final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
35-
private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
36-
"Overlay permissions have been granted.";
37-
private static final String REDBOX_PERMISSION_MESSAGE =
38-
"Overlay permissions needs to be granted in order for react native apps to run in dev mode";
39-
4029
private final @Nullable Activity mActivity;
4130
private final @Nullable FragmentActivity mFragmentActivity;
4231
private final @Nullable String mMainComponentName;
@@ -84,19 +73,7 @@ public ReactInstanceManager getReactInstanceManager() {
8473
}
8574

8675
protected void onCreate(Bundle savedInstanceState) {
87-
boolean needsOverlayPermission = false;
88-
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
89-
// Get permission to show redbox in dev builds.
90-
if (!Settings.canDrawOverlays(getContext())) {
91-
needsOverlayPermission = true;
92-
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
93-
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
94-
Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
95-
((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
96-
}
97-
}
98-
99-
if (mMainComponentName != null && !needsOverlayPermission) {
76+
if (mMainComponentName != null) {
10077
loadApp(mMainComponentName);
10178
}
10279
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
@@ -147,16 +124,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
147124
if (getReactNativeHost().hasInstance()) {
148125
getReactNativeHost().getReactInstanceManager()
149126
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
150-
} else {
151-
// Did we request overlay permissions?
152-
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
153-
if (Settings.canDrawOverlays(getContext())) {
154-
if (mMainComponentName != null) {
155-
loadApp(mMainComponentName);
156-
}
157-
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
158-
}
159-
}
160127
}
161128
}
162129

ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java

+41-6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import android.content.Intent;
3737
import android.net.Uri;
3838
import android.os.Process;
39+
import android.support.v4.view.ViewCompat;
3940
import android.util.Log;
4041
import android.view.View;
4142
import com.facebook.common.logging.FLog;
@@ -65,7 +66,7 @@
6566
import com.facebook.react.common.ReactConstants;
6667
import com.facebook.react.common.annotations.VisibleForTesting;
6768
import com.facebook.react.devsupport.DevSupportManagerFactory;
68-
import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler;
69+
import com.facebook.react.devsupport.ReactInstanceManagerDevHelper;
6970
import com.facebook.react.devsupport.RedBoxHandler;
7071
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
7172
import com.facebook.react.devsupport.interfaces.DevSupportManager;
@@ -221,7 +222,7 @@ public static ReactInstanceManagerBuilder builder() {
221222
mDevSupportManager =
222223
DevSupportManagerFactory.create(
223224
applicationContext,
224-
createDevInterface(),
225+
createDevHelperInterface(),
225226
mJSMainModulePath,
226227
useDeveloperSupport,
227228
redBoxHandler,
@@ -261,8 +262,8 @@ public void invokeDefaultOnBackPressed() {
261262
}
262263
}
263264

264-
private ReactInstanceDevCommandsHandler createDevInterface() {
265-
return new ReactInstanceDevCommandsHandler() {
265+
private ReactInstanceManagerDevHelper createDevHelperInterface() {
266+
return new ReactInstanceManagerDevHelper() {
266267
@Override
267268
public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
268269
ReactInstanceManager.this.onReloadWithJSDebugger(jsExecutorFactory);
@@ -277,6 +278,11 @@ public void onJSBundleLoadedFromServer() {
277278
public void toggleElementInspector() {
278279
ReactInstanceManager.this.toggleElementInspector();
279280
}
281+
282+
@Override
283+
public @Nullable Activity getCurrentActivity() {
284+
return ReactInstanceManager.this.mCurrentActivity;
285+
}
280286
};
281287
}
282288

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

565571
mDefaultBackButtonImpl = defaultBackButtonImpl;
572+
mCurrentActivity = activity;
573+
566574
if (mUseDeveloperSupport) {
567-
mDevSupportManager.setDevSupportEnabled(true);
575+
// Resume can be called from one of two different states:
576+
// a) when activity was paused
577+
// b) when activity has just been created
578+
// In case of (a) the activity is attached to window and it is ok to add new views to it or
579+
// open dialogs. In case of (b) there is often a slight delay before such a thing happens.
580+
// As dev support manager can add views or open dialogs immediately after it gets enabled
581+
// (e.g. in the case when JS bundle is being fetched in background) we only want to enable
582+
// it once we know for sure the current activity is attached.
583+
584+
// We check if activity is attached to window by checking if decor view is attached
585+
final View decorView = mCurrentActivity.getWindow().getDecorView();
586+
if (!ViewCompat.isAttachedToWindow(decorView)) {
587+
decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
588+
@Override
589+
public void onViewAttachedToWindow(View v) {
590+
// we can drop listener now that we know the view is attached
591+
decorView.removeOnAttachStateChangeListener(this);
592+
mDevSupportManager.setDevSupportEnabled(true);
593+
}
594+
595+
@Override
596+
public void onViewDetachedFromWindow(View v) {
597+
// do nothing
598+
}
599+
});
600+
} else {
601+
// activity is attached to window, we can enable dev support immediately
602+
mDevSupportManager.setDevSupportEnabled(true);
603+
}
568604
}
569605

570-
mCurrentActivity = activity;
571606
moveToResumedLifecycleState(false);
572607
}
573608

ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java

+68-2
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,83 @@
99

1010
package com.facebook.react.devsupport;
1111

12-
import javax.annotation.Nullable;
13-
12+
import android.Manifest;
1413
import android.content.Context;
14+
import android.content.Intent;
15+
import android.content.pm.PackageInfo;
16+
import android.content.pm.PackageManager;
1517
import android.graphics.PixelFormat;
18+
import android.net.Uri;
19+
import android.os.Build;
20+
import android.provider.Settings;
1621
import android.view.WindowManager;
1722
import android.widget.FrameLayout;
1823

24+
import com.facebook.common.logging.FLog;
1925
import com.facebook.react.bridge.ReactContext;
26+
import com.facebook.react.common.ReactConstants;
27+
28+
import javax.annotation.Nullable;
2029

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

36+
public static void requestPermission(Context context) {
37+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
38+
// Get permission to show debug overlay in dev builds.
39+
if (!Settings.canDrawOverlays(context)) {
40+
Intent intent = new Intent(
41+
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
42+
Uri.parse("package:" + context.getPackageName()));
43+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
44+
FLog.w(ReactConstants.TAG, "Overlay permissions needs to be granted in order for react native apps to run in dev mode");
45+
if (canHandleIntent(context, intent)) {
46+
context.startActivity(intent);
47+
}
48+
}
49+
}
50+
}
51+
52+
private static boolean permissionCheck(Context context) {
53+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
54+
// Get permission to show debug overlay in dev builds.
55+
if (!Settings.canDrawOverlays(context)) {
56+
// overlay permission not yet granted
57+
return false;
58+
} else {
59+
return true;
60+
}
61+
}
62+
// on pre-M devices permission needs to be specified in manifest
63+
return hasPermission(context, Manifest.permission.SYSTEM_ALERT_WINDOW);
64+
}
65+
66+
private static boolean hasPermission(Context context, String permission) {
67+
try {
68+
PackageInfo info = context.getPackageManager().getPackageInfo(
69+
context.getPackageName(),
70+
PackageManager.GET_PERMISSIONS);
71+
if (info.requestedPermissions != null) {
72+
for (String p : info.requestedPermissions) {
73+
if (p.equals(permission)) {
74+
return true;
75+
}
76+
}
77+
}
78+
} catch (PackageManager.NameNotFoundException e) {
79+
FLog.e(ReactConstants.TAG, "Error while retrieving package info", e);
80+
}
81+
return false;
82+
}
83+
84+
private static boolean canHandleIntent(Context context, Intent intent) {
85+
PackageManager packageManager = context.getPackageManager();
86+
return intent.resolveActivity(packageManager) != null;
87+
}
88+
2789
private final WindowManager mWindowManager;
2890
private final ReactContext mReactContext;
2991

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

3799
public void setFpsDebugViewVisible(boolean fpsDebugViewVisible) {
38100
if (fpsDebugViewVisible && mFPSDebugViewContainer == null) {
101+
if (!permissionCheck(mReactContext)) {
102+
FLog.d(ReactConstants.TAG, "Wait for overlay permission to be set");
103+
return;
104+
}
39105
mFPSDebugViewContainer = new FpsView(mReactContext);
40106
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
41107
WindowManager.LayoutParams.MATCH_PARENT,

0 commit comments

Comments
 (0)