Commit 988db385 authored by Pavel Yatsuk's avatar Pavel Yatsuk Committed by Commit Bot

[ModalDialog] Touch filtering for obscured button

Add support for touch filtering for buttons obscured by another visible
window. This change is needed for migrating permission dialogs to
ModalDialogManager.

In this change:
- Add boolean property FILTER_TOUCH_FOR_SECURITY to ModalDialogProperties
- Add logic in ModalDialogView to filter touch events
- Propagate the property to ModalDialogView through ModalDialogViewBinder

BUG=940046

Change-Id: I6f363f3922c3ba2883f24b376347f305590478a7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1524909Reviewed-by: default avatarTheresa <twellington@chromium.org>
Reviewed-by: default avatarBecky Zhou <huayinz@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Commit-Queue: Pavel Yatsuk <pavely@chromium.org>
Cr-Commit-Position: refs/heads/master@{#642726}
parent b88341fe
...@@ -6,8 +6,10 @@ package org.chromium.chrome.browser.modaldialog; ...@@ -6,8 +6,10 @@ package org.chromium.chrome.browser.modaldialog;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
...@@ -15,16 +17,21 @@ import android.widget.ImageView; ...@@ -15,16 +17,21 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.BoundedLinearLayout; import org.chromium.chrome.browser.widget.BoundedLinearLayout;
import org.chromium.chrome.browser.widget.FadingEdgeScrollView; import org.chromium.chrome.browser.widget.FadingEdgeScrollView;
import org.chromium.ui.UiUtils; import org.chromium.ui.UiUtils;
import org.chromium.ui.modaldialog.ModalDialogProperties; import org.chromium.ui.modaldialog.ModalDialogProperties;
import java.lang.reflect.Field;
/** /**
* Generic dialog view for app modal or tab modal alert dialogs. * Generic dialog view for app modal or tab modal alert dialogs.
*/ */
public class ModalDialogView extends BoundedLinearLayout implements View.OnClickListener { public class ModalDialogView extends BoundedLinearLayout implements View.OnClickListener {
private static final String TAG = "ModalDialogView";
private ModalDialogProperties.Controller mController; private ModalDialogProperties.Controller mController;
private FadingEdgeScrollView mScrollView; private FadingEdgeScrollView mScrollView;
...@@ -38,6 +45,7 @@ public class ModalDialogView extends BoundedLinearLayout implements View.OnClick ...@@ -38,6 +45,7 @@ public class ModalDialogView extends BoundedLinearLayout implements View.OnClick
private Button mNegativeButton; private Button mNegativeButton;
private Callback<Integer> mOnButtonClickedCallback; private Callback<Integer> mOnButtonClickedCallback;
private boolean mTitleScrollable; private boolean mTitleScrollable;
private boolean mFilterTouchForSecurity;
/** /**
* Constructor for inflating from XML. * Constructor for inflating from XML.
...@@ -157,6 +165,45 @@ public class ModalDialogView extends BoundedLinearLayout implements View.OnClick ...@@ -157,6 +165,45 @@ public class ModalDialogView extends BoundedLinearLayout implements View.OnClick
mCustomViewContainer.setLayoutParams(layoutParams); mCustomViewContainer.setLayoutParams(layoutParams);
} }
/**
* @param filterTouchForSecurity Whether button touch events should be filtered when buttons are
* obscured by another visible window.
*/
void setFilterTouchForSecurity(boolean filterTouchForSecurity) {
if (mFilterTouchForSecurity == filterTouchForSecurity) return;
mFilterTouchForSecurity = filterTouchForSecurity;
if (filterTouchForSecurity) {
setupFilterTouchForSecurity();
} else {
assert false : "Shouldn't remove touch filter after setting it up";
}
}
/** Setup touch filters to block events when buttons are obscured by another window. */
private void setupFilterTouchForSecurity() {
Button positiveButton = getButton(ModalDialogProperties.ButtonType.POSITIVE);
Button negativeButton = getButton(ModalDialogProperties.ButtonType.NEGATIVE);
View.OnTouchListener onTouchListener = (View v, MotionEvent ev) -> {
// Filter touch events based MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED which is
// introduced on M+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;
try {
Field field = MotionEvent.class.getField("FLAG_WINDOW_IS_PARTIALLY_OBSCURED");
if ((ev.getFlags() & field.getInt(null)) != 0) return true;
} catch (NoSuchFieldException | IllegalAccessException e) {
Log.e(TAG, "Reflection failure: " + e);
}
return false;
};
positiveButton.setFilterTouchesWhenObscured(true);
positiveButton.setOnTouchListener(onTouchListener);
negativeButton.setFilterTouchesWhenObscured(true);
negativeButton.setOnTouchListener(onTouchListener);
}
/** @param message The message in the dialog content. */ /** @param message The message in the dialog content. */
void setMessage(String message) { void setMessage(String message) {
mMessageView.setText(message); mMessageView.setText(message);
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
package org.chromium.chrome.browser.modaldialog; package org.chromium.chrome.browser.modaldialog;
import android.text.TextUtils;
import org.chromium.ui.modaldialog.ModalDialogProperties; import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
...@@ -27,12 +29,14 @@ public class ModalDialogViewBinder ...@@ -27,12 +29,14 @@ public class ModalDialogViewBinder
} else if (ModalDialogProperties.CUSTOM_VIEW == propertyKey) { } else if (ModalDialogProperties.CUSTOM_VIEW == propertyKey) {
view.setCustomView(model.get(ModalDialogProperties.CUSTOM_VIEW)); view.setCustomView(model.get(ModalDialogProperties.CUSTOM_VIEW));
} else if (ModalDialogProperties.POSITIVE_BUTTON_TEXT == propertyKey) { } else if (ModalDialogProperties.POSITIVE_BUTTON_TEXT == propertyKey) {
assert checkFilterTouchConsistency(model);
view.setButtonText(ModalDialogProperties.ButtonType.POSITIVE, view.setButtonText(ModalDialogProperties.ButtonType.POSITIVE,
model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT)); model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT));
} else if (ModalDialogProperties.POSITIVE_BUTTON_DISABLED == propertyKey) { } else if (ModalDialogProperties.POSITIVE_BUTTON_DISABLED == propertyKey) {
view.setButtonEnabled(ModalDialogProperties.ButtonType.POSITIVE, view.setButtonEnabled(ModalDialogProperties.ButtonType.POSITIVE,
!model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)); !model.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED));
} else if (ModalDialogProperties.NEGATIVE_BUTTON_TEXT == propertyKey) { } else if (ModalDialogProperties.NEGATIVE_BUTTON_TEXT == propertyKey) {
assert checkFilterTouchConsistency(model);
view.setButtonText(ModalDialogProperties.ButtonType.NEGATIVE, view.setButtonText(ModalDialogProperties.ButtonType.NEGATIVE,
model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT)); model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
} else if (ModalDialogProperties.NEGATIVE_BUTTON_DISABLED == propertyKey) { } else if (ModalDialogProperties.NEGATIVE_BUTTON_DISABLED == propertyKey) {
...@@ -46,10 +50,26 @@ public class ModalDialogViewBinder ...@@ -46,10 +50,26 @@ public class ModalDialogViewBinder
}); });
} else if (ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE == propertyKey) { } else if (ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE == propertyKey) {
// Intentionally left empty since this is a property for the dialog container. // Intentionally left empty since this is a property for the dialog container.
} else if (ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY == propertyKey) {
assert checkFilterTouchConsistency(model);
view.setFilterTouchForSecurity(
model.get(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY));
} else if (ModalDialogProperties.CONTENT_DESCRIPTION == propertyKey) { } else if (ModalDialogProperties.CONTENT_DESCRIPTION == propertyKey) {
// Intentionally left empty since this is a property used for the dialog container. // Intentionally left empty since this is a property used for the dialog container.
} else { } else {
assert false : "Unhandled property detected in ModalDialogViewBinder!"; assert false : "Unhandled property detected in ModalDialogViewBinder!";
} }
} }
/**
* Checks if FILTER_TOUCH_FOR_SECURITY flag is consistent with the set of enabled buttons.
* Touch event filtering in ModalDialogView is only applied to standard buttons. When buttons
* are hidden, filtering touch events doesn't have effect.
* @return false if security sensitive dialog doesn't have standard buttons.
*/
static boolean checkFilterTouchConsistency(PropertyModel model) {
return !model.get(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY)
|| !TextUtils.isEmpty(model.get(ModalDialogProperties.POSITIVE_BUTTON_TEXT))
|| !TextUtils.isEmpty(model.get(ModalDialogProperties.NEGATIVE_BUTTON_TEXT));
}
} }
...@@ -44,6 +44,7 @@ class PermissionAppModalDialogView { ...@@ -44,6 +44,7 @@ class PermissionAppModalDialogView {
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
delegate.getSecondaryButtonText()) delegate.getSecondaryButtonText())
.with(ModalDialogProperties.CONTENT_DESCRIPTION, delegate.getMessageText()) .with(ModalDialogProperties.CONTENT_DESCRIPTION, delegate.getMessageText())
.with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true)
.build(); .build();
} }
......
...@@ -23,10 +23,14 @@ import android.support.test.filters.MediumTest; ...@@ -23,10 +23,14 @@ import android.support.test.filters.MediumTest;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
...@@ -335,6 +339,46 @@ public class ModalDialogViewTest { ...@@ -335,6 +339,46 @@ public class ModalDialogViewTest {
onView(withId(R.id.negative_button)).check(matches(not(isDisplayed()))); onView(withId(R.id.negative_button)).check(matches(not(isDisplayed())));
} }
@Test
@MediumTest
@Feature({"ModalDialog"})
public void testTouchFilter() {
PropertyModel model = createModel(
mModelBuilder
.with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, mResources, R.string.ok)
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, mResources,
R.string.cancel)
.with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true));
onView(withId(R.id.positive_button)).check(matches(touchFilterEnabled()));
onView(withId(R.id.negative_button)).check(matches(touchFilterEnabled()));
}
@Test
@MediumTest
@Feature({"ModalDialog"})
public void testTouchFilterDisabled() {
PropertyModel model = createModel(
mModelBuilder
.with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, mResources, R.string.ok)
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, mResources,
R.string.cancel));
onView(withId(R.id.positive_button)).check(matches(not(touchFilterEnabled())));
onView(withId(R.id.negative_button)).check(matches(not(touchFilterEnabled())));
}
private static Matcher<View> touchFilterEnabled() {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Touch filtering enabled");
}
@Override
public boolean matchesSafely(View view) {
return view.getFilterTouchesWhenObscured();
}
};
}
private PropertyModel createModel(PropertyModel.Builder modelBuilder) { private PropertyModel createModel(PropertyModel.Builder modelBuilder) {
return ThreadUtils.runOnUiThreadBlockingNoException(() -> { return ThreadUtils.runOnUiThreadBlockingNoException(() -> {
PropertyModel model = modelBuilder.build(); PropertyModel model = modelBuilder.build();
......
...@@ -10,6 +10,7 @@ import android.view.View; ...@@ -10,6 +10,7 @@ import android.view.View;
import org.chromium.ui.modelutil.PropertyKey; import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel; import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModel.ReadableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.ReadableObjectPropertyKey; import org.chromium.ui.modelutil.PropertyModel.ReadableObjectPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableBooleanPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey; import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
...@@ -96,6 +97,13 @@ public class ModalDialogProperties { ...@@ -96,6 +97,13 @@ public class ModalDialogProperties {
public static final WritableBooleanPropertyKey CANCEL_ON_TOUCH_OUTSIDE = public static final WritableBooleanPropertyKey CANCEL_ON_TOUCH_OUTSIDE =
new WritableBooleanPropertyKey(); new WritableBooleanPropertyKey();
/**
* Whether button touch events should be filtered when buttons are obscured by another visible
* window.
*/
public static final ReadableBooleanPropertyKey FILTER_TOUCH_FOR_SECURITY =
new ReadableBooleanPropertyKey();
/** Whether the title is scrollable with the message. */ /** Whether the title is scrollable with the message. */
public static final WritableBooleanPropertyKey TITLE_SCROLLABLE = public static final WritableBooleanPropertyKey TITLE_SCROLLABLE =
new WritableBooleanPropertyKey(); new WritableBooleanPropertyKey();
...@@ -103,5 +111,5 @@ public class ModalDialogProperties { ...@@ -103,5 +111,5 @@ public class ModalDialogProperties {
public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {CONTROLLER, CONTENT_DESCRIPTION, public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {CONTROLLER, CONTENT_DESCRIPTION,
TITLE, TITLE_ICON, MESSAGE, CUSTOM_VIEW, POSITIVE_BUTTON_TEXT, POSITIVE_BUTTON_DISABLED, TITLE, TITLE_ICON, MESSAGE, CUSTOM_VIEW, POSITIVE_BUTTON_TEXT, POSITIVE_BUTTON_DISABLED,
NEGATIVE_BUTTON_TEXT, NEGATIVE_BUTTON_DISABLED, CANCEL_ON_TOUCH_OUTSIDE, NEGATIVE_BUTTON_TEXT, NEGATIVE_BUTTON_DISABLED, CANCEL_ON_TOUCH_OUTSIDE,
TITLE_SCROLLABLE}; FILTER_TOUCH_FOR_SECURITY, TITLE_SCROLLABLE};
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment