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

Commit 06128c24 authored by Hiroki Sato's avatar Hiroki Sato
Browse files

InputEventCompatProcessor chaining

This introduces InputEventCompatHandler, a wrapper of
InputEventCompatProcessor.
Now that each compatibility feature extends the Processor class, and the
Handler class will form a chain of reponsibility to rewrite events if
necessary.

The goal of this change is, while keeping the current
InputEventCompatProcessor interface, allowing the chain of processors.

Bug: 369865835
Test: InputEventCompatHandlerTest LetterboxScrollProcessorTest StylusButtonCompatibilityTest
Flag: EXEMPT refactor
Change-Id: I52adf5c1ed42fce6b9021d2f5571f4330c2b5fbf
parent c0cfadf9
Loading
Loading
Loading
Loading
+11 −71
Original line number Diff line number Diff line
@@ -16,29 +16,23 @@

package android.view;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Handler;
import android.view.input.LetterboxScrollProcessor;
import android.view.input.StylusButtonCompatibility;

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

/**
 * Compatibility processor for InputEvents that allows events to be adjusted before and
 * after it is sent to the application.
 *
 * {@hide}
 * @hide
 */
public class InputEventCompatProcessor {
public abstract class InputEventCompatProcessor {

    protected Context mContext;
    protected int mTargetSdkVersion;
    private final StylusButtonCompatibility mStylusButtonCompatibility;
    private final LetterboxScrollProcessor mLetterboxScrollProcessor;

    /** List of events to be used to return the processed events */
    private final List<InputEvent> mProcessedEvents;

    public InputEventCompatProcessor(Context context) {
        this(context, null);
@@ -47,84 +41,30 @@ public class InputEventCompatProcessor {
    public InputEventCompatProcessor(Context context, Handler handler) {
        mContext = context;
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        if (StylusButtonCompatibility.isCompatibilityNeeded(context)) {
            mStylusButtonCompatibility = new StylusButtonCompatibility();
        } else {
            mStylusButtonCompatibility = null;
        }
        if (LetterboxScrollProcessor.isCompatibilityNeeded()) {
            mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext, handler);
        } else {
            mLetterboxScrollProcessor = null;
        }

        mProcessedEvents = new ArrayList<>();
    }


    /**
     * Processes the InputEvent for compatibility before it is sent to the app, allowing for the
     * Process the InputEvent for compatibility before it is sent to the app, allowing for the
     * generation of more than one event if necessary.
     *
     * @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.
     */
    public List<InputEvent> processInputEventForCompatibility(InputEvent inputEvent) {
        mProcessedEvents.clear();

        // Process the event for StylusButtonCompatibility.
        final InputEvent stylusCompatEvent = processStylusButtonCompatibility(inputEvent);

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

        // If no adjustments are needed for LetterboxCompatibility.
        if (letterboxScrollCompatEvents == null) {
            // If stylus compatibility made adjustments, return that adjusted event.
            if (stylusCompatEvent != null) {
                mProcessedEvents.add(stylusCompatEvent);
                return mProcessedEvents;
            }
            // Otherwise, return null to indicate no adjustments.
    @Nullable
    public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
        return null;
    }

        // Otherwise if LetterboxCompatibility made adjustments, return the list of adjusted events.
        mProcessedEvents.addAll(letterboxScrollCompatEvents);
        return mProcessedEvents;
    }

    /**
     * Processes the InputEvent for compatibility before it is finished by calling
     * Process the InputEvent for compatibility before it is finished by calling
     * InputEventReceiver#finishInputEvent().
     *
     * @param inputEvent The InputEvent to process.
     * @return The InputEvent to finish, or null if it should not be finished.
     */
    public InputEvent processInputEventBeforeFinish(InputEvent inputEvent) {
        if (mLetterboxScrollProcessor != null) {
            // LetterboxScrollProcessor may have generated events while processing motion events.
            return mLetterboxScrollProcessor.processInputEventBeforeFinish(inputEvent);
        }

        // No changes needed
    @Nullable
    public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
        return inputEvent;
    }


    private List<InputEvent> processLetterboxScrollCompatibility(InputEvent inputEvent) {
        if (mLetterboxScrollProcessor != null) {
            return mLetterboxScrollProcessor.processInputEventForCompatibility(inputEvent);
        }
        return null;
    }

    private InputEvent processStylusButtonCompatibility(InputEvent inputEvent) {
        if (mStylusButtonCompatibility != null) {
            return mStylusButtonCompatibility.processInputEventForCompatibility(inputEvent);
        }
        return null;
    }
}
+15 −30
Original line number Diff line number Diff line
@@ -36,8 +36,8 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST;
import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
import static android.view.View.FRAME_RATE_CATEGORY_REASON_BOOST;
import static android.view.View.FRAME_RATE_CATEGORY_REASON_CONFLICTED;
import static android.view.View.FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
@@ -263,6 +263,7 @@ import android.view.autofill.AutofillManager;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.view.flags.Flags;
import android.view.input.InputEventCompatHandler;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
@@ -978,7 +979,7 @@ public final class ViewRootImpl implements ViewParent,
    private boolean mNeedsRendererSetup;
    private final InputEventCompatProcessor mInputCompatProcessor;
    private final InputEventCompatHandler mInputCompatHandler;
    /**
     * Consistency verifier for debugging purposes.
@@ -1282,24 +1283,7 @@ public final class ViewRootImpl implements ViewParent,
        initializeProtoLogInProcess();
        String processorOverrideName = context.getResources().getString(
                                    R.string.config_inputEventCompatProcessorOverrideClassName);
        if (processorOverrideName.isEmpty()) {
            // No compatibility processor override, using default.
            mInputCompatProcessor = new InputEventCompatProcessor(context, mHandler);
        } else {
            InputEventCompatProcessor compatProcessor = null;
            try {
                final Class<? extends InputEventCompatProcessor> klass =
                        (Class<? extends InputEventCompatProcessor>) Class.forName(
                                processorOverrideName);
                compatProcessor = klass.getConstructor(Context.class).newInstance(context);
            } catch (Exception e) {
                Log.e(TAG, "Unable to create the InputEventCompatProcessor. ", e);
            } finally {
                mInputCompatProcessor = compatProcessor;
            }
        }
        mInputCompatHandler = InputEventCompatHandler.buildChain(context, mHandler);
        if (!sCompatibilityDone) {
            sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;
@@ -10584,12 +10568,12 @@ public final class ViewRootImpl implements ViewParent,
        if (q.mReceiver != null) {
            boolean handled = (q.mFlags & QueuedInputEvent.FLAG_FINISHED_HANDLED) != 0;
            boolean modified = (q.mFlags & QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY) != 0;
            if (modified) {
            if (modified && mInputCompatHandler != null) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventBeforeFinish");
                InputEvent processedEvent;
                try {
                    processedEvent =
                            mInputCompatProcessor.processInputEventBeforeFinish(q.mEvent);
                            mInputCompatHandler.processInputEventBeforeFinish(q.mEvent);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
@@ -10889,17 +10873,18 @@ public final class ViewRootImpl implements ViewParent,
     */
    @VisibleForTesting
    public void processRawInputEvent(InputEvent event) {
        List<InputEvent> processedEvents = null;
        if (mInputCompatHandler != null) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
        List<InputEvent> processedEvents;
            try {
            processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event);
                processedEvents = mInputCompatHandler.processInputEvent(event);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
        if (processedEvents != null) {
            if (processedEvents.isEmpty()) {
                // InputEvent consumed by mInputCompatProcessor
                // InputEvent consumed by mInputCompatHandler
                mInputEventReceiver.finishInputEvent(event, true);
            } else {
                for (int i = 0; i < processedEvents.size(); i++) {
+143 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view.input;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;

import com.android.internal.R;

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

/**
 * Controller of {@link InputEventCompatProcessor}s. One handler instance holds one feature,
 * and handlers can be chained.
 *
 * @hide
 */
public class InputEventCompatHandler {
    private static final String TAG = InputEventCompatHandler.class.getSimpleName();

    private final InputEventCompatProcessor mProcessor;
    private final InputEventCompatHandler mNext;

    public InputEventCompatHandler(InputEventCompatProcessor processor,
            @Nullable InputEventCompatHandler next) {
        mProcessor = processor;
        mNext = next;
    }

    /**
     * Process the InputEvent for compatibility before it is sent to the app, allowing for the
     * generation of more than one event if necessary.
     *
     * @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.
     */
    public List<InputEvent> processInputEvent(InputEvent inputEvent) {
        final List<InputEvent> events = mProcessor.processInputEventForCompatibility(inputEvent);
        if (mNext == null) {
            // This is the end of the chain. Returns the result.
            return events;
        } else if (events == null) {
            // The processor doesn't modified event.
            return mNext.processInputEvent(inputEvent);
        } else if (events.isEmpty()) {
            // The processor consumed the event.
            return events;
        } else if (events.size() == 1) {
            // The processor rewrote the event to another event.
            final List<InputEvent> res = mNext.processInputEvent(events.get(0));
            return res == null ? events : res;
        } else {
            // The processor synthesizes multiple events for a given event.
            final List<InputEvent> tmpEvents = new ArrayList<>(events.size());
            for (InputEvent ev : events) {
                final List<InputEvent> res = mNext.processInputEvent(ev);
                if (res != null) {
                    tmpEvents.addAll(res);
                } else {
                    tmpEvents.add(ev);
                }
            }
            return tmpEvents;
        }
    }

    /**
     * Process the InputEvent for compatibility before it is finished by calling
     * InputEventReceiver#finishInputEvent().
     *
     * @param inputEvent The InputEvent to process.
     * @return The InputEvent to finish, or null if it should not be finished.
     */
    @Nullable
    public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
        if (mNext != null) {
            inputEvent = mNext.processInputEventBeforeFinish(inputEvent);
            if (inputEvent == null) {
                return null;
            }
        }
        return mProcessor.processInputEventBeforeFinish(inputEvent);
    }

    /**
     * Create a list of {@link InputEventCompatProcessor} to be used based on a given context.
     * Returns the head processor of the chain, or null if no compatibility feature is needed.
     */
    public static InputEventCompatHandler buildChain(Context context, Handler handler) {
        // Build features from the tail.
        InputEventCompatHandler chainHead = null;

        final String processorOverrideName = context.getResources().getString(
                R.string.config_inputEventCompatProcessorOverrideClassName);
        if (!processorOverrideName.isEmpty()) {
            try {
                final Class<? extends InputEventCompatProcessor> klass =
                        (Class<? extends InputEventCompatProcessor>) Class.forName(
                                processorOverrideName);
                final InputEventCompatProcessor processor = klass
                        .getConstructor(Context.class, Handler.class)
                        .newInstance(context, handler);
                chainHead = new InputEventCompatHandler(processor, chainHead);
            } catch (Exception e) {
                Log.e(TAG, "Unable to create the InputEventCompatProcessor. ", e);
                chainHead = null;
            }
        }

        if (LetterboxScrollProcessor.isCompatibilityNeeded()) {
            chainHead = new InputEventCompatHandler(
                    new LetterboxScrollProcessor(context, handler), chainHead);
        }

        if (StylusButtonCompatibility.isCompatibilityNeeded(context)) {
            chainHead = new InputEventCompatHandler(
                    new StylusButtonCompatibility(context, handler), chainHead);
        }

        return chainHead;
    }
}
+5 −19
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.os.Handler;
import android.view.GestureDetector;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;
import android.view.MotionEvent;

import com.android.window.flags.Flags;
@@ -40,7 +41,7 @@ import java.util.Set;
 *
 * @hide
 */
public class LetterboxScrollProcessor {
public class LetterboxScrollProcessor extends InputEventCompatProcessor {

    private enum LetterboxScrollState {
        AWAITING_GESTURE_START,
@@ -63,6 +64,7 @@ public class LetterboxScrollProcessor {
    private final Set<Integer> mGeneratedEventIds = new HashSet<>();

    public LetterboxScrollProcessor(@NonNull Context context, @Nullable Handler handler) {
        super(context, handler);
        mContext = context;
        mScrollDetector = new GestureDetector(context, new ScrollListener(), handler);
    }
@@ -71,18 +73,8 @@ public class LetterboxScrollProcessor {
        return Flags.scrollingFromLetterbox();
    }

    /**
     * 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 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
    @Override
    public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
        if (!(inputEvent instanceof MotionEvent motionEvent)
                || motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE
@@ -150,14 +142,8 @@ public class LetterboxScrollProcessor {
        return makeNoAdjustments ? null : mProcessedEvents;
    }

    /**
     * Processes the InputEvent for compatibility before it is finished by calling
     * InputEventReceiver#finishInputEvent().
     *
     * @param inputEvent The InputEvent to process.
     * @return The motionEvent to finish, or null if it should not be finished.
     */
    @Nullable
    @Override
    public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
        if (mGeneratedEventIds.remove(inputEvent.getId())) {
            inputEvent.recycleIfNeededAfterDispatch();
+12 −7
Original line number Diff line number Diff line
@@ -20,15 +20,19 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;
import android.view.MotionEvent;

import java.util.List;

/**
 * This rewrites stylus button events for an application targeting older SDK.
 *
 * @hide
 */
public class StylusButtonCompatibility {
public class StylusButtonCompatibility extends InputEventCompatProcessor {
    private static final int STYLUS_BUTTONS_MASK =
            MotionEvent.BUTTON_STYLUS_PRIMARY | MotionEvent.BUTTON_STYLUS_SECONDARY;

@@ -39,12 +43,13 @@ public class StylusButtonCompatibility {
        return context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M;
    }

    /**
     * Returns a rewritten event if compatibility is applied.
     * Returns a null if the event is not modified.
     */
    public StylusButtonCompatibility(Context context, Handler handler) {
        super(context, handler);
    }

    @Nullable
    public InputEvent processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
    @Override
    public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
        if (!(inputEvent instanceof MotionEvent motion)) {
            return null;
        }
@@ -57,6 +62,6 @@ public class StylusButtonCompatibility {
            return null;
        }
        motion.setButtonState(buttonState | compatButtonState);
        return motion;
        return List.of(motion);
    }
}
Loading