From c0cc90d027d0cc8708b5472300126eec37247d14 Mon Sep 17 00:00:00 2001 From: Jason Chang Date: Tue, 2 Jun 2020 20:11:22 +0800 Subject: [PATCH] Support NavigationBar 3-button mode gesture for One-Handed mode Adding new gesture handler for supporting NavigationBar 3-button mode gesture. Bug: 157949041 Test: manual Test: atest SystemUITests Change-Id: Iaaa8592dd508504b03a625d3dce503ab7628b67e --- packages/SystemUI/res/values/dimens.xml | 4 + .../onehanded/OneHandedGestureHandler.java | 285 ++++++++++++++++++ .../onehanded/OneHandedManagerImpl.java | 29 ++ .../OneHandedGestureHandlerTest.java | 118 ++++++++ .../onehanded/OneHandedManagerImplTest.java | 3 + .../onehanded/OneHandedTouchHandlerTest.java | 7 + 6 files changed, 446 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/onehanded/OneHandedGestureHandler.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedGestureHandlerTest.java diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2f3407d562631..3096a01951902 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1371,4 +1371,8 @@ @*android:dimen/rounded_corner_radius @*android:dimen/rounded_corner_radius_top @*android:dimen/rounded_corner_radius_bottom + + + + 20dp diff --git a/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedGestureHandler.java b/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedGestureHandler.java new file mode 100644 index 0000000000000..f995bf9eb2a10 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedGestureHandler.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2020 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.onehanded; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Looper; +import android.util.Log; +import android.view.Display; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.ViewConfiguration; +import android.window.WindowContainerTransaction; + +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.NavigationModeController; +import com.android.systemui.wm.DisplayChangeController; +import com.android.systemui.wm.DisplayController; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * The class manage swipe up and down gesture for 3-Button mode navigation, + * others(e.g, 2-button, full gesture mode) are handled by Launcher quick steps. + */ +@Singleton +public class OneHandedGestureHandler implements OneHandedTransitionCallback, + NavigationModeController.ModeChangedListener, + DisplayChangeController.OnDisplayChangingListener { + private static final String TAG = "OneHandedGestureHandler"; + private static final boolean DEBUG_GESTURE = false; + + private static final int ANGLE_MAX = 150; + private static final int ANGLE_MIN = 30; + private final float mDragDistThreshold; + private final float mSquaredSlop; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private final PointF mStartDragPos = new PointF(); + private boolean mPassedSlop; + + private boolean mAllowGesture; + private boolean mIsEnabled; + private int mNavGestureHeight; + private boolean mIsThreeButtonModeEnable; + private int mRotation = Surface.ROTATION_0; + + @VisibleForTesting + InputMonitor mInputMonitor; + @VisibleForTesting + InputEventReceiver mInputEventReceiver; + private DisplayController mDisplayController; + @VisibleForTesting + @Nullable + OneHandedGestureEventCallback mGestureEventCallback; + private Rect mGestureRegion = new Rect(); + + /** + * Constructor of OneHandedGestureHandler, we only handle the gesture of + * {@link Display#DEFAULT_DISPLAY} + * + * @param context {@link Context} + * @param displayController {@link DisplayController} + * @param navigationModeController {@link NavigationModeController} + */ + @Inject + public OneHandedGestureHandler(Context context, DisplayController displayController, + NavigationModeController navigationModeController) { + mDisplayController = displayController; + displayController.addDisplayChangingController(this); + final int NavBarMode = navigationModeController.addListener(this); + mIsThreeButtonModeEnable = (NavBarMode == NAV_BAR_MODE_3BUTTON); + mNavGestureHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); + mDragDistThreshold = context.getResources().getDimensionPixelSize( + R.dimen.gestures_onehanded_drag_threshold); + final float slop = ViewConfiguration.get(context).getScaledTouchSlop(); + mSquaredSlop = slop * slop; + updateIsEnabled(); + } + + /** + * Notified by {@link OneHandedManager}, when user update settings of Enabled or Disabled + * + * @param isEnabled is one handed settings enabled or not + */ + public void onOneHandedEnabled(boolean isEnabled) { + if (DEBUG_GESTURE) { + Log.d(TAG, "onOneHandedEnabled, isEnabled = " + isEnabled); + } + mIsEnabled = isEnabled; + updateIsEnabled(); + } + + /** + * Register {@link OneHandedGestureEventCallback} to receive onStart(), onStop() callback + */ + public void setGestureEventListener(OneHandedGestureEventCallback callback) { + mGestureEventCallback = callback; + } + + private void onMotionEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mAllowGesture = isWithinTouchRegion(ev.getX(), ev.getY()) + && mRotation == Surface.ROTATION_0; + if (mAllowGesture) { + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + } + if (DEBUG_GESTURE) { + Log.d(TAG, "ACTION_DOWN, mDownPos=" + mDownPos + ", mAllowGesture=" + + mAllowGesture); + } + } else if (mAllowGesture) { + switch (action) { + case MotionEvent.ACTION_MOVE: + mLastPos.set(ev.getX(), ev.getY()); + if (!mPassedSlop) { + if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) + > mSquaredSlop) { + mStartDragPos.set(mLastPos.x, mLastPos.y); + if (isValidStartAngle( + mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y) + || isValidExitAngle( + mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) { + mPassedSlop = true; + mInputMonitor.pilferPointers(); + } + } + } else { + float distance = (float) Math.hypot(mLastPos.x - mDownPos.x, + mLastPos.y - mDownPos.y); + if (distance > mDragDistThreshold && mPassedSlop) { + mGestureEventCallback.onStop(); + } + } + break; + case MotionEvent.ACTION_UP: + if (mLastPos.y >= mDownPos.y && mPassedSlop) { + mGestureEventCallback.onStart(); + } + mPassedSlop = false; + mAllowGesture = false; + break; + case MotionEvent.ACTION_CANCEL: + mPassedSlop = false; + mAllowGesture = false; + break; + default: + break; + } + } + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + private boolean isWithinTouchRegion(float x, float y) { + if (DEBUG_GESTURE) { + Log.d(TAG, "isWithinTouchRegion(), mGestureRegion=" + mGestureRegion + ", downX=" + x + + ", downY=" + y); + } + return mGestureRegion.contains(Math.round(x), Math.round(y)); + } + + private void updateIsEnabled() { + disposeInputChannel(); + + if (mIsEnabled && mIsThreeButtonModeEnable) { + final Point displaySize = new Point(); + if (mDisplayController != null) { + final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY); + if (display != null) { + display.getRealSize(displaySize); + } + } + // Register input event receiver to monitor the touch region of NavBar gesture height + mGestureRegion.set(0, displaySize.y - mNavGestureHeight, displaySize.x, + displaySize.y); + mInputMonitor = InputManager.getInstance().monitorGestureInput( + "onehanded-gesture-offset", DEFAULT_DISPLAY); + mInputEventReceiver = new SysUiInputEventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()); + } + } + + private void onInputEvent(InputEvent ev) { + if (ev instanceof MotionEvent) { + onMotionEvent((MotionEvent) ev); + } + } + + @Override + public void onNavigationModeChanged(int mode) { + if (DEBUG_GESTURE) { + Log.d(TAG, "onNavigationModeChanged, mode =" + mode); + } + mIsThreeButtonModeEnable = (mode == NAV_BAR_MODE_3BUTTON); + updateIsEnabled(); + } + + @Override + public void onRotateDisplay(int displayId, int fromRotation, int toRotation, + WindowContainerTransaction t) { + mRotation = toRotation; + } + + private class SysUiInputEventReceiver extends InputEventReceiver { + SysUiInputEventReceiver(InputChannel channel, Looper looper) { + super(channel, looper); + } + + public void onInputEvent(InputEvent event) { + OneHandedGestureHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } + + private boolean isValidStartAngle(float deltaX, float deltaY) { + final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); + return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN); + } + + private boolean isValidExitAngle(float deltaX, float deltaY) { + final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); + return angle > ANGLE_MIN && angle < ANGLE_MAX; + } + + private float squaredHypot(float x, float y) { + return x * x + y * y; + } + + /** + * The touch(gesture) events to notify {@link OneHandedManager} start or stop one handed + */ + public interface OneHandedGestureEventCallback { + /** + * Handle the start event event, and return whether the event was consumed. + */ + boolean onStart(); + + /** + * Handle the exit event event, and return whether the event was consumed. + */ + boolean onStop(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedManagerImpl.java b/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedManagerImpl.java index bae25757b4e0b..d60b43f7a0aaa 100644 --- a/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/onehanded/OneHandedManagerImpl.java @@ -52,6 +52,8 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { private float mOffSetFraction; private DisplayController mDisplayController; private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer; + private OneHandedGestureHandler mGestureHandler; + private OneHandedGestureHandler.OneHandedGestureEventCallback mGestureEventCallback; private OneHandedTimeoutHandler mTimeoutHandler; private OneHandedTouchHandler mTouchHandler; private OneHandedTouchHandler.OneHandedTouchEventCallback mTouchEventCallback; @@ -89,6 +91,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { DisplayController displayController, OneHandedDisplayAreaOrganizer displayAreaOrganizer, OneHandedTouchHandler touchHandler, + OneHandedGestureHandler gestureHandler, SysUiState sysUiState) { mDisplayAreaOrganizer = displayAreaOrganizer; @@ -100,6 +103,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { context.getContentResolver()); mTimeoutHandler = OneHandedTimeoutHandler.get(); mTouchHandler = touchHandler; + mGestureHandler = gestureHandler; updateOneHandedEnabled(); setupGestures(); } @@ -170,6 +174,29 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { }; mTouchHandler.registerTouchEventListener(mTouchEventCallback); + mGestureEventCallback = new OneHandedGestureHandler.OneHandedGestureEventCallback() { + @Override + public boolean onStart() { + boolean result = false; + if (!mDisplayAreaOrganizer.isInOneHanded()) { + startOneHanded(); + result = true; + } + return result; + } + + @Override + public boolean onStop() { + boolean result = false; + if (mDisplayAreaOrganizer.isInOneHanded()) { + stopOneHanded(); + result = true; + } + return result; + } + }; + mGestureHandler.setGestureEventListener(mGestureEventCallback); + mTransitionCallback = new OneHandedTransitionCallback() { @Override public void onStartFinished(Rect bounds) { @@ -185,6 +212,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { }; mDisplayAreaOrganizer.registerTransitionCallback(mTransitionCallback); mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler); + mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler); } /** @@ -215,6 +243,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable { ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); } mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled); + mGestureHandler.onOneHandedEnabled(mIsOneHandedEnabled); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedGestureHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedGestureHandlerTest.java new file mode 100644 index 0000000000000..180c4507bd091 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedGestureHandlerTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2020 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.onehanded; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Instrumentation; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.systemui.model.SysUiState; +import com.android.systemui.statusbar.phone.NavigationModeController; +import com.android.systemui.wm.DisplayController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class OneHandedGestureHandlerTest extends OneHandedTestCase { + Instrumentation mInstrumentation; + OneHandedTouchHandler mTouchHandler; + OneHandedGestureHandler mGestureHandler; + OneHandedManagerImpl mOneHandedManagerImpl; + @Mock + DisplayController mMockDisplayController; + @Mock + OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; + @Mock + SysUiState mMockSysUiState; + @Mock + NavigationModeController mMockNavigationModeController; + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mTouchHandler = Mockito.spy(new OneHandedTouchHandler()); + mGestureHandler = Mockito.spy(new OneHandedGestureHandler( + mContext, mMockDisplayController, mMockNavigationModeController)); + mOneHandedManagerImpl = new OneHandedManagerImpl(mInstrumentation.getContext(), + mMockDisplayController, + mMockDisplayAreaOrganizer, + mTouchHandler, + mGestureHandler, + mMockSysUiState); + } + + @Test + public void testOneHandedManager_registerForDisplayAreaOrganizer() { + verify(mMockDisplayAreaOrganizer, times(1)).registerTransitionCallback(mGestureHandler); + } + + @Test + public void testOneHandedManager_setGestureEventListener() { + verify(mGestureHandler).setGestureEventListener(any()); + + assertThat(mGestureHandler.mGestureEventCallback).isNotNull(); + } + + @Test + public void testReceiveNewConfig_whenSetOneHandedEnabled() { + // 1st called at init + verify(mGestureHandler).onOneHandedEnabled(true); + mOneHandedManagerImpl.setOneHandedEnabled(true); + // 2nd called by setOneHandedEnabled() + verify(mGestureHandler, times(2)).onOneHandedEnabled(true); + } + + @Test + public void testOneHandedDisabled_shouldDisposeInputChannel() { + mOneHandedManagerImpl.setOneHandedEnabled(false); + + assertThat(mGestureHandler.mInputMonitor).isNull(); + assertThat(mGestureHandler.mInputEventReceiver).isNull(); + } + + @Test + public void testChangeNavBarTo2Button_shouldDisposeInputChannel() { + // 1st called at init + verify(mGestureHandler).onOneHandedEnabled(true); + mOneHandedManagerImpl.setOneHandedEnabled(true); + // 2nd called by setOneHandedEnabled() + verify(mGestureHandler, times(2)).onOneHandedEnabled(true); + + mGestureHandler.onNavigationModeChanged(NAV_BAR_MODE_2BUTTON); + + assertThat(mGestureHandler.mInputMonitor).isNull(); + assertThat(mGestureHandler.mInputEventReceiver).isNull(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedManagerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedManagerImplTest.java index e2a4b10951c75..f4fc7eafe2479 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedManagerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedManagerImplTest.java @@ -57,6 +57,8 @@ public class OneHandedManagerImplTest extends OneHandedTestCase { @Mock OneHandedTouchHandler mMockTouchHandler; @Mock + OneHandedGestureHandler mMockGestureHandler; + @Mock SysUiState mMockSysUiState; @Before @@ -66,6 +68,7 @@ public class OneHandedManagerImplTest extends OneHandedTestCase { mMockDisplayController, mMockDisplayAreaOrganizer, mMockTouchHandler, + mMockGestureHandler, mMockSysUiState); mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedTouchHandlerTest.java index 6db2e1b7e3300..80fe0f0950209 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/onehanded/OneHandedTouchHandlerTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.systemui.model.SysUiState; +import com.android.systemui.statusbar.phone.NavigationModeController; import com.android.systemui.wm.DisplayController; import org.junit.Before; @@ -45,10 +46,13 @@ import org.mockito.MockitoAnnotations; public class OneHandedTouchHandlerTest extends OneHandedTestCase { Instrumentation mInstrumentation; OneHandedTouchHandler mTouchHandler; + OneHandedGestureHandler mGestureHandler; OneHandedManagerImpl mOneHandedManagerImpl; @Mock DisplayController mMockDisplayController; @Mock + NavigationModeController mMockNavigationModeController; + @Mock OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer; @Mock SysUiState mMockSysUiState; @@ -58,10 +62,13 @@ public class OneHandedTouchHandlerTest extends OneHandedTestCase { MockitoAnnotations.initMocks(this); mInstrumentation = InstrumentationRegistry.getInstrumentation(); mTouchHandler = Mockito.spy(new OneHandedTouchHandler()); + mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController, + mMockNavigationModeController); mOneHandedManagerImpl = new OneHandedManagerImpl(mInstrumentation.getContext(), mMockDisplayController, mMockDisplayAreaOrganizer, mTouchHandler, + mGestureHandler, mMockSysUiState); }