diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 9dcf3e5321c5f..45babb252a9fb 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1384,4 +1384,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);
}