Merge "Creating GestureExclusionManager for a single place to listen for gesture exclusion region changes" into main

This commit is contained in:
Sunny Goyal
2024-02-10 08:17:50 +00:00
committed by Android (Google) Code Review
4 changed files with 297 additions and 116 deletions
@@ -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());
}
/**
@@ -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<ExclusionListener>()
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()
}
}
@@ -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<Any> { null }.get()
verifyZeroInteractions(exclusionManager)
}
}
@@ -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<Any> { null }.get()
Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
}
}