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

Commit b11c55c0 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "InputEventCompatProcessor chaining" into main

parents ddec3dbd 06128c24
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
@@ -37,8 +37,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;
@@ -264,6 +264,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;
@@ -987,7 +988,7 @@ public final class ViewRootImpl implements ViewParent,
    private boolean mNeedsRendererSetup;
    private final InputEventCompatProcessor mInputCompatProcessor;
    private final InputEventCompatHandler mInputCompatHandler;
    /**
     * Consistency verifier for debugging purposes.
@@ -1291,24 +1292,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;
@@ -10604,12 +10588,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);
                }
@@ -10932,17 +10916,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