Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 44cb1941 authored by Mina Granic's avatar Mina Granic
Browse files

Check event in app's bounds using absolute coordinates.

Event's X and Y coordinates are relative to the current window,
not necessarily the app, meaning that windows with different
coordinate systems than the apps - for example dialogs -
would have incorrect calculations if used. Also, even if the tap
is outside the current window's bounds, but inside app's bounds,
it should be forwarded to the app (for example to trigger dialog
dismiss).

Flag: com.android.window.flags.scrolling_from_letterbox
Test: atest FrameworksCoreTests:LetterboxScrollProcessorTest
Test: verified dialogs are dismissable.
Fixes: 372918146
Change-Id: I01f57aacf8b2aa04854f76c0d5cb0417b797a8aa
parent 882509d8
Loading
Loading
Loading
Loading
+13 −3
Original line number Diff line number Diff line
@@ -145,9 +145,19 @@ public class LetterboxScrollProcessor {
        return mContext.getResources().getConfiguration().windowConfiguration.getBounds();
    }

    private boolean isOutsideAppBounds(MotionEvent motionEvent, Rect appBounds) {
        return motionEvent.getX() < 0 || motionEvent.getX() >= appBounds.width()
                || motionEvent.getY() < 0 || motionEvent.getY() >= appBounds.height();
    /** Checks whether the gesture is located on the letterbox area. */
    private boolean isOutsideAppBounds(@NonNull MotionEvent motionEvent, @NonNull Rect appBounds) {
        // The events are in the coordinate system of the ViewRootImpl (window). The window might
        // not have the same dimensions as the app bounds - for example in case of Dialogs - thus
        // `getRawX()` and `getRawY()` are used, with the absolute bounds (left, top, etc) instead
        // of width and height.
        // The event should be passed to the app if it has happened anywhere in the app area,
        // irrespective of the current window size, therefore the app bounds are used instead of the
        // current window.
        return motionEvent.getRawX() < appBounds.left
                || motionEvent.getRawX() >= appBounds.right
                || motionEvent.getRawY() < appBounds.top
                || motionEvent.getRawY() >= appBounds.bottom;
    }

    private void applyOffset(MotionEvent event, Rect appBounds) {
+62 −6
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;

import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

@@ -57,13 +58,15 @@ public class LetterboxScrollProcessorTest {
    // Constant delta used when comparing coordinates (floats)
    private static final float EPSILON = 0.1f;

    private static final Rect APP_BOUNDS =
            new Rect(/* left= */ 200, /* top= */ 200, /* right= */ 600, /* bottom= */ 1000);

    @Before
    public void setUp() {
        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

        // Set app bounds as if it was letterboxed.
        mContext.getResources().getConfiguration().windowConfiguration
                .setBounds(new Rect(200, 200, 600, 1000));
        mContext.getResources().getConfiguration().windowConfiguration.setBounds(APP_BOUNDS);

        Handler handler = new Handler(Looper.getMainLooper());

@@ -86,6 +89,24 @@ public class LetterboxScrollProcessorTest {
        assertMotionEventsShouldBeFinished(processedEvents);
    }

    @Test
    public void testGestureInAppBoundsButOutsideTopWindowAlsoForwardedToTheApp() {
        final Rect dialogBounds =
                new Rect(/* left= */ 300, /* top= */ 500, /* right= */ 500, /* bottom= */ 700);
        // Tap-like gesture outside the dialog, but in app bounds.
        List<MotionEvent> tapGestureEvents = createTapGestureEventsWithCoordinateSystem(0f, 0f,
                dialogBounds);

        // Get processed events from Letterbox Scroll Processor.
        List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);

        // Ensure no changes are made to events after processing - the event should be forwarded as
        // normal.
        assertEventLocationsAreNotAdjusted(tapGestureEvents, processedEvents);
        // Ensure all of these events should be finished (expect no generated events).
        assertMotionEventsShouldBeFinished(processedEvents);
    }

    @Test
    public void testGestureOutsideBoundsIsIgnored() {
        // Tap-like gesture outside bounds (non-scroll).
@@ -187,11 +208,25 @@ public class LetterboxScrollProcessorTest {
        return processedEvents;
    }

    /**
     * Creates and returns a tap gesture with X and Y in reference to the app bounds (top left
     * corner is x=0, y=0).
     */
    private List<MotionEvent> createTapGestureEvents(float startX, float startY) {
        return createTapGestureEventsWithCoordinateSystem(startX, startY, APP_BOUNDS);
    }

    /**
     * @param referenceWindowBounds the amount the event will be translated by.
     */
    private List<MotionEvent> createTapGestureEventsWithCoordinateSystem(float startX, float startY,
            @NonNull Rect referenceWindowBounds) {
        // Events for tap-like gesture (non-scroll)
        List<MotionEvent> motionEvents = new ArrayList<>();
        motionEvents.add(createBasicMotionEvent(0, ACTION_DOWN, startX, startY));
        motionEvents.add(createBasicMotionEvent(10, ACTION_UP, startX , startY));
        motionEvents.add(createBasicMotionEventWithCoordinateSystem(0, ACTION_DOWN,
                startX, startY, referenceWindowBounds));
        motionEvents.add(createBasicMotionEventWithCoordinateSystem(10, ACTION_UP,
                startX , startY, referenceWindowBounds));
        return motionEvents;
    }

@@ -213,8 +248,29 @@ public class LetterboxScrollProcessorTest {
        return motionEvents;
    }

    private MotionEvent createBasicMotionEvent(int downTime, int action, float x, float y) {
        return MotionEvent.obtain(0, downTime, action, x, y, 0);
    /**
     * Creates and returns an event with X and Y in reference to the app bounds (top left corner is
     * x=0, y=0).
     */
    @NonNull
    private MotionEvent createBasicMotionEvent(int eventTime, int action, float x, float y) {
        return createBasicMotionEventWithCoordinateSystem(eventTime, action, x, y, APP_BOUNDS);
    }

    /**
     * @param referenceWindowBounds the amount the event will be translated by.
     */
    @NonNull
    private MotionEvent createBasicMotionEventWithCoordinateSystem(int eventTime, int action,
            float x, float y, @NonNull Rect referenceWindowBounds) {
        final float rawX = referenceWindowBounds.left + x;
        final float rawY = referenceWindowBounds.top + y;
        // RawX and RawY cannot be changed once the event is created. Therefore, pass rawX and rawY
        // according to the app's bounds on the display, and then offset to make X and Y relative to
        // the app's bounds.
        final MotionEvent event = MotionEvent.obtain(0, eventTime, action, rawX, rawY, 0);
        event.offsetLocation(-referenceWindowBounds.left, -referenceWindowBounds.top);
        return event;
    }

    private void assertEventLocationsAreNotAdjusted(