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

Commit 471293da authored by Hiroki Sato's avatar Hiroki Sato
Browse files

Refactor LetterboxScrollProcessor

This refactors LetterboxScrollProcessor so that all feature related
logic are inside the class and now that InputEventCompatProcessor just
calls its method.

This is a preparation of InputEventCompatProcessor refactoring.

Bug: 369865835
Test: LetterboxScrollProcessorTest InputEventCompatProcessorTest
Flag: EXEMPT refactor
Change-Id: I7424c2778413d8c9d0e47bb30a720d64a2d9e829
parent b50f9440
Loading
Loading
Loading
Loading
+7 −11
Original line number Diff line number Diff line
@@ -21,8 +21,6 @@ import android.os.Handler;
import android.view.input.LetterboxScrollProcessor;
import android.view.input.StylusButtonCompatibility;

import com.android.window.flags.Flags;

import java.util.ArrayList;
import java.util.List;

@@ -54,7 +52,7 @@ public class InputEventCompatProcessor {
        } else {
            mStylusButtonCompatibility = null;
        }
        if (Flags.scrollingFromLetterbox()) {
        if (LetterboxScrollProcessor.isCompatibilityNeeded()) {
            mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext, handler);
        } else {
            mLetterboxScrollProcessor = null;
@@ -79,7 +77,7 @@ public class InputEventCompatProcessor {
        final InputEvent stylusCompatEvent = processStylusButtonCompatibility(inputEvent);

        // Process the event for LetterboxScrollCompatibility.
        List<MotionEvent> letterboxScrollCompatEvents = processLetterboxScrollCompatibility(
        List<InputEvent> letterboxScrollCompatEvents = processLetterboxScrollCompatibility(
                stylusCompatEvent != null ? stylusCompatEvent : inputEvent);

        // If no adjustments are needed for LetterboxCompatibility.
@@ -106,9 +104,9 @@ public class InputEventCompatProcessor {
     * @return The InputEvent to finish, or null if it should not be finished.
     */
    public InputEvent processInputEventBeforeFinish(InputEvent inputEvent) {
        if (mLetterboxScrollProcessor != null && inputEvent instanceof MotionEvent motionEvent) {
        if (mLetterboxScrollProcessor != null) {
            // LetterboxScrollProcessor may have generated events while processing motion events.
            return mLetterboxScrollProcessor.processMotionEventBeforeFinish(motionEvent);
            return mLetterboxScrollProcessor.processInputEventBeforeFinish(inputEvent);
        }

        // No changes needed
@@ -116,11 +114,9 @@ public class InputEventCompatProcessor {
    }


    private List<MotionEvent> processLetterboxScrollCompatibility(InputEvent inputEvent) {
        if (mLetterboxScrollProcessor != null
                && inputEvent instanceof MotionEvent motionEvent
                && motionEvent.getAction() != MotionEvent.ACTION_OUTSIDE) {
            return mLetterboxScrollProcessor.processMotionEvent(motionEvent);
    private List<InputEvent> processLetterboxScrollCompatibility(InputEvent inputEvent) {
        if (mLetterboxScrollProcessor != null) {
            return mLetterboxScrollProcessor.processInputEventForCompatibility(inputEvent);
        }
        return null;
    }
+28 −21
Original line number Diff line number Diff line
@@ -16,8 +16,7 @@

package android.view.input;

import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
@@ -27,9 +26,7 @@ import android.view.InputDevice;
import android.view.InputEvent;
import android.view.MotionEvent;

import androidx.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags.Flags;

import java.util.ArrayList;
import java.util.HashSet;
@@ -38,12 +35,11 @@ import java.util.Objects;
import java.util.Set;

/**
 * {@link MotionEvent} processor that forwards scrolls on the letterbox area to the app's view
 * {@link InputEvent} processor that forwards scrolls on the letterbox area to the app's view
 * hierarchy by translating the coordinates to app's inbound area.
 *
 * @hide
 */
@VisibleForTesting(visibility = PACKAGE)
public class LetterboxScrollProcessor {

    private enum LetterboxScrollState {
@@ -53,11 +49,15 @@ public class LetterboxScrollProcessor {
        SCROLLING_STARTED_OUTSIDE_APP
    }

    @NonNull private LetterboxScrollState mState = LetterboxScrollState.AWAITING_GESTURE_START;
    @NonNull private final List<MotionEvent> mProcessedEvents = new ArrayList<>();
    @NonNull
    private LetterboxScrollState mState = LetterboxScrollState.AWAITING_GESTURE_START;
    @NonNull
    private final List<InputEvent> mProcessedEvents = new ArrayList<>();

    @NonNull private final GestureDetector mScrollDetector;
    @NonNull private final Context mContext;
    @NonNull
    private final GestureDetector mScrollDetector;
    @NonNull
    private final Context mContext;

    /** IDs of events generated from this class */
    private final Set<Integer> mGeneratedEventIds = new HashSet<>();
@@ -67,24 +67,32 @@ public class LetterboxScrollProcessor {
        mScrollDetector = new GestureDetector(context, new ScrollListener(), handler);
    }

    public static boolean isCompatibilityNeeded() {
        return Flags.scrollingFromLetterbox();
    }

    /**
     * Processes the MotionEvent. If the gesture is started in the app's bounds, or moves over the
     * Processes the InputEvent. If the gesture is started in the app's bounds, or moves over the
     * app then the motion events are not adjusted. Motion events from outside the app's
     * bounds that are detected as a scroll gesture are adjusted to be over the app's bounds.
     * Otherwise (if the events are outside the app's bounds and not part of a scroll gesture), the
     * motion events are ignored.
     *
     * @param motionEvent The MotionEvent to process.
     * @param inputEvent The InputEvent to process.
     * @return The list of adjusted events, or null if no adjustments are needed. The list is empty
     * if the event should be ignored. Do not keep a reference to the output as the list is reused.
     */
    @Nullable
    public List<MotionEvent> processMotionEvent(@NonNull MotionEvent motionEvent) {
        if (!motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
            // This is a non-pointer event that doesn't correspond to any location on the screen.
            // Ignore it.
    public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
        if (!(inputEvent instanceof MotionEvent motionEvent)
                || motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE
                || !motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
            return null;
        }
        return processMotionEvent(motionEvent);
    }

    private List<InputEvent> processMotionEvent(@NonNull MotionEvent motionEvent) {
        mProcessedEvents.clear();
        final Rect appBounds = getAppBounds();

@@ -146,13 +154,12 @@ public class LetterboxScrollProcessor {
     * Processes the InputEvent for compatibility before it is finished by calling
     * InputEventReceiver#finishInputEvent().
     *
     * @param motionEvent The MotionEvent to process.
     * @param inputEvent The InputEvent to process.
     * @return The motionEvent to finish, or null if it should not be finished.
     */
    @Nullable
    @VisibleForTesting(visibility = PACKAGE)
    public InputEvent processMotionEventBeforeFinish(@NonNull MotionEvent motionEvent) {
        return mGeneratedEventIds.remove(motionEvent.getId()) ? null : motionEvent;
    public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
        return mGeneratedEventIds.remove(inputEvent.getId()) ? null : inputEvent;
    }

    @NonNull
+26 −22
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import android.view.InputEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

@@ -81,7 +82,7 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ 0f, /* startY= */ 0f);

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

        // Ensure no changes are made to events after processing - event locations should not be
        // adjusted because the gesture started in the app's bounds (for all gestures).
@@ -99,7 +100,7 @@ public class LetterboxScrollProcessorTest {
                dialogBounds);

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

        // Ensure no changes are made to events after processing - the event should be forwarded as
        // normal.
@@ -115,7 +116,7 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ -100f, /* startY= */ -100f);

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

        // All events should be ignored since it was a non-scroll gesture and out of bounds.
        assertEquals(0, processedEvents.size());
@@ -128,7 +129,7 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ 0f, /* startY= */ 0f);

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

        // Ensure no changes are made to events after processing - event locations should not be
        // adjusted because the gesture started in the app's bounds (for all gestures).
@@ -144,7 +145,7 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ 390f, /* startY= */ 790f);

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

        // Ensure no changes are made to events after processing - event locations should not be
        // adjusted because the gesture started in the app's bounds (for all gestures), even if it
@@ -161,18 +162,18 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ -100f, /* startY= */ 0f);

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

        // When a scroll occurs outside bounds: once detected as a scroll, the ACTION_DOWN is
        // expected to be received again but with an offset so it is over the app's bounds.

        // Ensure offset ACTION_DOWN is first event received.
        MotionEvent firstProcessedEvent = processedEvents.getFirst();
        MotionEvent firstProcessedEvent = (MotionEvent) processedEvents.getFirst();
        assertEquals(ACTION_DOWN, firstProcessedEvent.getAction());
        assertEquals(0, firstProcessedEvent.getX(), EPSILON);
        assertEquals(0, firstProcessedEvent.getY(), EPSILON);
        // Ensure this event is not finished (because it was generated by LetterboxScrollProcessor).
        assertNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(firstProcessedEvent));
        assertNull(mLetterboxScrollProcessor.processInputEventBeforeFinish(firstProcessedEvent));
    }

    @Test
@@ -182,7 +183,7 @@ public class LetterboxScrollProcessorTest {
                /* startX= */ -100f, /* startY= */ 0f);

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

        // When a scroll occurs outside bounds: once detected as a scroll, an offset ACTION_DOWN is
        // placed and then the rest of the gesture is offset also. Some ACTION_MOVE events may be
@@ -198,12 +199,12 @@ public class LetterboxScrollProcessorTest {
    }

    @NonNull
    private List<MotionEvent> processMotionEvents(@NonNull List<MotionEvent> motionEvents) {
        final List<MotionEvent> processedEvents = new ArrayList<>();
    private List<InputEvent> processMotionEvents(@NonNull List<MotionEvent> motionEvents) {
        final List<InputEvent> processedEvents = new ArrayList<>();
        for (MotionEvent motionEvent : motionEvents) {
            MotionEvent clonedEvent = MotionEvent.obtain(motionEvent);
            List<MotionEvent> letterboxScrollCompatEvents =
                    mLetterboxScrollProcessor.processMotionEvent(clonedEvent);
            List<InputEvent> letterboxScrollCompatEvents =
                    mLetterboxScrollProcessor.processInputEventForCompatibility(clonedEvent);
            if (letterboxScrollCompatEvents == null) {
                // Use original event if null returned (no adjustments made).
                processedEvents.add(clonedEvent);
@@ -284,35 +285,38 @@ public class LetterboxScrollProcessorTest {

    private void assertEventLocationsAreNotAdjusted(
            @NonNull List<MotionEvent> originalEvents,
            @NonNull List<MotionEvent> processedEvents) {
            @NonNull List<InputEvent> processedEvents) {
        assertEquals("MotionEvent arrays are not the same size",
                originalEvents.size(), processedEvents.size());

        for (int i = 0; i < originalEvents.size(); i++) {
            assertEquals("X coordinates was unexpectedly adjusted at index " + i,
                    originalEvents.get(i).getX(), processedEvents.get(i).getX(), EPSILON);
                    originalEvents.get(i).getX(), ((MotionEvent) processedEvents.get(i)).getX(),
                    EPSILON);
            assertEquals("Y coordinates was unexpectedly adjusted at index " + i,
                    originalEvents.get(i).getY(), processedEvents.get(i).getY(), EPSILON);
                    originalEvents.get(i).getY(), ((MotionEvent) processedEvents.get(i)).getY(),
                    EPSILON);
        }
    }

    private void assertXCoordinatesAdjustedToZero(
            @NonNull List<MotionEvent> originalEvents,
            @NonNull List<MotionEvent> processedEvents) {
            @NonNull List<InputEvent> processedEvents) {
        assertEquals("MotionEvent arrays are not the same size",
                originalEvents.size(), processedEvents.size());

        for (int i = 0; i < originalEvents.size(); i++) {
            assertEquals("X coordinate was not adjusted to 0 at index " + i,
                    0, processedEvents.get(i).getX(), EPSILON);
                    0, ((MotionEvent) processedEvents.get(i)).getX(), EPSILON);
            assertEquals("Y coordinate was unexpectedly adjusted at index " + i,
                    originalEvents.get(i).getY(), processedEvents.get(i).getY(), EPSILON);
                    originalEvents.get(i).getY(), ((MotionEvent) processedEvents.get(i)).getY(),
                    EPSILON);
        }
    }

    private void assertMotionEventsShouldBeFinished(@NonNull List<MotionEvent> processedEvents) {
        for (MotionEvent processedEvent : processedEvents) {
            assertNotNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(processedEvent));
    private void assertMotionEventsShouldBeFinished(@NonNull List<InputEvent> processedEvents) {
        for (InputEvent processedEvent : processedEvents) {
            assertNotNull(mLetterboxScrollProcessor.processInputEventBeforeFinish(processedEvent));
        }
    }
}