diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index d617828b60..cb0aa8f69f 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -22,7 +22,6 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.android.launcher3.util.DisplayController.CHANGE_ALL; import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.NavigationMode.NO_BUTTON; import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS; import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED; @@ -54,15 +53,11 @@ import android.net.Uri; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; -import android.util.Log; -import android.view.ISystemGestureExclusionListener; -import android.view.IWindowManager; import android.view.MotionEvent; import android.view.ViewConfiguration; -import android.view.WindowManagerGlobal; -import androidx.annotation.BinderThread; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.config.FeatureFlags; @@ -73,6 +68,8 @@ import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.SettingsCache; import com.android.quickstep.TopTaskTracker.CachedTaskInfo; import com.android.quickstep.util.ActiveGestureLog; +import com.android.quickstep.util.GestureExclusionManager; +import com.android.quickstep.util.GestureExclusionManager.ExclusionListener; import com.android.quickstep.util.NavBarPosition; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; @@ -86,7 +83,7 @@ import java.util.ArrayList; /** * Manages the state of the system during a swipe up gesture. */ -public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { +public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener { private static final String TAG = "RecentsAnimationDeviceState"; @@ -99,25 +96,9 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { private final Context mContext; private final DisplayController mDisplayController; - private final IWindowManager mIWindowManager; + private final GestureExclusionManager mExclusionManager; + - @VisibleForTesting - final ISystemGestureExclusionListener mGestureExclusionListener = - new ISystemGestureExclusionListener.Stub() { - @BinderThread - @Override - public void onSystemGestureExclusionChanged(int displayId, - Region systemGestureExclusionRegion, Region unrestrictedOrNull) { - if (displayId != DEFAULT_DISPLAY) { - return; - } - // Assignments are atomic, it should be safe on binder thread. Also we don't - // think systemGestureExclusionRegion can be null but just in case, don't - // let mExclusionRegion be null. - mExclusionRegion = systemGestureExclusionRegion != null - ? systemGestureExclusionRegion : new Region(); - } - }; private final RotationTouchHelper mRotationTouchHelper; private final TaskStackChangeListener mPipListener; // Cache for better performance since it doesn't change at runtime. @@ -140,20 +121,20 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { private boolean mPipIsActive; private int mGestureBlockingTaskId = -1; - private @NonNull Region mExclusionRegion = new Region(); + private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION; private boolean mExclusionListenerRegistered; public RecentsAnimationDeviceState(Context context) { - this(context, false, WindowManagerGlobal.getWindowManagerService()); + this(context, false, GestureExclusionManager.INSTANCE); } public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) { - this(context, isInstanceForTouches, WindowManagerGlobal.getWindowManagerService()); + this(context, isInstanceForTouches, GestureExclusionManager.INSTANCE); } @VisibleForTesting - RecentsAnimationDeviceState(Context context, IWindowManager windowManager) { - this(context, false, windowManager); + RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) { + this(context, false, exclusionManager); } /** @@ -162,10 +143,10 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { */ RecentsAnimationDeviceState( Context context, boolean isInstanceForTouches, - IWindowManager windowManager) { + GestureExclusionManager exclusionManager) { mContext = context; mDisplayController = DisplayController.INSTANCE.get(context); - mIWindowManager = windowManager; + mExclusionManager = exclusionManager; mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false); mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context); if (isInstanceForTouches) { @@ -276,43 +257,33 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { } } - /** - * Registers {@link mGestureExclusionListener} for getting exclusion rect changes. Note that we - * make binder call on {@link UI_HELPER_EXECUTOR} to avoid jank. - */ - public void registerExclusionListener() { - UI_HELPER_EXECUTOR.execute(() -> { - if (mExclusionListenerRegistered) { - return; - } - try { - mIWindowManager.registerSystemGestureExclusionListener( - mGestureExclusionListener, DEFAULT_DISPLAY); - mExclusionListenerRegistered = true; - } catch (RemoteException e) { - Log.e(TAG, "Failed to register window manager callbacks", e); - } - }); + @Override + public void onGestureExclusionChanged(@Nullable Region exclusionRegion, + @Nullable Region unrestrictedOrNull) { + mExclusionRegion = exclusionRegion != null + ? exclusionRegion : GestureExclusionManager.EMPTY_REGION; } /** - * Unregisters {@link mGestureExclusionListener} if previously registered. We make binder call - * on same {@link UI_HELPER_EXECUTOR} as in {@link #registerExclusionListener()} so that - * read/write {@link mExclusionListenerRegistered} field is thread safe. + * Registers itself for getting exclusion rect changes. + */ + public void registerExclusionListener() { + if (mExclusionListenerRegistered) { + return; + } + mExclusionManager.addListener(this); + mExclusionListenerRegistered = true; + } + + /** + * Unregisters itself as gesture exclusion listener if previously registered. */ public void unregisterExclusionListener() { - UI_HELPER_EXECUTOR.execute(() -> { - if (!mExclusionListenerRegistered) { - return; - } - try { - mIWindowManager.unregisterSystemGestureExclusionListener( - mGestureExclusionListener, DEFAULT_DISPLAY); - mExclusionListenerRegistered = false; - } catch (RemoteException e) { - Log.e(TAG, "Failed to unregister window manager callbacks", e); - } - }); + if (!mExclusionListenerRegistered) { + return; + } + mExclusionManager.removeListener(this); + mExclusionListenerRegistered = false; } public void onOneHandedModeChanged(int newGesturalHeight) { @@ -515,10 +486,8 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { * This is only used for quickswitch, and not swipe up. */ public boolean isInExclusionRegion(MotionEvent event) { - // mExclusionRegion can change on binder thread, use a local instance here. - Region exclusionRegion = mExclusionRegion; return mMode == NO_BUTTON - && exclusionRegion.contains((int) event.getX(), (int) event.getY()); + && mExclusionRegion.contains((int) event.getX(), (int) event.getY()); } /** diff --git a/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt new file mode 100644 index 0000000000..24b0e3aa6e --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt @@ -0,0 +1,111 @@ +/* + * 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.quickstep.util + +import android.graphics.Region +import android.os.RemoteException +import android.util.Log +import android.view.Display.DEFAULT_DISPLAY +import android.view.ISystemGestureExclusionListener +import android.view.IWindowManager +import android.view.WindowManagerGlobal +import androidx.annotation.BinderThread +import androidx.annotation.VisibleForTesting +import com.android.launcher3.util.Executors + +/** Wrapper over system gesture exclusion listener to optimize for multiple RPCs */ +class GestureExclusionManager(private val windowManager: IWindowManager) { + + private val listeners = mutableListOf() + + private var lastExclusionRegion: Region? = null + private var lastUnrestrictedOrNull: Region? = null + + @VisibleForTesting + val exclusionListener = + object : ISystemGestureExclusionListener.Stub() { + @BinderThread + override fun onSystemGestureExclusionChanged( + displayId: Int, + exclusionRegion: Region?, + unrestrictedOrNull: Region? + ) { + if (displayId != DEFAULT_DISPLAY) { + return + } + Executors.MAIN_EXECUTOR.execute { + lastExclusionRegion = exclusionRegion + lastUnrestrictedOrNull = unrestrictedOrNull + listeners.forEach { + it.onGestureExclusionChanged(exclusionRegion, unrestrictedOrNull) + } + } + } + } + + /** Adds a listener for receiving gesture exclusion regions */ + fun addListener(listener: ExclusionListener) { + val wasEmpty = listeners.isEmpty() + listeners.add(listener) + if (wasEmpty) { + Executors.UI_HELPER_EXECUTOR.execute { + try { + windowManager.registerSystemGestureExclusionListener( + exclusionListener, + DEFAULT_DISPLAY + ) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to register gesture exclusion listener", e) + } + } + } else { + // If we had already registered before, dispatch the last known value, + // otherwise registering the listener will initiate a dispatch + listener.onGestureExclusionChanged(lastExclusionRegion, lastUnrestrictedOrNull) + } + } + + /** Removes a previously added exclusion listener */ + fun removeListener(listener: ExclusionListener) { + if (listeners.remove(listener) && listeners.isEmpty()) { + Executors.UI_HELPER_EXECUTOR.execute { + try { + windowManager.unregisterSystemGestureExclusionListener( + exclusionListener, + DEFAULT_DISPLAY + ) + } catch (e: RemoteException) { + Log.e(TAG, "Failed to unregister gesture exclusion listener", e) + } + } + } + } + + interface ExclusionListener { + fun onGestureExclusionChanged(exclusionRegion: Region?, unrestrictedOrNull: Region?) + } + + companion object { + + private const val TAG = "GestureExclusionManager" + + @JvmField + val INSTANCE = GestureExclusionManager(WindowManagerGlobal.getWindowManagerService()!!) + + @JvmField val EMPTY_REGION = Region() + } +} diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt index 53bc2a23f2..2916952c41 100644 --- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt +++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt @@ -2,17 +2,15 @@ package com.android.quickstep import android.content.Context import android.testing.AndroidTestingRunner -import android.view.Display -import android.view.IWindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.filters.SmallTest import com.android.launcher3.util.DisplayController.CHANGE_DENSITY import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE import com.android.launcher3.util.DisplayController.CHANGE_ROTATION import com.android.launcher3.util.DisplayController.Info -import com.android.launcher3.util.Executors import com.android.launcher3.util.NavigationMode import com.android.launcher3.util.window.WindowManagerProxy +import com.android.quickstep.util.GestureExclusionManager import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +26,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class RecentsAnimationDeviceStateTest { - @Mock private lateinit var windowManager: IWindowManager + @Mock private lateinit var exclusionManager: GestureExclusionManager @Mock private lateinit var windowManagerProxy: WindowManagerProxy @Mock private lateinit var info: Info @@ -38,60 +36,45 @@ class RecentsAnimationDeviceStateTest { @Before fun setup() { MockitoAnnotations.initMocks(this) - underTest = RecentsAnimationDeviceState(context, windowManager) + underTest = RecentsAnimationDeviceState(context, exclusionManager) } @Test fun registerExclusionListener_success() { underTest.registerExclusionListener() - awaitTasksCompleted() - verify(windowManager) - .registerSystemGestureExclusionListener( - underTest.mGestureExclusionListener, - Display.DEFAULT_DISPLAY - ) + verify(exclusionManager).addListener(underTest) } @Test fun registerExclusionListener_again_fail() { underTest.registerExclusionListener() - awaitTasksCompleted() - reset(windowManager) + reset(exclusionManager) underTest.registerExclusionListener() - awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyZeroInteractions(exclusionManager) } @Test fun unregisterExclusionListener_success() { underTest.registerExclusionListener() - awaitTasksCompleted() - reset(windowManager) + reset(exclusionManager) underTest.unregisterExclusionListener() - awaitTasksCompleted() - verify(windowManager) - .unregisterSystemGestureExclusionListener( - underTest.mGestureExclusionListener, - Display.DEFAULT_DISPLAY - ) + verify(exclusionManager).removeListener(underTest) } @Test fun unregisterExclusionListener_again_fail() { underTest.registerExclusionListener() underTest.unregisterExclusionListener() - awaitTasksCompleted() - reset(windowManager) + reset(exclusionManager) underTest.unregisterExclusionListener() - awaitTasksCompleted() - verifyZeroInteractions(windowManager) + verifyZeroInteractions(exclusionManager) } @Test @@ -100,45 +83,28 @@ class RecentsAnimationDeviceStateTest { underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE) - awaitTasksCompleted() - verify(windowManager) - .registerSystemGestureExclusionListener( - underTest.mGestureExclusionListener, - Display.DEFAULT_DISPLAY - ) + verify(exclusionManager).addListener(underTest) } @Test fun onDisplayInfoChanged_twoButton_unregisterExclusionListener() { underTest.registerExclusionListener() - awaitTasksCompleted() whenever(info.getNavigationMode()).thenReturn(NavigationMode.TWO_BUTTONS) - reset(windowManager) + reset(exclusionManager) underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE) - awaitTasksCompleted() - verify(windowManager) - .unregisterSystemGestureExclusionListener( - underTest.mGestureExclusionListener, - Display.DEFAULT_DISPLAY - ) + verify(exclusionManager).removeListener(underTest) } @Test fun onDisplayInfoChanged_changeDensity_noOp() { underTest.registerExclusionListener() - awaitTasksCompleted() whenever(info.getNavigationMode()).thenReturn(NavigationMode.NO_BUTTON) - reset(windowManager) + reset(exclusionManager) underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY) - awaitTasksCompleted() - verifyZeroInteractions(windowManager) - } - - private fun awaitTasksCompleted() { - Executors.UI_HELPER_EXECUTOR.submit { null }.get() + verifyZeroInteractions(exclusionManager) } } diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt new file mode 100644 index 0000000000..c190cfebab --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt @@ -0,0 +1,135 @@ +/* + * 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.quickstep.util + +import android.graphics.Rect +import android.graphics.Region +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.IWindowManager +import androidx.test.filters.SmallTest +import com.android.launcher3.util.Executors +import com.android.quickstep.util.GestureExclusionManager.ExclusionListener +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions + +/** Unit test for [GestureExclusionManager]. */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class GestureExclusionManagerTest { + + @Mock private lateinit var windowManager: IWindowManager + + @Mock private lateinit var listener1: ExclusionListener + @Mock private lateinit var listener2: ExclusionListener + + private val r1 = Region().apply { union(Rect(0, 0, 100, 200)) } + private val r2 = Region().apply { union(Rect(200, 200, 500, 800)) } + + private lateinit var underTest: GestureExclusionManager + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = GestureExclusionManager(windowManager) + } + + @Test + fun addListener_registers() { + underTest.addListener(listener1) + + awaitTasksCompleted() + verify(windowManager) + .registerSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY) + } + + @Test + fun addListener_again_skips_register() { + underTest.addListener(listener1) + awaitTasksCompleted() + reset(windowManager) + + underTest.addListener(listener2) + + awaitTasksCompleted() + verifyZeroInteractions(windowManager) + } + + @Test + fun removeListener_unregisters() { + underTest.addListener(listener1) + awaitTasksCompleted() + reset(windowManager) + + underTest.removeListener(listener1) + + awaitTasksCompleted() + verify(windowManager) + .unregisterSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY) + } + + @Test + fun removeListener_again_skips_unregister() { + underTest.addListener(listener1) + underTest.addListener(listener2) + awaitTasksCompleted() + reset(windowManager) + + underTest.removeListener(listener1) + + awaitTasksCompleted() + verifyZeroInteractions(windowManager) + } + + @Test + fun onSystemGestureExclusionChanged_dispatches_to_listeners() { + underTest.addListener(listener1) + underTest.addListener(listener2) + awaitTasksCompleted() + + underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2) + awaitTasksCompleted() + verify(listener1).onGestureExclusionChanged(r1, r2) + verify(listener2).onGestureExclusionChanged(r1, r2) + } + + @Test + fun addLister_dispatches_second_time() { + underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2) + awaitTasksCompleted() + underTest.addListener(listener1) + awaitTasksCompleted() + verifyZeroInteractions(listener1) + + underTest.addListener(listener2) + awaitTasksCompleted() + + verifyZeroInteractions(listener1) + verify(listener2).onGestureExclusionChanged(r1, r2) + } + + private fun awaitTasksCompleted() { + Executors.UI_HELPER_EXECUTOR.submit { null }.get() + Executors.MAIN_EXECUTOR.submit { null }.get() + } +}