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

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

Merge "Ensure previous batched input is consumed when enabling batching" into main

parents 044631df dba0e2aa
Loading
Loading
Loading
Loading
+61 −7
Original line number Diff line number Diff line
@@ -26,7 +26,48 @@ import android.os.Trace;
 * @hide
 */
public class BatchedInputEventReceiver extends InputEventReceiver {
    /**
     * Interface used to schedule requests to consume batched input avents around vsync boundaries.
     */
    public interface BatchedInputScheduler {
        /** Posts a task to consume pending batched events. */
        void postCallback(Runnable action);

        /** Cancels previously posted task to consume pending batched events. */
        void removeCallbacks(Runnable action);

        /** Gets the time of the frame to which pending events should be batched. */
        long getFrameTimeNanos();
    };

    /**
     * Implementation of the `BatchedInputScheduler` interface that's backed by a Choreographer.
     * To be used in production.
     */
    private static class ChoreographerBatchedInputScheduler implements BatchedInputScheduler {
        private Choreographer mChoreographer;

        ChoreographerBatchedInputScheduler(Choreographer choreographer) {
            mChoreographer = choreographer;
        }

        @Override
        public void postCallback(Runnable action) {
            mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, action, null);
        }

        @Override
        public void removeCallbacks(Runnable action) {
            mChoreographer.removeCallbacks(Choreographer.CALLBACK_INPUT, action, null);
        }

        @Override
        public long getFrameTimeNanos() {
            return mChoreographer.getFrameTimeNanos();
        }
    };

    private BatchedInputScheduler mScheduler;
    private boolean mBatchingEnabled;
    private boolean mBatchedInputScheduled;
    private final String mTag;
@@ -41,8 +82,13 @@ public class BatchedInputEventReceiver extends InputEventReceiver {
    @UnsupportedAppUsage
    public BatchedInputEventReceiver(
            InputChannel inputChannel, Looper looper, Choreographer choreographer) {
        this(inputChannel, looper, new ChoreographerBatchedInputScheduler(choreographer));
    }

    public BatchedInputEventReceiver(
            InputChannel inputChannel, Looper looper, BatchedInputScheduler scheduler) {
        super(inputChannel, looper);
        mChoreographer = choreographer;
        mScheduler = scheduler;
        mBatchingEnabled = true;
        mTag = inputChannel.getName();
        traceBoolVariable("mBatchingEnabled", mBatchingEnabled);
@@ -77,7 +123,16 @@ public class BatchedInputEventReceiver extends InputEventReceiver {

        mBatchingEnabled = batchingEnabled;
        traceBoolVariable("mBatchingEnabled", mBatchingEnabled);
        if (mHandler.hasCallbacks(mConsumeBatchedInputEvents)) {
            mHandler.removeCallbacks(mConsumeBatchedInputEvents);
            // Existence of `mConsumeBatchedInputEvents` implies that there are pending batched
            // input events from the last time batching was enabled that need to be consumed -
            // consume them so the receiver keeps getting `onBatchedInputEventPending()`
            // notifications.
            if (batchingEnabled) {
                scheduleBatchedInput();
            }
        }
        if (!batchingEnabled) {
            unscheduleBatchedInput();
            mHandler.post(mConsumeBatchedInputEvents);
@@ -103,7 +158,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver {
        if (!mBatchedInputScheduled) {
            mBatchedInputScheduled = true;
            traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled);
            mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null);
            mScheduler.postCallback(mBatchedInputRunnable);
        }
    }

@@ -111,8 +166,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver {
        if (mBatchedInputScheduled) {
            mBatchedInputScheduled = false;
            traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_INPUT, mBatchedInputRunnable, null);
            mScheduler.removeCallbacks(mBatchedInputRunnable);
        }
    }

@@ -127,7 +181,7 @@ public class BatchedInputEventReceiver extends InputEventReceiver {
        public void run() {
            try {
                Trace.traceBegin(Trace.TRACE_TAG_INPUT, mTag);
                doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
                doConsumeBatchedInput(mScheduler.getFrameTimeNanos());
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_INPUT);
            }
+173 −0
Original line number 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.test.input

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.view.BatchedInputEventReceiver
import android.view.InputChannel
import android.view.InputDevice
import android.view.InputDevice.SOURCE_MOUSE
import android.view.InputEvent
import android.view.MotionEvent
import com.android.cts.input.BlockingQueueEventVerifier
import com.android.cts.input.MotionEventBuilder
import com.android.cts.input.PointerBuilder
import com.android.cts.input.inputeventmatchers.withMotionAction
import java.util.concurrent.LinkedBlockingQueue
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
import org.junit.Test

private fun getTestMouseMotionEvent(action: Int, eventTime: Long): MotionEvent {
    return MotionEventBuilder(action, InputDevice.SOURCE_MOUSE)
        .downTime(eventTime)
        .eventTime(eventTime)
        .pointer(PointerBuilder(/* id= */ 0, MotionEvent.TOOL_TYPE_MOUSE).x(0f).y(0f))
        .build()
}

private class TestBatchedInputEventReceiver(
    channel: InputChannel,
    looper: Looper,
    scheduler: BatchedInputEventReceiver.BatchedInputScheduler,
) : BatchedInputEventReceiver(channel, looper, scheduler) {
    // If set, event receiver will disable, and then immediately enable batching after handling
    // next pending batched input event - i.e. after scheduling batched input, but before the
    // scheduled batched input is actually consumed.
    var resetBatchingAfterNextPendingEvent = false

    // Keeps track of received input events.
    private val inputEvents = LinkedBlockingQueue<InputEvent>()
    private val verifier = BlockingQueueEventVerifier(inputEvents)

    override fun onBatchedInputEventPending(source: Int) {
        super.onBatchedInputEventPending(source)

        if (resetBatchingAfterNextPendingEvent) {
            resetBatchingAfterNextPendingEvent = false
            setBatchingEnabled(false)
            setBatchingEnabled(true)
        }
    }

    override fun onInputEvent(event: InputEvent) {
        when (event) {
            is MotionEvent -> inputEvents.put(MotionEvent.obtain(event))
            else -> throw Exception("Received $event is not  a motion")
        }
        finishInputEvent(event, true /*handled*/)
    }

    fun assertReceivedMotion(matcher: Matcher<MotionEvent>) {
        verifier.assertReceivedMotion(matcher)
    }
}

/**
 * Test implementation for interface used to schedule tasks to consume batched input events used by
 * `BatchedInputEventReceiver`. The implementation posts the task asynchronously to the provided
 * looper.
 */
private class TestBatchedInputScheduler(
    looper: Looper,
    var frameTimeMs: Long,
    val frameTimeIncrementMs: Long,
) : BatchedInputEventReceiver.BatchedInputScheduler {
    val handler = Handler(looper)

    override fun postCallback(callback: Runnable) {
        handler.post(callback)
    }

    override fun removeCallbacks(callback: Runnable) {
        handler.removeCallbacks(callback)
    }

    override fun getFrameTimeNanos(): Long {
        val frameTime = frameTimeMs * 1000000L
        frameTimeMs += frameTimeIncrementMs
        return frameTime
    }
}

class BatchedInputEventReceiverTest {
    companion object {
        private const val TAG = "BatchedInputEventReceiverTest"
    }

    private val channels = InputChannel.openInputChannelPair("TestChannel")
    private val handlerThread = HandlerThread("Process input events")
    private lateinit var sender: SpyInputEventSender
    private lateinit var batchedReceiver: TestBatchedInputEventReceiver

    @Before
    fun setUp() {
        handlerThread.start()

        val looper = handlerThread.getLooper()
        sender = SpyInputEventSender(channels[0], looper)
        batchedReceiver =
            TestBatchedInputEventReceiver(
                channels[1],
                looper,
                TestBatchedInputScheduler(looper, 1000L, 50L),
            )
    }

    @After
    fun tearDown() {
        handlerThread.quitSafely()
    }

    // Consumption of batched input events should continue if batching is disabled, and then enabled
    // again before running the task to consume pending events in response to batching getting
    // disabled.
    @Test
    fun testKeepConsumingEventsAfterBatchingRestart() {
        val seq = 12
        // Send ACTION_DOWN, and verify it gets handled by the receiver without batching.
        sender.sendInputEvent(seq, getTestMouseMotionEvent(MotionEvent.ACTION_DOWN, 900L))
        batchedReceiver.assertReceivedMotion(withMotionAction(MotionEvent.ACTION_DOWN))
        sender.assertReceivedFinishedSignal(seq, handled = true)

        batchedReceiver.resetBatchingAfterNextPendingEvent = true

        // Send hover move events, which will get batched.
        // The reveiver resets batching after receiving batched input notification for this event.
        sender.sendInputEvent(seq + 1, getTestMouseMotionEvent(MotionEvent.ACTION_MOVE, 930L))
        sender.sendInputEvent(seq + 2, getTestMouseMotionEvent(MotionEvent.ACTION_MOVE, 1010L))

        // Verify that the receiver consumed batched hover move event, even though batching was
        // restarted upon receiving first pending batched input events notification.
        batchedReceiver.assertReceivedMotion(withMotionAction(MotionEvent.ACTION_MOVE))
        sender.assertReceivedFinishedSignal(seq + 1, handled = true)
        batchedReceiver.assertReceivedMotion(withMotionAction(MotionEvent.ACTION_MOVE))
        sender.assertReceivedFinishedSignal(seq + 2, handled = true)

        // Send another hover move event, and verify it gets consumed in case where batching is not
        // restarted.
        sender.sendInputEvent(seq + 3, getTestMouseMotionEvent(MotionEvent.ACTION_MOVE, 1060L))
        batchedReceiver.assertReceivedMotion(withMotionAction(MotionEvent.ACTION_MOVE))

        sender.assertReceivedFinishedSignal(seq + 3, handled = true)

        sender.dispose()
    }
}