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

Commit d6594160 authored by Roy Chou's avatar Roy Chou
Browse files

chore(a11yInputFilter): dump received and sent events when exceptions in AccessibilityInputFilter

Cache the events when the AccessibilityInputFilter onInputEvent and
sendInputEvent methods are called, then dump the events when sendInputEvent gets an
expection (implies not succeed), so that we can have more data to investigate
b/414179573.

The dump only works when the original DEBUG flag true (implies
Log.debuggable) or the build is debuggable, so by default it's an no-op.

Bug: 414179573
Flag: EXEMPT only add logging for bug investigation
Test: manually check logcat & local bug report
Change-Id: I7ecdc94e88a68e474e02b6ef5249254290ec11d3
parent 37a0f79a
Loading
Loading
Loading
Loading
+10 −1
Original line number Original line Diff line number Diff line
@@ -173,7 +173,7 @@ public abstract class InputFilter extends IInputFilter.Stub {
        try {
        try {
            mHost.sendInputEvent(event, policyFlags);
            mHost.sendInputEvent(event, policyFlags);
        } catch (RemoteException re) {
        } catch (RemoteException re) {
            /* ignore */
            onSendInputEventException(re);
        }
        }
    }
    }


@@ -214,6 +214,15 @@ public abstract class InputFilter extends IInputFilter.Stub {
    public void onUninstalled() {
    public void onUninstalled() {
    }
    }


    /**
     * Called when a exception is raised when the filter sends the input event.
     *
     * @param exception The exception
     */
    public void onSendInputEventException(Exception exception) {
        /* ignore */
    }

    private final class H extends Handler {
    private final class H extends Handler {
        public H(Looper looper) {
        public H(Looper looper) {
            super(looper);
            super(looper);
+1 −0
Original line number Original line Diff line number Diff line
@@ -296,6 +296,7 @@ flag {
    description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness."
    description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness."
    bug: "295575684"
    bug: "295575684"
}
}

flag {
flag {
    name: "send_hover_events_based_on_event_stream"
    name: "send_hover_events_based_on_event_stream"
    namespace: "accessibility"
    namespace: "accessibility"
+147 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 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 com.android.server.accessibility;

import android.util.Slog;
import android.view.InputEvent;

/**
 * Provides debugging capabilities for {@link InputEvent}s processed by an
 * {@link AccessibilityInputFilter}. This class caches recent input events
 * (both received and sent) to assist in diagnosing issues, particularly
 * when an exception occurs during event injection.
 */
public class AccessibilityInputDebugger {

    private static final String TAG = "A11yInputDebugger";

    // Determines the capacity of the recent event caches.
    private static final int RECENT_EVENTS_TO_LOG = 10;

    // Copy of the most recent received input events.
    private final InputEvent[] mRecentReceivedEvents = new InputEvent[RECENT_EVENTS_TO_LOG];
    private int mMostRecentReceivedEventIndex = 0;

    // Copy of the most recent injected input events.
    private final InputEvent[] mRecentSentEvents = new InputEvent[RECENT_EVENTS_TO_LOG];
    private int mMostRecentSentEventIndex = 0;

    /**
     * Records an {@link InputEvent} that has been received by the accessibility input filter.
     */
    void onReceiveEvent(InputEvent event) {
        final int index = (mMostRecentReceivedEventIndex + 1) % RECENT_EVENTS_TO_LOG;
        mMostRecentReceivedEventIndex = index;
        if (mRecentReceivedEvents[index] != null) {
            mRecentReceivedEvents[index].recycle();
        }
        mRecentReceivedEvents[index] = event.copy();
    }

    /**
     * Records an {@link InputEvent} that is about to be sent (injected) by the
     * accessibility input filter.
     */
    void onSendEvent(InputEvent event) {
        final int index = (mMostRecentSentEventIndex + 1) % RECENT_EVENTS_TO_LOG;
        mMostRecentSentEventIndex = index;
        if (mRecentSentEvents[index] != null) {
            mRecentSentEvents[index].recycle();
        }
        mRecentSentEvents[index] = event.copy();
    }

    /**
     * Called when an exception occurs while sending an {@link InputEvent}. This method logs the
     * exception and dumps the cached received and sent events, to aid in debugging the cause of
     * the exception.
     */
    void onSendEventException(Exception exception) {
        Slog.w(TAG, "on accessibility send input event exception: " + exception);
        dumpCachedEvents();
    }

    /**
     * Clears all cached {@link InputEvent}s, releasing their resources.
     */
    void clearCachedEvents() {
        clearReceivedEventCaches();
        clearSentEventCaches();
    }

    private void clearReceivedEventCaches() {
        for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
            if (mRecentReceivedEvents[i] != null) {
                mRecentReceivedEvents[i].recycle();
                mRecentReceivedEvents[i] = null;
            }
        }
        mMostRecentReceivedEventIndex = 0;
    }

    private void clearSentEventCaches() {
        for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
            if (mRecentSentEvents[i] != null) {
                mRecentSentEvents[i].recycle();
                mRecentSentEvents[i] = null;
            }
        }
        mMostRecentSentEventIndex = 0;
    }

    private void dumpCachedEvents() {
        if (RECENT_EVENTS_TO_LOG == 0) {
            return;
        }

        StringBuilder message = new StringBuilder();
        // Recent received events
        message.append("\n  -- recent received events --");
        for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
            final int index = (mMostRecentReceivedEventIndex + RECENT_EVENTS_TO_LOG - i)
                    % RECENT_EVENTS_TO_LOG;
            final InputEvent event = mRecentReceivedEvents[index];
            if (event == null) {
                break;
            }
            message.append("\n  ");
            appendEvent(message, i + 1, event);
        }
        // Recent sent events
        message.append("\n  -- recent sent events --");
        for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) {
            final int index = (mMostRecentSentEventIndex + RECENT_EVENTS_TO_LOG - i)
                    % RECENT_EVENTS_TO_LOG;
            final InputEvent event = mRecentSentEvents[index];
            if (event == null) {
                break;
            }
            message.append("\n  ");
            appendEvent(message, i + 1, event);
        }
        Slog.w(TAG, message.toString());

        // Clear the caches so the future dumps will not have already printed events
        clearCachedEvents();
    }

    private static void appendEvent(StringBuilder message, int index, InputEvent event) {
        message.append(index).append(": sent at ").append(event.getEventTimeNanos());
        message.append(", ");
        message.append(event);
    }
}
+32 −1
Original line number Original line Diff line number Diff line
@@ -28,6 +28,7 @@ import android.annotation.Nullable;
import android.content.Context;
import android.content.Context;
import android.graphics.Region;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.hardware.input.InputManager;
import android.os.Build;
import android.os.Looper;
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.SystemClock;
@@ -251,6 +252,9 @@ public class AccessibilityInputFilter extends InputFilter implements EventStream
     */
     */
    private MotionEvent mLastActiveDeviceMotionEvent = null;
    private MotionEvent mLastActiveDeviceMotionEvent = null;


    @Nullable
    private final AccessibilityInputDebugger mInputDebugger;

    private static MotionEvent cancelMotion(MotionEvent event) {
    private static MotionEvent cancelMotion(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
                || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
                || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
@@ -313,6 +317,10 @@ public class AccessibilityInputFilter extends InputFilter implements EventStream
        mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        mEventHandler = eventHandler;
        mEventHandler = eventHandler;
        mMagnificationGestureHandler = magnificationGestureHandler;
        mMagnificationGestureHandler = magnificationGestureHandler;
        // Enable debugger only for debug builds or when debug logging is active.
        mInputDebugger = (DEBUG || Build.isDebuggable())
                ? new AccessibilityInputDebugger()
                : null;
    }
    }


    @Override
    @Override
@@ -323,6 +331,9 @@ public class AccessibilityInputFilter extends InputFilter implements EventStream
        mInstalled = true;
        mInstalled = true;
        disableFeatures(/* featuresToBeEnabled= */ FLAG_FEATURE_NONE);
        disableFeatures(/* featuresToBeEnabled= */ FLAG_FEATURE_NONE);
        enableFeatures();
        enableFeatures();
        if (mInputDebugger != null) {
            mInputDebugger.clearCachedEvents();
        }
        mAms.onInputFilterInstalled(true);
        mAms.onInputFilterInstalled(true);
        super.onInstalled();
        super.onInstalled();
    }
    }
@@ -334,13 +345,15 @@ public class AccessibilityInputFilter extends InputFilter implements EventStream
        }
        }
        mInstalled = false;
        mInstalled = false;
        disableFeatures(/* featuresToBeEnabled= */ FLAG_FEATURE_NONE);
        disableFeatures(/* featuresToBeEnabled= */ FLAG_FEATURE_NONE);
        if (mInputDebugger != null) {
            mInputDebugger.clearCachedEvents();
        }
        mAms.onInputFilterInstalled(false);
        mAms.onInputFilterInstalled(false);
        super.onUninstalled();
        super.onUninstalled();
    }
    }


    void onDisplayAdded(@NonNull Display display) {
    void onDisplayAdded(@NonNull Display display) {
        enableFeaturesForDisplayIfInstalled(display);
        enableFeaturesForDisplayIfInstalled(display);

    }
    }


    void onDisplayRemoved(int displayId) {
    void onDisplayRemoved(int displayId) {
@@ -366,9 +379,27 @@ public class AccessibilityInputFilter extends InputFilter implements EventStream
            }
            }
        }
        }


        if (mInputDebugger != null) {
            mInputDebugger.onReceiveEvent(event);
        }
        onInputEventInternal(event, policyFlags);
        onInputEventInternal(event, policyFlags);
    }
    }


    @Override
    public void sendInputEvent(InputEvent event, int policyFlags) {
        if (mInputDebugger != null) {
            mInputDebugger.onSendEvent(event);
        }
        super.sendInputEvent(event, policyFlags);
    }

    @Override
    public void onSendInputEventException(Exception exception) {
        if (mInputDebugger != null) {
            mInputDebugger.onSendEventException(exception);
        }
    }

    private void onInputEventInternal(InputEvent event, int policyFlags) {
    private void onInputEventInternal(InputEvent event, int policyFlags) {
        if (mEventHandler.size() == 0) {
        if (mEventHandler.size() == 0) {
            if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
            if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
+8 −3
Original line number Original line Diff line number Diff line
@@ -54,7 +54,6 @@ import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.DisplayTopologyGraph;
import android.hardware.display.DisplayTopologyGraph;
import android.hardware.display.DisplayViewport;
import android.hardware.display.DisplayViewport;
import android.hardware.input.AidlInputGestureData;
import android.hardware.input.AidlInputGestureData;
import android.hardware.input.AppLaunchData;
import android.hardware.input.HostUsiVersion;
import android.hardware.input.HostUsiVersion;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.IInputDeviceBatteryState;
import android.hardware.input.IInputDeviceBatteryState;
@@ -3467,7 +3466,8 @@ public class InputManagerService extends IInputManager.Stub
        }
        }


        @Override
        @Override
        public void sendInputEvent(@NonNull InputEvent event, int policyFlags) {
        public void sendInputEvent(@NonNull InputEvent event, int policyFlags)
                throws RemoteException {
            if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
            if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
                    "sendInputEvent()")) {
                    "sendInputEvent()")) {
                throw new SecurityException(
                throw new SecurityException(
@@ -3477,9 +3477,14 @@ public class InputManagerService extends IInputManager.Stub


            synchronized (mInputFilterLock) {
            synchronized (mInputFilterLock) {
                if (!mDisconnected) {
                if (!mDisconnected) {
                    mNative.injectInputEvent(event, false /* injectIntoUid */, -1 /* uid */,
                    @InputEventInjectionResult int result = mNative.injectInputEvent(
                            event, false /* injectIntoUid */, -1 /* uid */,
                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */,
                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */,
                            policyFlags | WindowManagerPolicy.FLAG_FILTERED);
                            policyFlags | WindowManagerPolicy.FLAG_FILTERED);
                    if (result != InputEventInjectionResult.SUCCEEDED) {
                        throw new RemoteException(
                                "Injection did not succeed, result= " + result + ".");
                    }
                }
                }
            }
            }
        }
        }