Loading packages/SystemUI/aconfig/systemui.aconfig +11 −1 Original line number Diff line number Diff line Loading @@ -774,3 +774,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "dream_input_session_pilfer_once" namespace: "systemui" description: "Pilfer at most once per input session" bug: "324600132" metadata { purpose: PURPOSE_BUGFIX } } packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java +23 −13 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.dreams.touch; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.PILFER_ON_GESTURE_CONSUME; import android.os.Looper; Loading @@ -24,7 +23,8 @@ import android.view.Choreographer; import android.view.GestureDetector; import android.view.MotionEvent; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; Loading @@ -42,26 +42,34 @@ public class InputSession { private final InputChannelCompat.InputEventReceiver mInputEventReceiver; private final GestureDetector mGestureDetector; // Pilfering is a destructive operation. Once pilfering starts, the all events will be captured // by the associated monitor. We track whether we're pilfering since initiating pilfering // requires reaching out to the InputManagerService, which can be a heavy operation. This is // especially costly if this is happening on a continuous stream of motion events. private boolean mPilfering; /** * Default session constructor. * @param sessionName The session name that will be applied to the underlying * {@link InputMonitorCompat}. * @param inputMonitor Input monitor to track input events. * @param gestureDetector Gesture detector for detecting gestures. * @param inputEventListener A listener to receive input events. * @param gestureListener A listener to receive gesture events. * @param choreographer Choreographer to use with the input receiver. * @param looper Looper to use with the input receiver * @param pilferOnGestureConsume Whether touch events should be pilfered after a gesture has * been consumed. */ @Inject public InputSession(@Named(INPUT_SESSION_NAME) String sessionName, public InputSession( InputMonitorCompat inputMonitor, GestureDetector gestureDetector, InputChannelCompat.InputEventListener inputEventListener, GestureDetector.OnGestureListener gestureListener, DisplayTracker displayTracker, Choreographer choreographer, @Main Looper looper, @Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) { mInputMonitor = new InputMonitorCompat(sessionName, displayTracker.getDefaultDisplayId()); mGestureDetector = new GestureDetector(gestureListener); mInputMonitor = inputMonitor; mGestureDetector = gestureDetector; mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance(), mInputEventReceiver = mInputMonitor.getInputReceiver(looper, choreographer, ev -> { // Process event. Since sometimes input may be a prerequisite for some // gesture logic, process input first. Loading @@ -69,7 +77,9 @@ public class InputSession { if (ev instanceof MotionEvent && mGestureDetector.onTouchEvent((MotionEvent) ev) && pilferOnGestureConsume) { && pilferOnGestureConsume && !(mPilfering && Flags.dreamInputSessionPilferOnce())) { mPilfering = true; mInputMonitor.pilferPointers(); } }); Loading packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java +5 −3 Original line number Diff line number Diff line Loading @@ -24,16 +24,18 @@ import android.view.GestureDetector; import com.android.systemui.dreams.touch.InputSession; import com.android.systemui.shared.system.InputChannelCompat; import javax.inject.Named; import dagger.BindsInstance; import dagger.Subcomponent; import javax.inject.Named; /** * {@link InputSessionComponent} generates {@link InputSession} with specific instances bound for * the session name and whether touches should be pilfered when consumed. */ @Subcomponent @Subcomponent( modules = { InputSessionModule.class } ) public interface InputSessionComponent { /** * Generates {@link InputSessionComponent}. Loading packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionModule.java 0 → 100644 +50 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.systemui.dreams.touch.dagger; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; import android.view.GestureDetector; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.shared.system.InputMonitorCompat; import dagger.Module; import dagger.Provides; import javax.inject.Named; /** * Module for providing dependencies to {@link com.android.systemui.dreams.touch.InputSession}. */ @Module public interface InputSessionModule { /** */ @Provides static InputMonitorCompat providesInputMonitorCompat(@Named(INPUT_SESSION_NAME) String name, DisplayTracker displayTracker) { return new InputMonitorCompat(name, displayTracker.getDefaultDisplayId()); } /** */ @Provides static GestureDetector providesGestureDetector( android.view.GestureDetector.OnGestureListener gestureListener) { return new GestureDetector(gestureListener); } } packages/SystemUI/tests/src/com/android/systemui/dreams/touch/InputSessionTest.java 0 → 100644 +155 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.systemui.dreams.touch; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Choreographer; import android.view.GestureDetector; import android.view.InputEvent; import android.view.MotionEvent; import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** * A test suite for exercising {@link InputSession}. */ @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper() public class InputSessionTest extends SysuiTestCase { @Mock InputMonitorCompat mInputMonitor; @Mock GestureDetector mGestureDetector; @Mock InputChannelCompat.InputEventListener mInputEventListener; TestableLooper mLooper; @Mock Choreographer mChoreographer; @Mock InputChannelCompat.InputEventReceiver mInputEventReceiver; InputSession mSession; InputChannelCompat.InputEventListener mEventListener; @Before public void setup() { MockitoAnnotations.initMocks(this); mLooper = TestableLooper.get(this); } private void createSession(boolean pilfer) { when(mInputMonitor.getInputReceiver(any(), any(), any())) .thenReturn(mInputEventReceiver); mSession = new InputSession(mInputMonitor, mGestureDetector, mInputEventListener, mChoreographer, mLooper.getLooper(), pilfer); final ArgumentCaptor<InputChannelCompat.InputEventListener> listenerCaptor = ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); verify(mInputMonitor).getInputReceiver(any(), any(), listenerCaptor.capture()); mEventListener = listenerCaptor.getValue(); } /** * Ensures consumed motion events are pilfered when option is set. */ @Test public void testPilferOnMotionEventGestureConsume() { createSession(true); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor).pilferPointers(); } /** * Ensures consumed motion events are not pilfered when option is not set. */ @Test public void testNoPilferOnMotionEventGestureConsume() { createSession(false); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor, never()).pilferPointers(); } /** * Ensures input events are never pilfered. */ @Test public void testNoPilferOnInputEvent() { createSession(true); final InputEvent event = Mockito.mock(InputEvent.class); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor, never()).pilferPointers(); } @Test @EnableFlags(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE) public void testPilferOnce() { createSession(true); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); mEventListener.onInputEvent(event); verify(mInputEventListener, times(2)).onInputEvent(eq(event)); verify(mInputMonitor, times(1)).pilferPointers(); } /** * Ensures components are properly disposed. */ @Test public void testDispose() { createSession(true); mSession.dispose(); verify(mInputMonitor).dispose(); verify(mInputEventReceiver).dispose(); } } Loading
packages/SystemUI/aconfig/systemui.aconfig +11 −1 Original line number Diff line number Diff line Loading @@ -774,3 +774,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "dream_input_session_pilfer_once" namespace: "systemui" description: "Pilfer at most once per input session" bug: "324600132" metadata { purpose: PURPOSE_BUGFIX } }
packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java +23 −13 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.dreams.touch; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.PILFER_ON_GESTURE_CONSUME; import android.os.Looper; Loading @@ -24,7 +23,8 @@ import android.view.Choreographer; import android.view.GestureDetector; import android.view.MotionEvent; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; Loading @@ -42,26 +42,34 @@ public class InputSession { private final InputChannelCompat.InputEventReceiver mInputEventReceiver; private final GestureDetector mGestureDetector; // Pilfering is a destructive operation. Once pilfering starts, the all events will be captured // by the associated monitor. We track whether we're pilfering since initiating pilfering // requires reaching out to the InputManagerService, which can be a heavy operation. This is // especially costly if this is happening on a continuous stream of motion events. private boolean mPilfering; /** * Default session constructor. * @param sessionName The session name that will be applied to the underlying * {@link InputMonitorCompat}. * @param inputMonitor Input monitor to track input events. * @param gestureDetector Gesture detector for detecting gestures. * @param inputEventListener A listener to receive input events. * @param gestureListener A listener to receive gesture events. * @param choreographer Choreographer to use with the input receiver. * @param looper Looper to use with the input receiver * @param pilferOnGestureConsume Whether touch events should be pilfered after a gesture has * been consumed. */ @Inject public InputSession(@Named(INPUT_SESSION_NAME) String sessionName, public InputSession( InputMonitorCompat inputMonitor, GestureDetector gestureDetector, InputChannelCompat.InputEventListener inputEventListener, GestureDetector.OnGestureListener gestureListener, DisplayTracker displayTracker, Choreographer choreographer, @Main Looper looper, @Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) { mInputMonitor = new InputMonitorCompat(sessionName, displayTracker.getDefaultDisplayId()); mGestureDetector = new GestureDetector(gestureListener); mInputMonitor = inputMonitor; mGestureDetector = gestureDetector; mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance(), mInputEventReceiver = mInputMonitor.getInputReceiver(looper, choreographer, ev -> { // Process event. Since sometimes input may be a prerequisite for some // gesture logic, process input first. Loading @@ -69,7 +77,9 @@ public class InputSession { if (ev instanceof MotionEvent && mGestureDetector.onTouchEvent((MotionEvent) ev) && pilferOnGestureConsume) { && pilferOnGestureConsume && !(mPilfering && Flags.dreamInputSessionPilferOnce())) { mPilfering = true; mInputMonitor.pilferPointers(); } }); Loading
packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java +5 −3 Original line number Diff line number Diff line Loading @@ -24,16 +24,18 @@ import android.view.GestureDetector; import com.android.systemui.dreams.touch.InputSession; import com.android.systemui.shared.system.InputChannelCompat; import javax.inject.Named; import dagger.BindsInstance; import dagger.Subcomponent; import javax.inject.Named; /** * {@link InputSessionComponent} generates {@link InputSession} with specific instances bound for * the session name and whether touches should be pilfered when consumed. */ @Subcomponent @Subcomponent( modules = { InputSessionModule.class } ) public interface InputSessionComponent { /** * Generates {@link InputSessionComponent}. Loading
packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionModule.java 0 → 100644 +50 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.systemui.dreams.touch.dagger; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; import android.view.GestureDetector; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.shared.system.InputMonitorCompat; import dagger.Module; import dagger.Provides; import javax.inject.Named; /** * Module for providing dependencies to {@link com.android.systemui.dreams.touch.InputSession}. */ @Module public interface InputSessionModule { /** */ @Provides static InputMonitorCompat providesInputMonitorCompat(@Named(INPUT_SESSION_NAME) String name, DisplayTracker displayTracker) { return new InputMonitorCompat(name, displayTracker.getDefaultDisplayId()); } /** */ @Provides static GestureDetector providesGestureDetector( android.view.GestureDetector.OnGestureListener gestureListener) { return new GestureDetector(gestureListener); } }
packages/SystemUI/tests/src/com/android/systemui/dreams/touch/InputSessionTest.java 0 → 100644 +155 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.systemui.dreams.touch; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Choreographer; import android.view.GestureDetector; import android.view.InputEvent; import android.view.MotionEvent; import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** * A test suite for exercising {@link InputSession}. */ @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper() public class InputSessionTest extends SysuiTestCase { @Mock InputMonitorCompat mInputMonitor; @Mock GestureDetector mGestureDetector; @Mock InputChannelCompat.InputEventListener mInputEventListener; TestableLooper mLooper; @Mock Choreographer mChoreographer; @Mock InputChannelCompat.InputEventReceiver mInputEventReceiver; InputSession mSession; InputChannelCompat.InputEventListener mEventListener; @Before public void setup() { MockitoAnnotations.initMocks(this); mLooper = TestableLooper.get(this); } private void createSession(boolean pilfer) { when(mInputMonitor.getInputReceiver(any(), any(), any())) .thenReturn(mInputEventReceiver); mSession = new InputSession(mInputMonitor, mGestureDetector, mInputEventListener, mChoreographer, mLooper.getLooper(), pilfer); final ArgumentCaptor<InputChannelCompat.InputEventListener> listenerCaptor = ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); verify(mInputMonitor).getInputReceiver(any(), any(), listenerCaptor.capture()); mEventListener = listenerCaptor.getValue(); } /** * Ensures consumed motion events are pilfered when option is set. */ @Test public void testPilferOnMotionEventGestureConsume() { createSession(true); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor).pilferPointers(); } /** * Ensures consumed motion events are not pilfered when option is not set. */ @Test public void testNoPilferOnMotionEventGestureConsume() { createSession(false); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor, never()).pilferPointers(); } /** * Ensures input events are never pilfered. */ @Test public void testNoPilferOnInputEvent() { createSession(true); final InputEvent event = Mockito.mock(InputEvent.class); mEventListener.onInputEvent(event); verify(mInputEventListener).onInputEvent(eq(event)); verify(mInputMonitor, never()).pilferPointers(); } @Test @EnableFlags(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE) public void testPilferOnce() { createSession(true); final MotionEvent event = Mockito.mock(MotionEvent.class); when(mGestureDetector.onTouchEvent(event)).thenReturn(true); mEventListener.onInputEvent(event); mEventListener.onInputEvent(event); verify(mInputEventListener, times(2)).onInputEvent(eq(event)); verify(mInputMonitor, times(1)).pilferPointers(); } /** * Ensures components are properly disposed. */ @Test public void testDispose() { createSession(true); mSession.dispose(); verify(mInputMonitor).dispose(); verify(mInputEventReceiver).dispose(); } }