Merge "Support NavigationBar 3-button mode gesture for One-Handed mode"

This commit is contained in:
Jason Chang
2020-06-29 14:26:52 +00:00
committed by Android (Google) Code Review
6 changed files with 446 additions and 0 deletions

View File

@@ -1384,4 +1384,8 @@
<dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
<dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
<dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
<!-- One-Handed Mode -->
<!-- Threshold for dragging distance to enable one-handed mode -->
<dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
</resources>

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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());

View File

@@ -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);
}