diff options
author | 2018-11-12 19:44:42 +0000 | |
---|---|---|
committer | 2018-11-12 19:44:42 +0000 | |
commit | b4c2dc0c1fc09acabfffa1f239e8fa48c75aecae (patch) | |
tree | 5a3dee986ac70173ca75a187ac20bc103de6daa8 | |
parent | 252c7c43c916aafb1f7d137285b64a4e667ffcbf (diff) | |
parent | 86a436efb8ab9898740c3594950eb332d8dd095d (diff) |
Merge "Refactor QuickStepController into Gestures"
9 files changed, 1623 insertions, 491 deletions
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java index 814324e63d19..99cc3a37d739 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java @@ -36,7 +36,7 @@ public interface NavGesture extends Plugin { public boolean onInterceptTouchEvent(MotionEvent event); - public void setBarState(boolean vertical, boolean isRtl); + public void setBarState(boolean isRtl, int navBarPosition); public void onDraw(Canvas canvas); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java index 4eca6bb4c3e7..119f01adddf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java @@ -263,6 +263,16 @@ public class ButtonDispatcher { } } + public void setTranslation(int x, int y, int z) { + final int N = mViews.size(); + for (int i = 0; i < N; i++) { + final View view = mViews.get(i); + view.setTranslationX(x); + view.setTranslationY(y); + view.setTranslationZ(z); + } + } + public ArrayList<View> getViews() { return mViews; } @@ -276,6 +286,11 @@ public class ButtonDispatcher { if (mImageDrawable != null) { mImageDrawable.setCallback(mCurrentView); } + if (mCurrentView != null) { + mCurrentView.setTranslationX(0); + mCurrentView.setTranslationY(0); + mCurrentView.setTranslationZ(0); + } } public void setVertical(boolean vertical) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java new file mode 100644 index 000000000000..1002f9e45b3c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 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.statusbar.phone; + +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME; + +import android.annotation.NonNull; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.SystemClock; +import android.view.HapticFeedbackConstants; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.android.systemui.recents.OverviewProxyService; + +/** + * A back action when triggered will execute a back command + */ +public class NavigationBackAction extends NavigationGestureAction { + + private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback"; + private static final String BACK_AFTER_END_PROP = + "quickstepcontroller_homegoesbackwhenend"; + private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled"; + private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60; + private static final long BACK_GESTURE_POLL_TIMEOUT = 1000; + + private final Handler mHandler = new Handler(); + + private final Runnable mExecuteBackRunnable = new Runnable() { + @Override + public void run() { + if (isEnabled() && canPerformAction()) { + performBack(); + mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT); + } + } + }; + + public NavigationBackAction(@NonNull NavigationBarView navigationBarView, + @NonNull OverviewProxyService service) { + super(navigationBarView, service); + } + + @Override + public int requiresTouchDownHitTarget() { + return HIT_TARGET_HOME; + } + + @Override + public boolean requiresDragWithHitTarget() { + return true; + } + + @Override + public boolean canPerformAction() { + return mProxySender.getBackButtonAlpha() > 0; + } + + @Override + public boolean isEnabled() { + return swipeHomeGoBackGestureEnabled(); + } + + @Override + protected void onGestureStart(MotionEvent event) { + if (!QuickStepController.shouldhideBackButton(getContext())) { + mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */, + BACK_BUTTON_FADE_OUT_ALPHA); + } + mHandler.removeCallbacks(mExecuteBackRunnable); + if (!shouldExecuteBackOnUp()) { + performBack(); + mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT); + } + } + + @Override + protected void onGestureEnd() { + mHandler.removeCallbacks(mExecuteBackRunnable); + if (!QuickStepController.shouldhideBackButton(getContext())) { + mNavigationBarView.getBackButton().setAlpha( + mProxySender.getBackButtonAlpha(), true /* animate */); + } + if (shouldExecuteBackOnUp()) { + performBack(); + } + } + + private void performBack() { + sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); + sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); + mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + + private boolean swipeHomeGoBackGestureEnabled() { + return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED) + && getGlobalBoolean(PULL_HOME_GO_BACK_PROP); + } + + private boolean shouldExecuteBackOnUp() { + return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED) + && getGlobalBoolean(BACK_AFTER_END_PROP); + } + + private void sendEvent(int action, int code) { + long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, + 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, + KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 6728f08581ec..2c3c27fe5039 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -38,9 +38,11 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.RemoteException; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; +import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.MotionEvent; @@ -49,6 +51,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.inputmethod.InputMethodManager; @@ -143,6 +146,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private RecentsOnboarding mRecentsOnboarding; private NotificationPanelView mPanelView; + private QuickScrubAction mQuickScrubAction; + private QuickStepAction mQuickStepAction; + private NavigationBackAction mBackAction; + /** * Helper that is responsible for showing the right toast when a disallowed activity operation * occurred. In pinned mode, we show instructions on how to break out of this mode, whilst in @@ -299,6 +306,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton); mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup); mDeadZone = new DeadZone(this); + + mQuickScrubAction = new QuickScrubAction(this, mOverviewProxyService); + mQuickStepAction = new QuickStepAction(this, mOverviewProxyService); + mBackAction = new NavigationBackAction(this, mOverviewProxyService); } public BarTransitions getBarTransitions() { @@ -313,6 +324,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mPanelView = panel; if (mGestureHelper instanceof QuickStepController) { ((QuickStepController) mGestureHelper).setComponents(this); + ((QuickStepController) mGestureHelper).setGestureActions(mQuickStepAction, + null /* swipeDownAction*/, mBackAction, mQuickScrubAction); } } @@ -756,24 +769,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mRecentsOnboarding.hide(true); } - /** - * @return the button at the given {@param x} and {@param y}. - */ - ButtonDispatcher getButtonAtPosition(int x, int y) { - for (int i = 0; i < mButtonDispatchers.size(); i++) { - ButtonDispatcher button = mButtonDispatchers.valueAt(i); - View buttonView = button.getCurrentView(); - if (buttonView != null) { - buttonView.getHitRect(mTmpRect); - offsetDescendantRectToMyCoords(buttonView, mTmpRect); - if (mTmpRect.contains(x, y)) { - return button; - } - } - } - return null; - } - @Override public void onFinishInflate() { mNavigationInflaterView = findViewById(R.id.navigation_inflater); @@ -908,7 +903,13 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private void updateTaskSwitchHelper() { if (mGestureHelper == null) return; boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - mGestureHelper.setBarState(mVertical, isRtl); + int navBarPos = 0; + try { + navBarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get nav bar position.", e); + } + mGestureHelper.setBarState(isRtl, navBarPos); } @Override @@ -1112,6 +1113,14 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mContextualButtonGroup.dump(pw); if (mGestureHelper != null) { + pw.println("Navigation Gesture Actions {"); + pw.print(" "); pw.println("QuickScrub Enabled=" + mQuickScrubAction.isEnabled()); + pw.print(" "); pw.println("QuickScrub Active=" + mQuickScrubAction.isActive()); + pw.print(" "); pw.println("QuickStep Enabled=" + mQuickStepAction.isEnabled()); + pw.print(" "); pw.println("QuickStep Active=" + mQuickStepAction.isActive()); + pw.print(" "); pw.println("Back Gesture Enabled=" + mBackAction.isEnabled()); + pw.print(" "); pw.println("Back Gesture Active=" + mBackAction.isActive()); + pw.println("}"); mGestureHelper.dump(pw); } mRecentsOnboarding.dump(pw); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java new file mode 100644 index 000000000000..593bfae2aa4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 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.statusbar.phone; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT; + +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Canvas; +import android.view.MotionEvent; + +import android.view.WindowManagerPolicyConstants; +import com.android.systemui.recents.OverviewProxyService; + +/** + * A gesture action that would be triggered and reassigned by {@link QuickStepController} + */ +public abstract class NavigationGestureAction { + + protected final NavigationBarView mNavigationBarView; + protected final OverviewProxyService mProxySender; + + protected int mNavigationBarPosition; + protected boolean mDragHorizontalPositive; + protected boolean mDragVerticalPositive; + private boolean mIsActive; + + public NavigationGestureAction(@NonNull NavigationBarView navigationBarView, + @NonNull OverviewProxyService service) { + mNavigationBarView = navigationBarView; + mProxySender = service; + } + + /** + * Pass event that the state of the bar (such as rotation) has changed + * @param changed if rotation or drag positive direction (such as ltr) has changed + * @param navBarPos position of navigation bar + * @param dragHorPositive direction of positive horizontal drag, could change with ltr changes + * @param dragVerPositive direction of positive vertical drag, could change with ltr changes + */ + public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive, + boolean dragVerPositive) { + mNavigationBarPosition = navBarPos; + mDragHorizontalPositive = dragHorPositive; + mDragVerticalPositive = dragVerPositive; + } + + /** + * Resets the state of the action. Called when touch down occurs over the Navigation Bar. + */ + public void reset() { + mIsActive = false; + } + + /** + * Start the gesture and the action will be active + * @param event the event that caused the gesture + */ + public void startGesture(MotionEvent event) { + mIsActive = true; + onGestureStart(event); + } + + /** + * Gesture has ended with action cancel or up and this action will not be active + */ + public void endGesture() { + mIsActive = false; + onGestureEnd(); + } + + /** + * If the action is currently active based on the gesture that triggered it. Only one action + * can occur at a time + * @return whether or not if this action has been triggered + */ + public boolean isActive() { + return mIsActive; + } + + /** + * @return whether or not this action can run if notification shade is shown + */ + public boolean canRunWhenNotificationsShowing() { + return true; + } + + /** + * @return whether or not this action triggers when starting a gesture from a certain hit target + * If {@link HIT_TARGET_NONE} is specified then action does not need to be triggered by button + */ + public int requiresTouchDownHitTarget() { + return HIT_TARGET_NONE; + } + + /** + * @return whether or not to move the button that started gesture over with user input drag + */ + public boolean requiresDragWithHitTarget() { + return false; + } + + /** + * Tell if the action is able to execute. Note that {@link #isEnabled()} must be true for this + * to be checked. The difference between this and {@link #isEnabled()} is that this dependent + * on the state of the navigation bar + * @return true if action can execute after gesture activates based on current states + */ + public boolean canPerformAction() { + return true; + } + + /** + * Tell if action is enabled. Compared to {@link #canPerformAction()} this is based on settings + * if the action is disabled for a particular gesture. For example a back action can be enabled + * however if there is nothing to back to then {@link #canPerformAction()} should return false. + * In this way if the action requires {@link #requiresDragWithHitTarget()} then if enabled, the + * button can be dragged with a large dampening factor during the gesture but will not activate + * the action. + * @return true if this action is enabled and can run + */ + public abstract boolean isEnabled(); + + protected void onDarkIntensityChange(float intensity) { + } + + protected void onDraw(Canvas canvas) { + } + + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + } + + /** + * When gesture starts, this will run to execute the action + * @param event the event that triggered the gesture + */ + protected abstract void onGestureStart(MotionEvent event); + + /** + * Channels motion move events to the action to track the user inputs + * @param x the x position + * @param y the y position + */ + public void onGestureMove(int x, int y) { + } + + /** + * When gesture ends, this will run from action up or cancel + */ + protected void onGestureEnd() { + } + + protected Context getContext() { + return mNavigationBarView.getContext(); + } + + protected boolean isNavBarVertical() { + return mNavigationBarPosition == NAV_BAR_LEFT || mNavigationBarPosition == NAV_BAR_RIGHT; + } + + protected boolean getGlobalBoolean(@NonNull String key) { + return QuickStepController.getBoolGlobalSetting(getContext(), key); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java new file mode 100644 index 000000000000..c64e12478098 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2018 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.statusbar.phone; + +import static com.android.systemui.Interpolators.ALPHA_IN; +import static com.android.systemui.Interpolators.ALPHA_OUT; +import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY; +import static com.android.systemui.recents.OverviewProxyService.TAG_OPS; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.annotation.NonNull; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.Shader; +import android.os.RemoteException; + +import android.util.FloatProperty; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import com.android.systemui.R; +import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.shared.recents.utilities.Utilities; + +/** + * QuickScrub action to send to launcher to start quickscrub gesture + */ +public class QuickScrubAction extends NavigationGestureAction { + private static final String TAG = "QuickScrubAction"; + + private static final float TRACK_SCALE = 0.95f; + private static final float GRADIENT_WIDTH = .75f; + private static final int ANIM_IN_DURATION_MS = 150; + private static final int ANIM_OUT_DURATION_MS = 134; + + private AnimatorSet mTrackAnimator; + private View mCurrentNavigationBarView; + + private float mTrackScale = TRACK_SCALE; + private float mTrackAlpha; + private float mHighlightCenter; + private float mDarkIntensity; + + private final int mTrackThickness; + private final int mTrackEndPadding; + private final Paint mTrackPaint = new Paint(); + private final Rect mTrackRect = new Rect(); + + private final FloatProperty<QuickScrubAction> mTrackAlphaProperty = + new FloatProperty<QuickScrubAction>("TrackAlpha") { + @Override + public void setValue(QuickScrubAction action, float alpha) { + mTrackAlpha = alpha; + mNavigationBarView.invalidate(); + } + + @Override + public Float get(QuickScrubAction action) { + return mTrackAlpha; + } + }; + + private final FloatProperty<QuickScrubAction> mTrackScaleProperty = + new FloatProperty<QuickScrubAction>("TrackScale") { + @Override + public void setValue(QuickScrubAction action, float scale) { + mTrackScale = scale; + mNavigationBarView.invalidate(); + } + + @Override + public Float get(QuickScrubAction action) { + return mTrackScale; + } + }; + + private final FloatProperty<QuickScrubAction> mNavBarAlphaProperty = + new FloatProperty<QuickScrubAction>("NavBarAlpha") { + @Override + public void setValue(QuickScrubAction action, float alpha) { + if (mCurrentNavigationBarView != null) { + mCurrentNavigationBarView.setAlpha(alpha); + } + } + + @Override + public Float get(QuickScrubAction action) { + if (mCurrentNavigationBarView != null) { + return mCurrentNavigationBarView.getAlpha(); + } + return 1f; + } + }; + + private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mCurrentNavigationBarView != null) { + mCurrentNavigationBarView.setAlpha(1f); + } + mCurrentNavigationBarView = null; + updateHighlight(); + } + }; + + public QuickScrubAction(@NonNull NavigationBarView navigationBarView, + @NonNull OverviewProxyService service) { + super(navigationBarView, service); + mTrackPaint.setAntiAlias(true); + mTrackPaint.setDither(true); + + final Resources res = navigationBarView.getResources(); + mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness); + mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding); + } + + @Override + public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive, + boolean dragVerPositive) { + super.setBarState(changed, navBarPos, dragHorPositive, dragVerPositive); + if (changed && isActive()) { + // End quickscrub if the state changes mid-transition + endQuickScrub(false /* animate */); + } + } + + @Override + public void reset() { + super.reset(); + + // End any existing quickscrub animations before starting the new transition + if (mTrackAnimator != null) { + mTrackAnimator.end(); + mTrackAnimator = null; + } + mCurrentNavigationBarView = mNavigationBarView.getCurrentView(); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int paddingLeft = mNavigationBarView.getPaddingLeft(); + final int paddingTop = mNavigationBarView.getPaddingTop(); + final int paddingRight = mNavigationBarView.getPaddingRight(); + final int paddingBottom = mNavigationBarView.getPaddingBottom(); + final int width = (right - left) - paddingRight - paddingLeft; + final int height = (bottom - top) - paddingBottom - paddingTop; + final int x1, x2, y1, y2; + if (isNavBarVertical()) { + x1 = (width - mTrackThickness) / 2 + paddingLeft; + x2 = x1 + mTrackThickness; + y1 = paddingTop + mTrackEndPadding; + y2 = y1 + height - 2 * mTrackEndPadding; + } else { + y1 = (height - mTrackThickness) / 2 + paddingTop; + y2 = y1 + mTrackThickness; + x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding; + x2 = x1 + width - 2 * mTrackEndPadding; + } + mTrackRect.set(x1, y1, x2, y2); + } + + @Override + public void onDarkIntensityChange(float intensity) { + mDarkIntensity = intensity; + updateHighlight(); + } + + @Override + public void onDraw(Canvas canvas) { + if (!isEnabled()) { + return; + } + mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha)); + + // Scale the track, but apply the inverse scale from the nav bar + final float radius = mTrackRect.height() / 2; + canvas.save(); + float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right); + canvas.translate(translate, 0); + canvas.scale(mTrackScale / mNavigationBarView.getScaleX(), + 1f / mNavigationBarView.getScaleY(), + mTrackRect.centerX(), mTrackRect.centerY()); + canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top, + mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint); + canvas.restore(); + } + + @Override + public boolean isEnabled() { + return mNavigationBarView.isQuickScrubEnabled(); + } + + @Override + protected void onGestureStart(MotionEvent event) { + updateHighlight(); + ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f), + PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f)); + trackAnimator.setInterpolator(ALPHA_IN); + trackAnimator.setDuration(ANIM_IN_DURATION_MS); + ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f); + navBarAnimator.setInterpolator(ALPHA_OUT); + navBarAnimator.setDuration(ANIM_OUT_DURATION_MS); + mTrackAnimator = new AnimatorSet(); + mTrackAnimator.playTogether(trackAnimator, navBarAnimator); + mTrackAnimator.start(); + + // Disable slippery for quick scrub to not cancel outside the nav bar + mNavigationBarView.updateSlippery(); + + try { + mProxySender.getProxy().onQuickScrubStart(); + if (DEBUG_OVERVIEW_PROXY) { + Log.d(TAG_OPS, "Quick Scrub Start"); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to send start of quick scrub.", e); + } + mProxySender.notifyQuickScrubStarted(); + } + + @Override + public void onGestureMove(int x, int y) { + int trackSize, offset; + if (isNavBarVertical()) { + trackSize = mTrackRect.height(); + offset = y - mTrackRect.top; + } else { + offset = x - mTrackRect.left; + trackSize = mTrackRect.width(); + } + if (!mDragHorizontalPositive || !mDragVerticalPositive) { + offset -= isNavBarVertical() ? mTrackRect.height() : mTrackRect.width(); + } + float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1); + try { + mProxySender.getProxy().onQuickScrubProgress(scrubFraction); + if (DEBUG_OVERVIEW_PROXY) { + Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to send progress of quick scrub.", e); + } + mHighlightCenter = x; + mNavigationBarView.invalidate(); + } + + @Override + protected void onGestureEnd() { + endQuickScrub(true /* animate */); + } + + private void endQuickScrub(boolean animate) { + animateEnd(); + try { + mProxySender.getProxy().onQuickScrubEnd(); + if (DEBUG_OVERVIEW_PROXY) { + Log.d(TAG_OPS, "Quick Scrub End"); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to send end of quick scrub.", e); + } + if (!animate) { + if (mTrackAnimator != null) { + mTrackAnimator.end(); + mTrackAnimator = null; + } + } + } + + private void updateHighlight() { + if (mTrackRect.isEmpty()) { + return; + } + int colorBase, colorGrad; + if (mDarkIntensity > 0.5f) { + colorBase = getContext().getColor(R.color.quick_step_track_background_background_dark); + colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_dark); + } else { + colorBase = getContext().getColor(R.color.quick_step_track_background_background_light); + colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_light); + } + final RadialGradient mHighlight = new RadialGradient(0, mTrackRect.height() / 2, + mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase, + Shader.TileMode.CLAMP); + mTrackPaint.setShader(mHighlight); + } + + private void animateEnd() { + if (mTrackAnimator != null) { + mTrackAnimator.cancel(); + } + + ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f), + PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE)); + trackAnimator.setInterpolator(ALPHA_OUT); + trackAnimator.setDuration(ANIM_OUT_DURATION_MS); + ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f); + navBarAnimator.setInterpolator(ALPHA_IN); + navBarAnimator.setDuration(ANIM_IN_DURATION_MS); + mTrackAnimator = new AnimatorSet(); + mTrackAnimator.playTogether(trackAnimator, navBarAnimator); + mTrackAnimator.addListener(mQuickScrubEndListener); + mTrackAnimator.start(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java new file mode 100644 index 000000000000..b18b79e0e6d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.statusbar.phone; + +import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY; +import static com.android.systemui.recents.OverviewProxyService.TAG_OPS; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.systemui.recents.OverviewProxyService; + +/** + * QuickStep action to send to launcher to start overview + */ +public class QuickStepAction extends NavigationGestureAction { + private static final String TAG = "QuickStepAction"; + + public QuickStepAction(@NonNull NavigationBarView navigationBarView, + @NonNull OverviewProxyService service) { + super(navigationBarView, service); + } + + @Override + public boolean canRunWhenNotificationsShowing() { + return false; + } + + @Override + public boolean isEnabled() { + return mNavigationBarView.isQuickStepSwipeUpEnabled(); + } + + @Override + public void onGestureStart(MotionEvent event) { + try { + mProxySender.getProxy().onQuickStep(event); + if (DEBUG_OVERVIEW_PROXY) { + Log.d(TAG_OPS, "Quick Step Start"); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to send quick step started.", e); + } + mProxySender.notifyQuickStepStarted(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java index 398012601587..c03800e5274a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java @@ -18,187 +18,96 @@ package com.android.systemui.statusbar.phone; import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; -import static com.android.systemui.Interpolators.ALPHA_IN; -import static com.android.systemui.Interpolators.ALPHA_OUT; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT; + import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY; import static com.android.systemui.recents.OverviewProxyService.TAG_OPS; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE; import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.RadialGradient; -import android.graphics.Rect; -import android.graphics.Shader; -import android.hardware.input.InputManager; -import android.os.Handler; import android.os.RemoteException; -import android.os.SystemClock; import android.provider.Settings; -import android.util.FloatProperty; import android.util.Log; -import android.util.Slog; -import android.view.HapticFeedbackConstants; -import android.view.InputDevice; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewPropertyAnimator; -import android.view.WindowManagerGlobal; + import com.android.systemui.Dependency; import com.android.systemui.Interpolators; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper; import com.android.systemui.R; +import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.SysUiServiceProvider; -import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper; import com.android.systemui.shared.recents.IOverviewProxy; -import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.system.NavigationBarCompat; + import java.io.PrintWriter; /** * Class to detect gestures on the navigation bar and implement quick scrub. + * Note that the variables in this class horizontal and vertical represents horizontal always + * aligned with along the navigation bar). */ public class QuickStepController implements GestureHelper { private static final String TAG = "QuickStepController"; - private static final int ANIM_IN_DURATION_MS = 150; - private static final int ANIM_OUT_DURATION_MS = 134; - private static final float TRACK_SCALE = 0.95f; - private static final float GRADIENT_WIDTH = .75f; /** Experiment to swipe home button left to execute a back key press */ - private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback"; private static final String HIDE_BACK_BUTTON_PROP = "quickstepcontroller_hideback"; - private static final String BACK_AFTER_END_PROP - = "quickstepcontroller_homegoesbackwhenend"; - private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled"; - private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60; private static final long BACK_BUTTON_FADE_IN_ALPHA = 150; - private static final long BACK_GESTURE_POLL_TIMEOUT = 1000; /** When the home-swipe-back gesture is disallowed, make it harder to pull */ private static final float DISALLOW_GESTURE_DAMPING_FACTOR = 0.16f; + private static final int ACTION_SWIPE_UP_INDEX = 0; + private static final int ACTION_SWIPE_DOWN_INDEX = 1; + private static final int ACTION_SWIPE_LEFT_INDEX = 2; + private static final int ACTION_SWIPE_RIGHT_INDEX = 3; + private static final int MAX_GESTURES = 4; + private NavigationBarView mNavigationBarView; - private boolean mQuickScrubActive; private boolean mAllowGestureDetection; - private boolean mBackGestureActive; - private boolean mCanPerformBack; - private boolean mQuickStepStarted; private boolean mNotificationsVisibleOnDown; private int mTouchDownX; private int mTouchDownY; - private boolean mDragPositive; - private boolean mIsVertical; + private boolean mDragHPositive; + private boolean mDragVPositive; private boolean mIsRTL; - private float mTrackAlpha; - private float mTrackScale = TRACK_SCALE; + private int mNavBarPosition; private float mDarkIntensity; - private RadialGradient mHighlight; - private float mHighlightCenter; - private AnimatorSet mTrackAnimator; - private ViewPropertyAnimator mHomeAnimator; + private ViewPropertyAnimator mDragBtnAnimator; private ButtonDispatcher mHitTarget; - private View mCurrentNavigationBarView; private boolean mIsInScreenPinning; + private boolean mGestureHorizontalDragsButton; + private boolean mGestureVerticalDragsButton; + private boolean mGestureTrackPositive; + + private NavigationGestureAction mCurrentAction; + private NavigationGestureAction[] mGestureActions = new NavigationGestureAction[MAX_GESTURES]; - private final Handler mHandler = new Handler(); - private final Rect mTrackRect = new Rect(); private final OverviewProxyService mOverviewEventSender; - private final int mTrackThickness; - private final int mTrackEndPadding; private final int mHomeBackGestureDragLimit; private final Context mContext; private final StatusBar mStatusBar; private final Matrix mTransformGlobalMatrix = new Matrix(); private final Matrix mTransformLocalMatrix = new Matrix(); - private final Paint mTrackPaint = new Paint(); - - private final FloatProperty<QuickStepController> mTrackAlphaProperty = - new FloatProperty<QuickStepController>("TrackAlpha") { - @Override - public void setValue(QuickStepController controller, float alpha) { - mTrackAlpha = alpha; - mNavigationBarView.invalidate(); - } - - @Override - public Float get(QuickStepController controller) { - return mTrackAlpha; - } - }; - - private final FloatProperty<QuickStepController> mTrackScaleProperty = - new FloatProperty<QuickStepController>("TrackScale") { - @Override - public void setValue(QuickStepController controller, float scale) { - mTrackScale = scale; - mNavigationBarView.invalidate(); - } - - @Override - public Float get(QuickStepController controller) { - return mTrackScale; - } - }; - - private final FloatProperty<QuickStepController> mNavBarAlphaProperty = - new FloatProperty<QuickStepController>("NavBarAlpha") { - @Override - public void setValue(QuickStepController controller, float alpha) { - if (mCurrentNavigationBarView != null) { - mCurrentNavigationBarView.setAlpha(alpha); - } - } - - @Override - public Float get(QuickStepController controller) { - if (mCurrentNavigationBarView != null) { - return mCurrentNavigationBarView.getAlpha(); - } - return 1f; - } - }; - - private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - resetQuickScrub(); - } - }; - - private final Runnable mExecuteBackRunnable = new Runnable() { - @Override - public void run() { - if (canPerformHomeBackGesture()) { - performBack(); - mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT); - } - } - }; public QuickStepController(Context context) { final Resources res = context.getResources(); mContext = context; mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class); mOverviewEventSender = Dependency.get(OverviewProxyService.class); - mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness); - mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding); mHomeBackGestureDragLimit = res.getDimensionPixelSize(R.dimen.nav_home_back_gesture_drag_limit); - mTrackPaint.setAntiAlias(true); - mTrackPaint.setDither(true); } public void setComponents(NavigationBarView navigationBarView) { @@ -210,6 +119,31 @@ public class QuickStepController implements GestureHelper { } /** + * Set each gesture an action. After set the gestures triggered will run the actions attached. + * @param swipeUpAction action after swiping up + * @param swipeDownAction action after swiping down + * @param swipeLeftAction action after swiping left + * @param swipeRightAction action after swiping right + */ + public void setGestureActions(@Nullable NavigationGestureAction swipeUpAction, + @Nullable NavigationGestureAction swipeDownAction, + @Nullable NavigationGestureAction swipeLeftAction, + @Nullable NavigationGestureAction swipeRightAction) { + mGestureActions[ACTION_SWIPE_UP_INDEX] = swipeUpAction; + mGestureActions[ACTION_SWIPE_DOWN_INDEX] = swipeDownAction; + mGestureActions[ACTION_SWIPE_LEFT_INDEX] = swipeLeftAction; + mGestureActions[ACTION_SWIPE_RIGHT_INDEX] = swipeRightAction; + + // Set the current state to all actions + for (NavigationGestureAction action: mGestureActions) { + if (action != null) { + action.setBarState(true, mNavBarPosition, mDragHPositive, mDragVPositive); + action.onDarkIntensityChange(mDarkIntensity); + } + } + } + + /** * @return true if we want to intercept touch events for quick scrub and prevent proxying the * event to the overview service. */ @@ -242,8 +176,10 @@ public class QuickStepController implements GestureHelper { private boolean handleTouchEvent(MotionEvent event) { final boolean deadZoneConsumed = mNavigationBarView.getDownHitTarget() == HIT_TARGET_DEAD_ZONE; - if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled() - && !mNavigationBarView.isQuickStepSwipeUpEnabled())) { + + // Requires proxy and an active gesture or able to perform any gesture to continue + if (mOverviewEventSender.getProxy() == null + || (mCurrentAction == null && !canPerformAnyAction())) { return deadZoneConsumed; } mNavigationBarView.requestUnbufferedDispatch(event); @@ -255,33 +191,45 @@ public class QuickStepController implements GestureHelper { int y = (int) event.getY(); mIsInScreenPinning = mNavigationBarView.inScreenPinning(); - // End any existing quickscrub animations before starting the new transition - if (mTrackAnimator != null) { - mTrackAnimator.end(); - mTrackAnimator = null; + for (NavigationGestureAction gestureAction: mGestureActions) { + if (gestureAction != null) { + gestureAction.reset(); + } } - mCurrentNavigationBarView = mNavigationBarView.getCurrentView(); - mHitTarget = mNavigationBarView.getButtonAtPosition(x, y); + // Valid buttons to drag over + switch (mNavigationBarView.getDownHitTarget()) { + case HIT_TARGET_BACK: + mHitTarget = mNavigationBarView.getBackButton(); + break; + case HIT_TARGET_HOME: + mHitTarget = mNavigationBarView.getHomeButton(); + break; + case HIT_TARGET_OVERVIEW: + mHitTarget = mNavigationBarView.getRecentsButton(); + break; + default: + mHitTarget = null; + break; + } if (mHitTarget != null) { // Pre-emptively delay the touch feedback for the button that we just touched mHitTarget.setDelayTouchFeedback(true); } mTouchDownX = x; mTouchDownY = y; + mGestureHorizontalDragsButton = false; + mGestureVerticalDragsButton = false; mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX); mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX); mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix); mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix); - mQuickStepStarted = false; - mBackGestureActive = false; mAllowGestureDetection = true; mNotificationsVisibleOnDown = !mNavigationBarView.isNotificationsFullyCollapsed(); - mCanPerformBack = canPerformHomeBackGesture(); break; } case MotionEvent.ACTION_MOVE: { - if (mQuickStepStarted || !mAllowGestureDetection){ + if (!mAllowGestureDetection) { break; } int x = (int) event.getX(); @@ -289,108 +237,132 @@ public class QuickStepController implements GestureHelper { int xDiff = Math.abs(x - mTouchDownX); int yDiff = Math.abs(y - mTouchDownY); - boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop; - int pos, touchDown, offset, trackSize; + boolean exceededSwipeHorizontalTouchSlop, exceededSwipeVerticalTouchSlop; + int posH, touchDownH, posV, touchDownV; - if (mIsVertical) { - exceededScrubTouchSlop = + if (isNavBarVertical()) { + exceededSwipeHorizontalTouchSlop = yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff; - exceededSwipeUpTouchSlop = + exceededSwipeVerticalTouchSlop = xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff; - pos = y; - touchDown = mTouchDownY; - offset = pos - mTrackRect.top; - trackSize = mTrackRect.height(); + posH = y; + touchDownH = mTouchDownY; + posV = x; + touchDownV = mTouchDownX; } else { - exceededScrubTouchSlop = + exceededSwipeHorizontalTouchSlop = xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff; - exceededSwipeUpTouchSlop = + exceededSwipeVerticalTouchSlop = yDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && yDiff > xDiff; - pos = x; - touchDown = mTouchDownX; - offset = pos - mTrackRect.left; - trackSize = mTrackRect.width(); - } - // Decide to start quickstep if dragging away from the navigation bar, otherwise in - // the parallel direction, decide to start quickscrub. Only one may run. - if (!mBackGestureActive && !mQuickScrubActive && exceededSwipeUpTouchSlop) { - if (mNavigationBarView.isQuickStepSwipeUpEnabled() - && !mNotificationsVisibleOnDown) { - startQuickStep(event); - } - break; + posH = x; + touchDownH = mTouchDownX; + posV = y; + touchDownV = mTouchDownY; } - // Do not handle quick scrub if disabled - if (!mNavigationBarView.isQuickScrubEnabled()) { - break; - } - - if (!mDragPositive) { - offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width(); - } - - final boolean allowDrag = !mDragPositive - ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown; - float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1); - if (!mQuickScrubActive && !mBackGestureActive && exceededScrubTouchSlop) { - // Passing the drag slop then touch slop will start quick step - if (allowDrag) { - startQuickScrub(); - } else if (swipeHomeGoBackGestureEnabled(mContext) - && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME - && mDragPositive ? pos < touchDown : pos > touchDown) { - startBackGesture(); - } - } - - if (mQuickScrubActive && (mDragPositive && offset >= 0 - || !mDragPositive && offset <= 0)) { - try { - mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction); - if (DEBUG_OVERVIEW_PROXY) { - Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction); + if (mCurrentAction != null) { + // Gesture started, provide positions to the current action + mCurrentAction.onGestureMove(x, y); + } else { + // Detect gesture and try to execute an action, only one can run at a time + if (exceededSwipeVerticalTouchSlop) { + if (mDragVPositive ? (posV < touchDownV) : (posV > touchDownV)) { + // Swiping up gesture + tryToStartGesture(mGestureActions[ACTION_SWIPE_UP_INDEX], + false /* alignedWithNavBar */, false /* positiveDirection */, + event); + } else { + // Swiping down gesture + tryToStartGesture(mGestureActions[ACTION_SWIPE_DOWN_INDEX], + false /* alignedWithNavBar */, true /* positiveDirection */, + event); } - } catch (RemoteException e) { - Log.e(TAG, "Failed to send progress of quick scrub.", e); - } - mHighlightCenter = x; - mNavigationBarView.invalidate(); - } else if (mBackGestureActive) { - int diff = pos - touchDown; - // If dragging the incorrect direction after starting back gesture or unable - // to execute back functionality, then move home but dampen its distance - if (!mCanPerformBack || (mDragPositive ? diff > 0 : diff < 0)) { - diff *= DISALLOW_GESTURE_DAMPING_FACTOR; - } if (Math.abs(diff) > mHomeBackGestureDragLimit) { - // Once the user drags the home button past a certain limit, the distance - // will lessen as the home button dampens showing that it was pulled too far - float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit) - * DISALLOW_GESTURE_DAMPING_FACTOR; - diff = (int)(distanceAfterDragLimit + mHomeBackGestureDragLimit); - if (mDragPositive) { - diff *= -1; + } else if (exceededSwipeHorizontalTouchSlop) { + if (mDragHPositive ? (posH < touchDownH) : (posH > touchDownH)) { + // Swiping left (ltr) gesture + tryToStartGesture(mGestureActions[ACTION_SWIPE_LEFT_INDEX], + true /* alignedWithNavBar */, false /* positiveDirection */, + event); + } else { + // Swiping right (ltr) gesture + tryToStartGesture(mGestureActions[ACTION_SWIPE_RIGHT_INDEX], + true /* alignedWithNavBar */, true /* positiveDirection */, + event); } } - moveHomeButton(diff); } + + handleDragHitTarget(mGestureHorizontalDragsButton ? posH : posV, + mGestureHorizontalDragsButton ? touchDownH : touchDownV); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - endQuickScrub(true /* animate */); - endBackGesture(); + if (mCurrentAction != null) { + mCurrentAction.endGesture(); + mCurrentAction = null; + } + + // Return the hit target back to its original position + if (mHitTarget != null) { + final View button = mHitTarget.getCurrentView(); + if (mGestureHorizontalDragsButton || mGestureVerticalDragsButton) { + mDragBtnAnimator = button.animate().setDuration(BACK_BUTTON_FADE_IN_ALPHA) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + if (mGestureVerticalDragsButton ^ isNavBarVertical()) { + mDragBtnAnimator.translationY(0); + } else { + mDragBtnAnimator.translationX(0); + } + mDragBtnAnimator.start(); + } + } break; } if (shouldProxyEvents(action)) { proxyMotionEvents(event); } - return mBackGestureActive || mQuickScrubActive || mQuickStepStarted || deadZoneConsumed; + return mCurrentAction != null || deadZoneConsumed; + } + + private void handleDragHitTarget(int position, int touchDown) { + // Drag the hit target if gesture action requires it + if (mHitTarget != null && (mGestureVerticalDragsButton || mGestureHorizontalDragsButton)) { + final View button = mHitTarget.getCurrentView(); + if (mDragBtnAnimator != null) { + mDragBtnAnimator.cancel(); + mDragBtnAnimator = null; + } + + int diff = position - touchDown; + // If dragging the incorrect direction after starting gesture or unable to + // execute tried action, then move the button but dampen its distance + if (mCurrentAction == null || (mGestureTrackPositive ? diff < 0 : diff > 0)) { + diff *= DISALLOW_GESTURE_DAMPING_FACTOR; + } else if (Math.abs(diff) > mHomeBackGestureDragLimit) { + // Once the user drags the button past a certain limit, the distance will + // lessen as the button dampens that it was pulled too far + float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit) + * DISALLOW_GESTURE_DAMPING_FACTOR; + diff = (int) (distanceAfterDragLimit + mHomeBackGestureDragLimit); + if (!mGestureTrackPositive) { + diff *= -1; + } + } + if (mGestureVerticalDragsButton ^ isNavBarVertical()) { + button.setTranslationY(diff); + } else { + button.setTranslationX(diff); + } + } } private boolean shouldProxyEvents(int action) { - if (!mBackGestureActive && !mQuickScrubActive && !mIsInScreenPinning) { + final boolean actionValid = (mCurrentAction == null + || (mGestureActions[ACTION_SWIPE_UP_INDEX] != null + && mGestureActions[ACTION_SWIPE_UP_INDEX].isActive())); + if (actionValid && !mIsInScreenPinning) { // Allow down, cancel and up events, move and other events are passed if notifications // are not showing and disabled gestures (such as long press) are not executed switch (action) { @@ -407,46 +379,18 @@ public class QuickStepController implements GestureHelper { @Override public void onDraw(Canvas canvas) { - if (!mNavigationBarView.isQuickScrubEnabled()) { - return; + if (mCurrentAction != null) { + mCurrentAction.onDraw(canvas); } - mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha)); - - // Scale the track, but apply the inverse scale from the nav bar - final float radius = mTrackRect.height() / 2; - canvas.save(); - float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right); - canvas.translate(translate, 0); - canvas.scale(mTrackScale / mNavigationBarView.getScaleX(), - 1f / mNavigationBarView.getScaleY(), - mTrackRect.centerX(), mTrackRect.centerY()); - canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top, - mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint); - canvas.restore(); } @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int paddingLeft = mNavigationBarView.getPaddingLeft(); - final int paddingTop = mNavigationBarView.getPaddingTop(); - final int paddingRight = mNavigationBarView.getPaddingRight(); - final int paddingBottom = mNavigationBarView.getPaddingBottom(); - final int width = (right - left) - paddingRight - paddingLeft; - final int height = (bottom - top) - paddingBottom - paddingTop; - final int x1, x2, y1, y2; - if (mIsVertical) { - x1 = (width - mTrackThickness) / 2 + paddingLeft; - x2 = x1 + mTrackThickness; - y1 = paddingTop + mTrackEndPadding; - y2 = y1 + height - 2 * mTrackEndPadding; - } else { - y1 = (height - mTrackThickness) / 2 + paddingTop; - y2 = y1 + mTrackThickness; - x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding; - x2 = x1 + width - 2 * mTrackEndPadding; + for (NavigationGestureAction action: mGestureActions) { + if (action != null) { + action.onLayout(changed, left, top, right, bottom); + } } - mTrackRect.set(x1, y1, x2, y2); - updateHighlight(); } @Override @@ -456,268 +400,118 @@ public class QuickStepController implements GestureHelper { // When in quick scrub, invalidate gradient if changing intensity from black to white and // vice-versa - if (mNavigationBarView.isQuickScrubEnabled() + if (mCurrentAction != null && mNavigationBarView.isQuickScrubEnabled() && Math.round(intensity) != Math.round(oldIntensity)) { - updateHighlight(); + mCurrentAction.onDarkIntensityChange(mDarkIntensity); } mNavigationBarView.invalidate(); } @Override - public void setBarState(boolean isVertical, boolean isRTL) { - final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL); - if (changed) { - // End quickscrub if the state changes mid-transition - endQuickScrub(false /* animate */); - } - mIsVertical = isVertical; + public void setBarState(boolean isRTL, int navBarPosition) { + final boolean changed = (mIsRTL != isRTL) || (mNavBarPosition != navBarPosition); mIsRTL = isRTL; - try { - int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition(); - mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM; - if (isRTL) { - mDragPositive = !mDragPositive; + mNavBarPosition = navBarPosition; + + // Determine the drag directions depending on location of nav bar + switch (navBarPosition) { + case NAV_BAR_LEFT: + mDragHPositive = !isRTL; + mDragVPositive = false; + break; + case NAV_BAR_RIGHT: + mDragHPositive = isRTL; + mDragVPositive = true; + break; + case NAV_BAR_BOTTOM: + mDragHPositive = !isRTL; + mDragVPositive = true; + break; + } + + for (NavigationGestureAction action: mGestureActions) { + if (action != null) { + action.setBarState(changed, mNavBarPosition, mDragHPositive, mDragVPositive); } - } catch (RemoteException e) { - Slog.e(TAG, "Failed to get nav bar position.", e); } } @Override public void onNavigationButtonLongPress(View v) { mAllowGestureDetection = false; - mHandler.removeCallbacksAndMessages(null); } @Override public void dump(PrintWriter pw) { pw.println("QuickStepController {"); - pw.print(" "); pw.println("mQuickScrubActive=" + mQuickScrubActive); - pw.print(" "); pw.println("mQuickStepStarted=" + mQuickStepStarted); pw.print(" "); pw.println("mAllowGestureDetection=" + mAllowGestureDetection); - pw.print(" "); pw.println("mBackGestureActive=" + mBackGestureActive); - pw.print(" "); pw.println("mCanPerformBack=" + mCanPerformBack); pw.print(" "); pw.println("mNotificationsVisibleOnDown=" + mNotificationsVisibleOnDown); - pw.print(" "); pw.println("mIsVertical=" + mIsVertical); + pw.print(" "); pw.println("mNavBarPosition=" + mNavBarPosition); pw.print(" "); pw.println("mIsRTL=" + mIsRTL); pw.print(" "); pw.println("mIsInScreenPinning=" + mIsInScreenPinning); pw.println("}"); } - private void startQuickStep(MotionEvent event) { - if (mIsInScreenPinning) { - mNavigationBarView.showPinningEscapeToast(); - mAllowGestureDetection = false; - return; - } - - mQuickStepStarted = true; - event.transform(mTransformGlobalMatrix); - try { - mOverviewEventSender.getProxy().onQuickStep(event); - if (DEBUG_OVERVIEW_PROXY) { - Log.d(TAG_OPS, "Quick Step Start"); - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to send quick step started.", e); - } finally { - event.transform(mTransformLocalMatrix); - } - mOverviewEventSender.notifyQuickStepStarted(); - mHandler.removeCallbacksAndMessages(null); - - if (mHitTarget != null) { - mHitTarget.abortCurrentGesture(); - } - - if (mQuickScrubActive) { - animateEnd(); - } + public NavigationGestureAction getCurrentAction() { + return mCurrentAction; } - private void startQuickScrub() { + private void tryToStartGesture(NavigationGestureAction action, boolean alignedWithNavBar, + boolean positiveDirection, MotionEvent event) { + if (action == null) { + return; + } if (mIsInScreenPinning) { mNavigationBarView.showPinningEscapeToast(); mAllowGestureDetection = false; return; } - if (!mQuickScrubActive) { - updateHighlight(); - mQuickScrubActive = true; - ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f), - PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f)); - trackAnimator.setInterpolator(ALPHA_IN); - trackAnimator.setDuration(ANIM_IN_DURATION_MS); - ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f); - navBarAnimator.setInterpolator(ALPHA_OUT); - navBarAnimator.setDuration(ANIM_OUT_DURATION_MS); - mTrackAnimator = new AnimatorSet(); - mTrackAnimator.playTogether(trackAnimator, navBarAnimator); - mTrackAnimator.start(); - - // Disable slippery for quick scrub to not cancel outside the nav bar - mNavigationBarView.updateSlippery(); - - try { - mOverviewEventSender.getProxy().onQuickScrubStart(); - if (DEBUG_OVERVIEW_PROXY) { - Log.d(TAG_OPS, "Quick Scrub Start"); - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to send start of quick scrub.", e); + // Start new action from gesture if is able to start and depending on notifications + // visibility and starting touch down target. If the action is enabled, then also check if + // can perform the action so that if action requires the button to be dragged, then the + // gesture will have a large dampening factor and prevent action from running. + final boolean validHitTarget = action.requiresTouchDownHitTarget() == HIT_TARGET_NONE + || action.requiresTouchDownHitTarget() == mNavigationBarView.getDownHitTarget(); + if (mCurrentAction == null && validHitTarget && action.isEnabled() + && (!mNotificationsVisibleOnDown || action.canRunWhenNotificationsShowing())) { + if (action.canPerformAction()) { + mCurrentAction = action; + event.transform(mTransformGlobalMatrix); + action.startGesture(event); + event.transform(mTransformLocalMatrix); } - mOverviewEventSender.notifyQuickScrubStarted(); - - if (mHitTarget != null) { - mHitTarget.abortCurrentGesture(); - } - } - } - private void endQuickScrub(boolean animate) { - if (mQuickScrubActive) { - animateEnd(); - try { - mOverviewEventSender.getProxy().onQuickScrubEnd(); - if (DEBUG_OVERVIEW_PROXY) { - Log.d(TAG_OPS, "Quick Scrub End"); + // Handle direction of the hit target drag from the axis that started the gesture + if (action.requiresDragWithHitTarget()) { + if (alignedWithNavBar) { + mGestureHorizontalDragsButton = true; + mGestureVerticalDragsButton = false; + if (positiveDirection) { + mGestureTrackPositive = mDragHPositive; + } + } else { + mGestureVerticalDragsButton = true; + mGestureHorizontalDragsButton = false; + if (positiveDirection) { + mGestureTrackPositive = mDragVPositive; + } } - } catch (RemoteException e) { - Log.e(TAG, "Failed to send end of quick scrub.", e); } - } - if (!animate) { - if (mTrackAnimator != null) { - mTrackAnimator.end(); - mTrackAnimator = null; - } - } - } - private void startBackGesture() { - if (!mBackGestureActive) { - mBackGestureActive = true; - mNavigationBarView.getHomeButton().abortCurrentGesture(); - final boolean runBackMidGesture = !shouldExecuteBackOnUp(mContext); - if (mCanPerformBack) { - if (!shouldhideBackButton(mContext)) { - mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */, - BACK_BUTTON_FADE_OUT_ALPHA); - } - if (runBackMidGesture) { - performBack(); - } - } - mHandler.removeCallbacks(mExecuteBackRunnable); - if (runBackMidGesture) { - mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT); + if (mHitTarget != null) { + mHitTarget.abortCurrentGesture(); } } } - private void endBackGesture() { - if (mBackGestureActive) { - mHandler.removeCallbacks(mExecuteBackRunnable); - mHomeAnimator = mNavigationBarView.getHomeButton().getCurrentView() - .animate() - .setDuration(BACK_BUTTON_FADE_IN_ALPHA) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - if (mIsVertical) { - mHomeAnimator.translationY(0); - } else { - mHomeAnimator.translationX(0); + private boolean canPerformAnyAction() { + for (NavigationGestureAction action: mGestureActions) { + if (action != null && action.isEnabled()) { + return true; } - mHomeAnimator.start(); - if (!shouldhideBackButton(mContext)) { - mNavigationBarView.getBackButton().setAlpha( - mOverviewEventSender.getBackButtonAlpha(), true /* animate */); - } - if (shouldExecuteBackOnUp(mContext)) { - performBack(); - } - } - } - - private void animateEnd() { - if (mTrackAnimator != null) { - mTrackAnimator.cancel(); - } - - ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f), - PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE)); - trackAnimator.setInterpolator(ALPHA_OUT); - trackAnimator.setDuration(ANIM_OUT_DURATION_MS); - ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f); - navBarAnimator.setInterpolator(ALPHA_IN); - navBarAnimator.setDuration(ANIM_IN_DURATION_MS); - mTrackAnimator = new AnimatorSet(); - mTrackAnimator.playTogether(trackAnimator, navBarAnimator); - mTrackAnimator.addListener(mQuickScrubEndListener); - mTrackAnimator.start(); - } - - private void resetQuickScrub() { - mQuickScrubActive = false; - mAllowGestureDetection = false; - if (mCurrentNavigationBarView != null) { - mCurrentNavigationBarView.setAlpha(1f); - } - mCurrentNavigationBarView = null; - updateHighlight(); - } - - private void moveHomeButton(float pos) { - if (mHomeAnimator != null) { - mHomeAnimator.cancel(); - mHomeAnimator = null; } - final View homeButton = mNavigationBarView.getHomeButton().getCurrentView(); - if (mIsVertical) { - homeButton.setTranslationY(pos); - } else { - homeButton.setTranslationX(pos); - } - } - - private void updateHighlight() { - if (mTrackRect.isEmpty()) { - return; - } - int colorBase, colorGrad; - if (mDarkIntensity > 0.5f) { - colorBase = mContext.getColor(R.color.quick_step_track_background_background_dark); - colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_dark); - } else { - colorBase = mContext.getColor(R.color.quick_step_track_background_background_light); - colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_light); - } - mHighlight = new RadialGradient(0, mTrackRect.height() / 2, - mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase, - Shader.TileMode.CLAMP); - mTrackPaint.setShader(mHighlight); - } - - private boolean canPerformHomeBackGesture() { - return swipeHomeGoBackGestureEnabled(mContext) - && mOverviewEventSender.getBackButtonAlpha() > 0; - } - - private void performBack() { - sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); - sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); - mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - } - - private void sendEvent(int action, int code) { - long when = SystemClock.uptimeMillis(); - final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, - 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, - KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, - InputDevice.SOURCE_KEYBOARD); - InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + return false; } private boolean proxyMotionEvents(MotionEvent event) { @@ -740,22 +534,15 @@ public class QuickStepController implements GestureHelper { return false; } - private static boolean getBoolGlobalSetting(Context context, String key) { - return Settings.Global.getInt(context.getContentResolver(), key, 0) != 0; + protected boolean isNavBarVertical() { + return mNavBarPosition == NAV_BAR_LEFT || mNavBarPosition == NAV_BAR_RIGHT; } - public static boolean swipeHomeGoBackGestureEnabled(Context context) { - return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED) - && getBoolGlobalSetting(context, PULL_HOME_GO_BACK_PROP); + static boolean getBoolGlobalSetting(Context context, String key) { + return Settings.Global.getInt(context.getContentResolver(), key, 0) != 0; } public static boolean shouldhideBackButton(Context context) { - return swipeHomeGoBackGestureEnabled(context) - && getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP); - } - - public static boolean shouldExecuteBackOnUp(Context context) { - return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED) - && getBoolGlobalSetting(context, BACK_AFTER_END_PROP); + return getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java new file mode 100644 index 000000000000..078160293e58 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java @@ -0,0 +1,618 @@ +/* + * Copyright (C) 2018 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.statusbar.phone; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT; + +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyFloat; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.android.systemui.R; +import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.shared.recents.IOverviewProxy; +import com.android.systemui.SysuiTestCase; + +import android.content.res.Resources; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; +import android.view.MotionEvent; +import android.view.View; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.MockitoAnnotations; + +/** atest QuickStepControllerTest */ +@RunWith(AndroidTestingRunner.class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +public class QuickStepControllerTest extends SysuiTestCase { + private QuickStepController mController; + private NavigationBarView mNavigationBarView; + private StatusBar mStatusBar; + private OverviewProxyService mProxyService; + private IOverviewProxy mProxy; + private Resources mResources; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + final ButtonDispatcher backButton = mock(ButtonDispatcher.class); + mResources = mock(Resources.class); + + mProxyService = mock(OverviewProxyService.class); + mProxy = mock(IOverviewProxy.Stub.class); + doReturn(mProxy).when(mProxyService).getProxy(); + mDependency.injectTestDependency(OverviewProxyService.class, mProxyService); + + mStatusBar = mock(StatusBar.class); + doReturn(false).when(mStatusBar).isKeyguardShowing(); + mContext.putComponent(StatusBar.class, mStatusBar); + + mNavigationBarView = mock(NavigationBarView.class); + doReturn(false).when(mNavigationBarView).inScreenPinning(); + doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed(); + doReturn(true).when(mNavigationBarView).isQuickScrubEnabled(); + doReturn(HIT_TARGET_NONE).when(mNavigationBarView).getDownHitTarget(); + doReturn(backButton).when(mNavigationBarView).getBackButton(); + doReturn(mResources).when(mNavigationBarView).getResources(); + + mController = new QuickStepController(mContext); + mController.setComponents(mNavigationBarView); + mController.setBarState(false /* isRTL */, NAV_BAR_BOTTOM); + } + + @Test + public void testNoActionsNoGestures() throws Exception { + MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1); + assertFalse(mController.onInterceptTouchEvent(ev)); + verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev); + assertNull(mController.getCurrentAction()); + } + + @Test + public void testHasActionDetectGesturesTouchdown() throws Exception { + MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1); + + // Add enabled gesture action + NavigationGestureAction action = mockAction(true); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + assertFalse(mController.onInterceptTouchEvent(ev)); + verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev); + verify(action, times(1)).reset(); + verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget()); + verify(mProxy, times(1)).onMotionEvent(ev); + assertNull(mController.getCurrentAction()); + } + + @Test + public void testProxyDisconnectedNoGestures() throws Exception { + MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1); + + // Add enabled gesture action + mController.setGestureActions(mockAction(true), null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Set the gesture on deadzone + doReturn(null).when(mProxyService).getProxy(); + + assertFalse(mController.onInterceptTouchEvent(ev)); + verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev); + assertNull(mController.getCurrentAction()); + } + + @Test + public void testNoActionsNoGesturesOverDeadzone() throws Exception { + MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1); + + // Touched over deadzone + doReturn(HIT_TARGET_DEAD_ZONE).when(mNavigationBarView).getDownHitTarget(); + + assertTrue(mController.onInterceptTouchEvent(ev)); + verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev); + assertNull(mController.getCurrentAction()); + } + + @Test + public void testOnTouchIgnoredDownEventAfterOnIntercept() { + mController.setGestureActions(mockAction(true), null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1); + assertFalse(touch(ev)); + verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev); + + // OnTouch event for down is ignored, so requestUnbufferedDispatch ran once from before + assertFalse(mNavigationBarView.onTouchEvent(ev)); + verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev); + } + + @Test + public void testGesturesCallCorrectAction() throws Exception { + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // Swipe Up + assertGestureTriggersAction(swipeUp, 1, 100, 5, 1); + // Swipe Down + assertGestureTriggersAction(swipeDown, 1, 1, 5, 100); + // Swipe Left + assertGestureTriggersAction(swipeLeft, 100, 1, 5, 1); + // Swipe Right + assertGestureTriggersAction(swipeRight, 1, 1, 100, 5); + } + + @Test + public void testGesturesCallCorrectActionLandscape() throws Exception { + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // In landscape + mController.setBarState(false /* isRTL */, NAV_BAR_RIGHT); + + // Swipe Up + assertGestureTriggersAction(swipeRight, 1, 100, 5, 1); + // Swipe Down + assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100); + // Swipe Left + assertGestureTriggersAction(swipeUp, 100, 1, 5, 1); + // Swipe Right + assertGestureTriggersAction(swipeDown, 1, 1, 100, 5); + } + + @Test + public void testGesturesCallCorrectActionSeascape() throws Exception { + mController.setBarState(false /* isRTL */, NAV_BAR_LEFT); + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // Swipe Up + assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1); + // Swipe Down + assertGestureTriggersAction(swipeRight, 1, 1, 5, 100); + // Swipe Left + assertGestureTriggersAction(swipeDown, 100, 1, 5, 1); + // Swipe Right + assertGestureTriggersAction(swipeUp, 1, 1, 100, 5); + } + + @Test + public void testGesturesCallCorrectActionRTL() throws Exception { + mController.setBarState(true /* isRTL */, NAV_BAR_BOTTOM); + + // The swipe gestures below are for LTR, so RTL in portrait will be swapped + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // Swipe Up in RTL + assertGestureTriggersAction(swipeUp, 1, 100, 5, 1); + // Swipe Down in RTL + assertGestureTriggersAction(swipeDown, 1, 1, 5, 100); + // Swipe Left in RTL + assertGestureTriggersAction(swipeRight, 100, 1, 5, 1); + // Swipe Right in RTL + assertGestureTriggersAction(swipeLeft, 1, 1, 100, 5); + } + + @Test + public void testGesturesCallCorrectActionLandscapeRTL() throws Exception { + mController.setBarState(true /* isRTL */, NAV_BAR_RIGHT); + + // The swipe gestures below are for LTR, so RTL in landscape will be swapped + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // Swipe Up + assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1); + // Swipe Down + assertGestureTriggersAction(swipeRight, 1, 1, 5, 100); + // Swipe Left + assertGestureTriggersAction(swipeUp, 100, 1, 5, 1); + // Swipe Right + assertGestureTriggersAction(swipeDown, 1, 1, 100, 5); + } + + @Test + public void testGesturesCallCorrectActionSeascapeRTL() throws Exception { + mController.setBarState(true /* isRTL */, NAV_BAR_LEFT); + + // The swipe gestures below are for LTR, so RTL in seascape will be swapped + NavigationGestureAction swipeUp = mockAction(true); + NavigationGestureAction swipeDown = mockAction(true); + NavigationGestureAction swipeLeft = mockAction(true); + NavigationGestureAction swipeRight = mockAction(true); + mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight); + + // Swipe Up + assertGestureTriggersAction(swipeRight, 1, 100, 5, 1); + // Swipe Down + assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100); + // Swipe Left + assertGestureTriggersAction(swipeDown, 100, 1, 5, 1); + // Swipe Right + assertGestureTriggersAction(swipeUp, 1, 1, 100, 5); + } + + @Test + public void testActionPreventByPinnedState() throws Exception { + // Screen is pinned + doReturn(true).when(mNavigationBarView).inScreenPinning(); + + // Add enabled gesture action + NavigationGestureAction action = mockAction(true); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Touch down to begin swipe + MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100); + assertFalse(touch(downEvent)); + verify(mProxy, never()).onPreMotionEvent(mNavigationBarView.getDownHitTarget()); + verify(mProxy, never()).onMotionEvent(downEvent); + + // Move to start gesture, but pinned so it should not trigger action + MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1); + assertFalse(touch(moveEvent)); + assertNull(mController.getCurrentAction()); + verify(mNavigationBarView, times(1)).showPinningEscapeToast(); + verify(action, never()).onGestureStart(moveEvent); + } + + @Test + public void testActionPreventedNotificationsShown() throws Exception { + NavigationGestureAction action = mockAction(true); + doReturn(false).when(action).canRunWhenNotificationsShowing(); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Show the notifications + doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed(); + + // Swipe up + assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100)); + assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1)); + assertNull(mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + + // Hide the notifications + doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed(); + + // Swipe up + assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100)); + assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1)); + assertEquals(action, mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + } + + @Test + public void testActionCannotPerform() throws Exception { + NavigationGestureAction action = mockAction(true); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Cannot perform action + doReturn(false).when(action).canPerformAction(); + + // Swipe up + assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100)); + assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1)); + assertNull(mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + + // Cannot perform action + doReturn(true).when(action).canPerformAction(); + + // Swipe up + assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100)); + assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1)); + assertEquals(action, mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + } + + @Test + public void testQuickScrub() throws Exception { + QuickScrubAction action = spy(new QuickScrubAction(mNavigationBarView, mProxyService)); + mController.setGestureActions(null /* swipeUpAction */, null /* swipeDownAction */, + null /* swipeLeftAction */, action); + int y = 20; + + // Set the layout and other padding to make sure the scrub fraction is calculated correctly + action.onLayout(true, 0, 0, 400, 100); + doReturn(0).when(mNavigationBarView).getPaddingLeft(); + doReturn(0).when(mNavigationBarView).getPaddingRight(); + doReturn(0).when(mNavigationBarView).getPaddingStart(); + doReturn(0).when(mResources) + .getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding); + + // Quickscrub disabled, so the action should be disabled + doReturn(false).when(mNavigationBarView).isQuickScrubEnabled(); + assertFalse(action.isEnabled()); + doReturn(true).when(mNavigationBarView).isQuickScrubEnabled(); + + // Touch down + MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 0, y); + assertFalse(touch(downEvent)); + assertNull(mController.getCurrentAction()); + verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget()); + verify(mProxy, times(1)).onMotionEvent(downEvent); + + // Move to start trigger action from gesture + MotionEvent moveEvent1 = event(MotionEvent.ACTION_MOVE, 100, y); + assertTrue(touch(moveEvent1)); + assertEquals(action, mController.getCurrentAction()); + verify(action, times(1)).onGestureStart(moveEvent1); + verify(mProxy, times(1)).onQuickScrubStart(); + verify(mProxyService, times(1)).notifyQuickScrubStarted(); + verify(mNavigationBarView, times(1)).updateSlippery(); + + // Move again for scrub + MotionEvent moveEvent2 = event(MotionEvent.ACTION_MOVE, 200, y); + assertTrue(touch(moveEvent2)); + assertEquals(action, mController.getCurrentAction()); + verify(action, times(1)).onGestureMove(200, y); + verify(mProxy, times(1)).onQuickScrubProgress(1f / 2); + + // Action up + MotionEvent upEvent = event(MotionEvent.ACTION_UP, 1, y); + assertFalse(touch(upEvent)); + assertNull(mController.getCurrentAction()); + verify(action, times(1)).onGestureEnd(); + verify(mProxy, times(1)).onQuickScrubEnd(); + } + + @Test + public void testQuickStep() throws Exception { + QuickStepAction action = new QuickStepAction(mNavigationBarView, mProxyService); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Notifications are up, should prevent quickstep + doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed(); + + // Swipe up + assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100)); + assertNull(mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1)); + assertNull(mController.getCurrentAction()); + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed(); + + // Quickstep disabled, so the action should be disabled + doReturn(false).when(mNavigationBarView).isQuickStepSwipeUpEnabled(); + assertFalse(action.isEnabled()); + doReturn(true).when(mNavigationBarView).isQuickStepSwipeUpEnabled(); + + // Swipe up should call proxy events + MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100); + assertFalse(touch(downEvent)); + assertNull(mController.getCurrentAction()); + verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget()); + verify(mProxy, times(1)).onMotionEvent(downEvent); + + MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1); + assertTrue(touch(moveEvent)); + assertEquals(action, mController.getCurrentAction()); + verify(mProxy, times(1)).onQuickStep(moveEvent); + verify(mProxyService, times(1)).notifyQuickStepStarted(); + } + + @Test + public void testLongPressPreventDetection() throws Exception { + NavigationGestureAction action = mockAction(true); + mController.setGestureActions(action, null /* swipeDownAction */, + null /* swipeLeftAction */, null /* swipeRightAction */); + + // Start the drag up + assertFalse(touch(MotionEvent.ACTION_DOWN, 100, 1)); + assertNull(mController.getCurrentAction()); + + // Long press something on the navigation bar such as Home button + mNavigationBarView.onNavigationButtonLongPress(mock(View.class)); + + // Swipe right will not start any gestures + MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, 1, 1); + assertFalse(touch(motionMoveEvent)); + assertNull(mController.getCurrentAction()); + verify(action, never()).startGesture(motionMoveEvent); + + // Touch up + assertFalse(touch(MotionEvent.ACTION_UP, 1, 1)); + verify(action, never()).endGesture(); + } + + @Test + public void testHitTargetDragged() throws Exception { + ButtonDispatcher button = mock(ButtonDispatcher.class); + View buttonView = spy(new View(mContext)); + doReturn(buttonView).when(button).getCurrentView(); + + NavigationGestureAction action = mockAction(true); + mController.setGestureActions(action, action, action, action); + + // Setup getting the hit target + doReturn(HIT_TARGET_HOME).when(action).requiresTouchDownHitTarget(); + doReturn(true).when(action).requiresDragWithHitTarget(); + doReturn(HIT_TARGET_HOME).when(mNavigationBarView).getDownHitTarget(); + doReturn(button).when(mNavigationBarView).getHomeButton(); + + // Portrait + assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_BOTTOM); + + // Portrait RTL + assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_BOTTOM); + + // Landscape + assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_RIGHT); + + // Landscape RTL + assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_RIGHT); + + // Seascape + assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_LEFT); + + // Seascape RTL + assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_LEFT); + } + + private void assertGestureDragsHitTargetAllDirections(View buttonView, boolean isRTL, + int navPos) { + mController.setBarState(isRTL, navPos); + + // Swipe up + assertGestureDragsHitTarget(buttonView, 10 /* x1 */, 200 /* y1 */, 0 /* x2 */, 0 /* y2 */, + 0 /* dx */, -1 /* dy */); + // Swipe left + assertGestureDragsHitTarget(buttonView, 200 /* x1 */, 10 /* y1 */, 0 /* x2 */, 0 /* y2 */, + -1 /* dx */, 0 /* dy */); + // Swipe right + assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 200 /* x2 */, 10 /* y2 */, + 1 /* dx */, 0 /* dy */); + // Swipe down + assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 10 /* x2 */, 200 /* y2 */, + 0 /* dx */, 1 /* dy */); + } + + /** + * Asserts the gesture actually moves the hit target + * @param buttonView button to check if moved, use Mockito.spy on a real object + * @param x1 start x + * @param x2 start y + * @param y1 end x + * @param y2 end y + * @param dx diff in x, if not 0, its sign determines direction, value does not matter + * @param dy diff in y, if not 0, its sign determines direction, value does not matter + */ + private void assertGestureDragsHitTarget(View buttonView, int x1, int y1, int x2, int y2, + int dx, int dy) { + ArgumentCaptor<Float> captor = ArgumentCaptor.forClass(Float.class); + assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1)); + assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2)); + + // Verify positions of the button drag + if (dx == 0) { + verify(buttonView, never()).setTranslationX(anyFloat()); + } else { + verify(buttonView).setTranslationX(captor.capture()); + if (dx < 0) { + assertTrue("Button should have moved left", (float) captor.getValue() < 0); + } else { + assertTrue("Button should have moved right", (float) captor.getValue() > 0); + } + } + if (dy == 0) { + verify(buttonView, never()).setTranslationY(anyFloat()); + } else { + verify(buttonView).setTranslationY(captor.capture()); + if (dy < 0) { + assertTrue("Button should have moved up", (float) captor.getValue() < 0); + } else { + assertTrue("Button should have moved down", (float) captor.getValue() > 0); + } + } + + // Touch up + assertFalse(touch(MotionEvent.ACTION_UP, x2, y2)); + verify(buttonView, times(1)).animate(); + + // Reset button state + reset(buttonView); + } + + + private MotionEvent event(int action, float x, float y) { + final MotionEvent event = mock(MotionEvent.class); + doReturn(x).when(event).getX(); + doReturn(y).when(event).getY(); + doReturn(action & MotionEvent.ACTION_MASK).when(event).getActionMasked(); + doReturn(action).when(event).getAction(); + return event; + } + + private boolean touch(int action, float x, float y) { + return touch(event(action, x, y)); + } + + private boolean touch(MotionEvent event) { + return mController.onInterceptTouchEvent(event); + } + + private NavigationGestureAction mockAction(boolean enabled) { + final NavigationGestureAction action = mock(NavigationGestureAction.class); + doReturn(enabled).when(action).isEnabled(); + doReturn(HIT_TARGET_NONE).when(action).requiresTouchDownHitTarget(); + doReturn(true).when(action).canPerformAction(); + return action; + } + + private void assertGestureTriggersAction(NavigationGestureAction action, int x1, int y1, + int x2, int y2) { + // Start the drag + assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1)); + assertNull(mController.getCurrentAction()); + + // Swipe + MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, x2, y2); + assertTrue(touch(motionMoveEvent)); + assertEquals(action, mController.getCurrentAction()); + verify(action, times(1)).startGesture(motionMoveEvent); + + // Move again + assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2)); + verify(action, times(1)).onGestureMove(x2, y2); + + // Touch up + assertFalse(touch(MotionEvent.ACTION_UP, x2, y2)); + assertNull(mController.getCurrentAction()); + verify(action, times(1)).endGesture(); + } +} |