Loading core/java/android/view/InputEventCompatProcessor.java +11 −71 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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; } } core/java/android/view/ViewRootImpl.java +15 −30 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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++) { Loading core/java/android/view/input/InputEventCompatHandler.java 0 → 100644 +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; } } core/java/android/view/input/LetterboxScrollProcessor.java +5 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -40,7 +41,7 @@ import java.util.Set; * * @hide */ public class LetterboxScrollProcessor { public class LetterboxScrollProcessor extends InputEventCompatProcessor { private enum LetterboxScrollState { AWAITING_GESTURE_START, Loading @@ -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); } Loading @@ -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 Loading Loading @@ -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(); Loading core/java/android/view/input/StylusButtonCompatibility.java +12 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } Loading @@ -57,6 +62,6 @@ public class StylusButtonCompatibility { return null; } motion.setButtonState(buttonState | compatButtonState); return motion; return List.of(motion); } } Loading
core/java/android/view/InputEventCompatProcessor.java +11 −71 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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; } }
core/java/android/view/ViewRootImpl.java +15 −30 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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++) { Loading
core/java/android/view/input/InputEventCompatHandler.java 0 → 100644 +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; } }
core/java/android/view/input/LetterboxScrollProcessor.java +5 −19 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -40,7 +41,7 @@ import java.util.Set; * * @hide */ public class LetterboxScrollProcessor { public class LetterboxScrollProcessor extends InputEventCompatProcessor { private enum LetterboxScrollState { AWAITING_GESTURE_START, Loading @@ -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); } Loading @@ -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 Loading Loading @@ -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(); Loading
core/java/android/view/input/StylusButtonCompatibility.java +12 −7 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } Loading @@ -57,6 +62,6 @@ public class StylusButtonCompatibility { return null; } motion.setButtonState(buttonState | compatButtonState); return motion; return List.of(motion); } }