From 26d74b9ec0771bcb978bb2a6efbf1a0bc67721e5 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Mon, 16 Sep 2024 08:37:05 -0700 Subject: [PATCH] Have bubble bar unstash gesture track finger Follow the taskbar motion for unstashing via gesture. When swiping up in the handle area, move the handle slightly up with the finger movement. Once reaching a certain threshold, unstash the bubble bar. If user swipes up past a certain other threshold, and lifts the finger, expand the bubble bar. Otherwise leave bubble bar in collapsed state. Only stashed handle or collapsed bar can be swiped up on. Bug: 325673340 Test: atest NexusLauncherTests:com.android.launcher3.taskbar.bubbles.BubbleBarSwipeControllerTest Test: swipe up on stashed handle Test: tap on stashed handle Test: enable 3 button nav and tap on bubble bar Flag: com.android.wm.shell.enable_bubble_bar Change-Id: I6bb3c201cd03f05e2be55ebb0c972c577373ea79 --- .../taskbar/TaskbarActivityContext.java | 4 + .../bubbles/BubbleBarSwipeController.kt | 203 +++++++++++ .../taskbar/bubbles/BubbleControllers.java | 4 + .../taskbar/bubbles/BubbleCreator.java | 5 +- .../BubbleBarInputConsumer.java | 33 +- .../bubbles/BubbleBarInputConsumerTest.kt | 4 +- .../bubbles/BubbleBarSwipeControllerTest.kt | 327 ++++++++++++++++++ 7 files changed, 564 insertions(+), 16 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index a1cd7f7f0b..5f8baeda75 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -107,6 +107,7 @@ import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCall import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; import com.android.launcher3.taskbar.bubbles.BubbleBarView; import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; @@ -278,9 +279,11 @@ public class TaskbarActivityContext extends BaseTaskbarContext { BubbleBarController.onTaskbarRecreated(); if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) { Optional bubbleHandleController = Optional.empty(); + Optional bubbleBarSwipeController = Optional.empty(); if (isTransientTaskbar) { bubbleHandleController = Optional.of( new BubbleStashedHandleViewController(this, bubbleHandleView)); + bubbleBarSwipeController = Optional.of(new BubbleBarSwipeController(this)); } TaskbarHotseatDimensionsProvider dimensionsProvider = new DeviceProfileDimensionsProviderAdapter(this); @@ -298,6 +301,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { () -> DisplayController.INSTANCE.get(this).getInfo().currentSize), new BubblePinController(this, mDragLayer, () -> DisplayController.INSTANCE.get(this).getInfo().currentSize), + bubbleBarSwipeController, new BubbleCreator(this) )); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt new file mode 100644 index 0000000000..a831fd7282 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.animation.ValueAnimator +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.anim.SpringAnimationBuilder +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarThresholdUtils +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.touch.OverScroll + +/** Handle swipe events on the bubble bar and handle */ +class BubbleBarSwipeController { + + private val context: Context + + private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null + private var bubbleBarViewController: BubbleBarViewController? = null + private var bubbleStashController: BubbleStashController? = null + + private var springAnimation: ValueAnimator? = null + private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate) + + private val unstashThreshold: Int + private val expandThreshold: Int + private val maxOverscroll: Int + + private var swipeState: SwipeState = SwipeState() + + constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac)) + + @VisibleForTesting + constructor(context: Context, dimensionProvider: DimensionProvider) { + this.context = context + unstashThreshold = dimensionProvider.unstashThreshold + expandThreshold = dimensionProvider.expandThreshold + maxOverscroll = dimensionProvider.maxOverscroll + } + + fun init(bubbleControllers: BubbleControllers) { + bubbleStashedHandleViewController = + bubbleControllers.bubbleStashedHandleViewController.orElse(null) + bubbleBarViewController = bubbleControllers.bubbleBarViewController + bubbleStashController = bubbleControllers.bubbleStashController + } + + /** Start tracking a new swipe gesture */ + fun start() { + if (springAnimation != null) reset() + val stashed = bubbleStashController?.isStashed ?: false + val barVisible = bubbleStashController?.isBubbleBarVisible() ?: false + val expanded = bubbleBarViewController?.isExpanded ?: false + + swipeState = + SwipeState( + stashedOnStart = stashed, + collapsedOnStart = !stashed && barVisible && !expanded, + expandedOnStart = expanded, + ) + } + + /** Update swipe distance to [dy] */ + fun swipeTo(dy: Float) { + // Only handle swipe up and stashed or collapsed bar + if (dy > 0 || swipeState.expandedOnStart) return + + animatedSwipeTranslation.updateValue(dy) + + val prevState = swipeState + // We can pass unstash threshold once per gesture, keep it true if it happened once + val passedUnstashThreshold = isUnstash(dy) || prevState.passedUnstashThreshold + // Expand happens at the end of the gesture, always keep the current value + val passedExpandThreshold = isExpand(dy) + + if ( + passedUnstashThreshold != prevState.passedUnstashThreshold || + passedExpandThreshold != prevState.passedExpandThreshold + ) { + swipeState = + swipeState.copy( + passedUnstashThreshold = passedUnstashThreshold, + passedExpandThreshold = passedExpandThreshold, + ) + } + + if ( + swipeState.stashedOnStart && + swipeState.passedUnstashThreshold && + !prevState.passedUnstashThreshold + ) { + bubbleStashController?.showBubbleBar(expandBubbles = false) + } + } + + /** Finish tracking swipe gesture. Animate views back to resting state */ + fun finish() { + if (swipeState.passedExpandThreshold) { + bubbleStashController?.showBubbleBar(expandBubbles = true) + } + springToRest() + } + + /** Returns `true` if we are tracking a swipe gesture */ + fun isSwipeGesture(): Boolean { + return swipeState.passedUnstashThreshold || swipeState.passedExpandThreshold + } + + private fun isUnstash(dy: Float): Boolean { + return dy < -unstashThreshold + } + + private fun isExpand(dy: Float): Boolean { + return dy < -expandThreshold + } + + private fun reset() { + springAnimation?.let { + if (it.isRunning) { + it.removeAllListeners() + it.cancel() + animatedSwipeTranslation.updateValue(0f) + } + } + springAnimation = null + swipeState = SwipeState() + } + + private fun onSwipeUpdate(value: Float) { + val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat() + bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe) + bubbleBarViewController?.setTranslationYForSwipe(dampedSwipe) + } + + private fun springToRest() { + springAnimation = + SpringAnimationBuilder(context) + .setStartValue(animatedSwipeTranslation.value) + .setEndValue(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + .build(animatedSwipeTranslation, AnimatedFloat.VALUE) + .also { it.doOnEnd { reset() } } + springAnimation?.start() + } + + internal data class SwipeState( + val stashedOnStart: Boolean = false, + val collapsedOnStart: Boolean = false, + val expandedOnStart: Boolean = false, + val passedUnstashThreshold: Boolean = false, + val passedExpandThreshold: Boolean = false, + ) + + /** Allows overriding the dimension provider for testing */ + @VisibleForTesting + interface DimensionProvider { + val unstashThreshold: Int + val expandThreshold: Int + val maxOverscroll: Int + } + + private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) : + DimensionProvider { + override val unstashThreshold: Int + override val expandThreshold: Int + override val maxOverscroll: Int + + init { + val resources = taskbarActivityContext.resources + unstashThreshold = + TaskbarThresholdUtils.getFromNavThreshold( + resources, + taskbarActivityContext.deviceProfile, + ) + // TODO(325673340): review threshold with ux + expandThreshold = + TaskbarThresholdUtils.getAppWindowThreshold( + resources, + taskbarActivityContext.deviceProfile, + ) + maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index e00916af6c..ea83842c6b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -40,6 +40,7 @@ public class BubbleControllers { public final BubbleDismissController bubbleDismissController; public final BubbleBarPinController bubbleBarPinController; public final BubblePinController bubblePinController; + public final Optional bubbleBarSwipeController; public final BubbleCreator bubbleCreator; private final RunnableList mPostInitRunnables = new RunnableList(); @@ -58,6 +59,7 @@ public class BubbleControllers { BubbleDismissController bubbleDismissController, BubbleBarPinController bubbleBarPinController, BubblePinController bubblePinController, + Optional bubbleBarSwipeController, BubbleCreator bubbleCreator) { this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; @@ -67,6 +69,7 @@ public class BubbleControllers { this.bubbleDismissController = bubbleDismissController; this.bubbleBarPinController = bubbleBarPinController; this.bubblePinController = bubblePinController; + this.bubbleBarSwipeController = bubbleBarSwipeController; this.bubbleCreator = bubbleCreator; } @@ -104,6 +107,7 @@ public class BubbleControllers { bubbleDismissController.init(/* bubbleControllers = */ this); bubbleBarPinController.init(this); bubblePinController.init(this); + bubbleBarSwipeController.ifPresent(c -> c.init(this)); mPostInitRunnables.executeAllAndDestroy(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java index 12b1487552..340a120e33 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java @@ -44,8 +44,6 @@ import android.util.PathParser; import android.view.LayoutInflater; import android.view.ViewGroup; -import androidx.appcompat.content.res.AppCompatResources; - import com.android.internal.graphics.ColorUtils; import com.android.launcher3.R; import com.android.launcher3.icons.BitmapInfo; @@ -196,8 +194,7 @@ public class BubbleCreator { } private Bitmap createOverflowBitmap() { - Drawable iconDrawable = AppCompatResources.getDrawable(mContext, - R.drawable.bubble_ic_overflow_button); + Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button); final TypedArray ta = mContext.obtainStyledAttributes( new int[]{ diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java index 92031c5429..778c231056 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java @@ -23,10 +23,12 @@ import android.graphics.PointF; import android.view.MotionEvent; import android.view.ViewConfiguration; +import androidx.annotation.Nullable; + import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController; import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; -import com.android.launcher3.taskbar.bubbles.BubbleDragController; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; @@ -40,10 +42,11 @@ public class BubbleBarInputConsumer implements InputConsumer { private final BubbleStashController mBubbleStashController; private final BubbleBarViewController mBubbleBarViewController; - private final BubbleDragController mBubbleDragController; + @Nullable + private final BubbleBarSwipeController mBubbleBarSwipeController; private final InputMonitorCompat mInputMonitorCompat; - private boolean mSwipeUpOnBubbleHandle; + private boolean mPilfered; private boolean mPassedTouchSlop; private boolean mStashedOrCollapsedOnDown; @@ -57,7 +60,8 @@ public class BubbleBarInputConsumer implements InputConsumer { InputMonitorCompat inputMonitorCompat) { mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarViewController = bubbleControllers.bubbleBarViewController; - mBubbleDragController = bubbleControllers.bubbleDragController; + mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null); + mInputMonitorCompat = inputMonitorCompat; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mTimeForTap = ViewConfiguration.getTapTimeout(); @@ -77,6 +81,9 @@ public class BubbleBarInputConsumer implements InputConsumer { mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed(); + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.start(); + } break; case MotionEvent.ACTION_MOVE: int pointerIndex = ev.findPointerIndex(mActivePointerId); @@ -90,11 +97,10 @@ public class BubbleBarInputConsumer implements InputConsumer { if (!mPassedTouchSlop) { mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop; } - if (mStashedOrCollapsedOnDown && !mSwipeUpOnBubbleHandle && mPassedTouchSlop) { - boolean verticalGesture = Math.abs(dY) > Math.abs(dX); - if (verticalGesture && !mBubbleDragController.isDragging()) { - mSwipeUpOnBubbleHandle = true; - mBubbleStashController.showBubbleBar(/* expandBubbles= */ true); + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.swipeTo(dY); + if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) { + mPilfered = true; // Bubbles is handling the swipe so make sure no one else gets it. TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers"); mInputMonitorCompat.pilferPointers(); @@ -102,8 +108,10 @@ public class BubbleBarInputConsumer implements InputConsumer { } break; case MotionEvent.ACTION_UP: + boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null + && mBubbleBarSwipeController.isSwipeGesture(); boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap; - if (isWithinTapTime && !mSwipeUpOnBubbleHandle && !mPassedTouchSlop + if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop && mStashedOrCollapsedOnDown) { // Taps on the handle / collapsed state should open the bar mBubbleStashController.showBubbleBar(/* expandBubbles= */ true); @@ -116,8 +124,11 @@ public class BubbleBarInputConsumer implements InputConsumer { } private void cleanupAfterMotionEvent() { + if (mBubbleBarSwipeController != null) { + mBubbleBarSwipeController.finish(); + } mPassedTouchSlop = false; - mSwipeUpOnBubbleHandle = false; + mPilfered = false; } private boolean isCollapsed() { diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt index 785ec66420..c8f50f70d5 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt @@ -49,6 +49,7 @@ class BubbleBarInputConsumerTest { @Mock private lateinit var bubbleDismissController: BubbleDismissController @Mock private lateinit var bubbleBarPinController: BubbleBarPinController @Mock private lateinit var bubblePinController: BubblePinController + @Mock private lateinit var bubbleBarSwipeController: BubbleBarSwipeController @Mock private lateinit var bubbleCreator: BubbleCreator @Mock private lateinit var motionEvent: MotionEvent @@ -67,7 +68,8 @@ class BubbleBarInputConsumerTest { bubbleDismissController, bubbleBarPinController, bubblePinController, - bubbleCreator + Optional.of(bubbleBarSwipeController), + bubbleCreator, ) } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt new file mode 100644 index 0000000000..97847befaf --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.animation.AnimatorTestRule +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController +import com.android.launcher3.touch.OverScroll +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class BubbleBarSwipeControllerTest { + + companion object { + const val UNSTASH_THRESHOLD = 100 + const val EXPAND_THRESHOLD = 200 + const val MAX_OVERSCROLL = 300 + + const val UP_BELOW_UNSTASH = -UNSTASH_THRESHOLD + 10f + const val UP_ABOVE_UNSTASH = -UNSTASH_THRESHOLD - 10f + const val UP_ABOVE_EXPAND = -EXPAND_THRESHOLD - 10f + const val DOWN_BELOW_UNSTASH = UNSTASH_THRESHOLD + 10f + } + + private val context = ApplicationProvider.getApplicationContext() + + @get:Rule(order = 0) val mockitoRule: MockitoRule = MockitoJUnit.rule() + @get:Rule(order = 1) val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + + private lateinit var bubbleBarSwipeController: BubbleBarSwipeController + + @Mock private lateinit var bubbleBarController: BubbleBarController + @Mock private lateinit var bubbleBarViewController: BubbleBarViewController + @Mock private lateinit var bubbleStashController: BubbleStashController + @Mock private lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController + @Mock private lateinit var bubbleDragController: BubbleDragController + @Mock private lateinit var bubbleDismissController: BubbleDismissController + @Mock private lateinit var bubbleBarPinController: BubbleBarPinController + @Mock private lateinit var bubblePinController: BubblePinController + @Mock private lateinit var bubbleCreator: BubbleCreator + + @Before + fun setUp() { + val dimensionProvider = + object : BubbleBarSwipeController.DimensionProvider { + override val unstashThreshold: Int + get() = UNSTASH_THRESHOLD + + override val expandThreshold: Int + get() = EXPAND_THRESHOLD + + override val maxOverscroll: Int + get() = MAX_OVERSCROLL + } + bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider) + + val bubbleControllers = + BubbleControllers( + bubbleBarController, + bubbleBarViewController, + bubbleStashController, + Optional.of(bubbleStashedHandleViewController), + bubbleDragController, + bubbleDismissController, + bubbleBarPinController, + bubblePinController, + Optional.of(bubbleBarSwipeController), + bubbleCreator, + ) + + bubbleBarSwipeController.init(bubbleControllers) + } + + private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) { + val dampedTranslation = -OverScroll.dampedScroll(-swipe, MAX_OVERSCROLL).toFloat() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + } + verify(bubbleStashedHandleViewController).setTranslationYForSwipe(dampedTranslation) + verify(bubbleBarViewController).setTranslationYForSwipe(dampedTranslation) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveExpandThreshold_viewsHaveDampedTranslation() { + setUpStashedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() { + setUpCollapsedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveExpandThreshold_viewsHaveDampedTranslation() { + setUpCollapsedBar() + testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_EXPAND) + } + + private fun testViewsTranslationResetOnFinish(swipe: Float) { + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(swipe) + bubbleBarSwipeController.finish() + // We use a spring animation. Advance by 5 seconds to give it time to finish + animatorTestRule.advanceTimeBy(5000) + } + val handleSwipeTranslation = argumentCaptor() + val barSwipeTranslation = argumentCaptor() + verify(bubbleStashedHandleViewController, atLeastOnce()) + .setTranslationYForSwipe(handleSwipeTranslation.capture()) + verify(bubbleBarViewController, atLeastOnce()) + .setTranslationYForSwipe(barSwipeTranslation.capture()) + + assertThat(handleSwipeTranslation.firstValue).isNonZero() + assertThat(handleSwipeTranslation.lastValue).isZero() + + assertThat(barSwipeTranslation.firstValue).isNonZero() + assertThat(barSwipeTranslation.lastValue).isZero() + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_BELOW_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_stashedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() { + setUpStashedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND) + } + + @Test + fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() { + setUpCollapsedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH) + } + + @Test + fun swipeUp_collapsedBar_aboveExpandThreshold_animateTranslationToZeroOnFinish() { + setUpCollapsedBar() + testViewsTranslationResetOnFinish(UP_ABOVE_EXPAND) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeUp_stashedBar_belowUnstashThreshold_isSwipeGestureFalse() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse() + } + + @Test + fun swipeUp_stashedBar_aboveUnstashThreshold_unstashBubbleBar() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false) + } + + @Test + fun swipeUp_stashedBar_overUnstashThreshold_isSwipeGestureTrue() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_stashedBar_overUnstashThresholdMultipleTimes_unstashBubbleBarOnce() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH) + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false) + } + + @Test + fun swipeUp_stashedBar_overExpandThreshold_doesNotExpandBeforeFinish() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false) + getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() } + verify(bubbleStashController).showBubbleBar(expandBubbles = true) + } + + @Test + fun swipeUp_stashedBar_overExpandThreshold_isSwipeGestureTrue() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND) + } + assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue() + } + + @Test + fun swipeUp_stashedBar_overExpandThresholdAndBackDown_doesNotExpandAfterFinish() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND) + bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) + } + verify(bubbleStashController).showBubbleBar(expandBubbles = false) + getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() } + verify(bubbleStashController).showBubbleBar(expandBubbles = false) + } + + @Test + fun swipeUp_expandedBar_swipeIgnored() { + setUpExpandedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(UP_ABOVE_EXPAND) + bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH) + bubbleBarSwipeController.finish() + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + @Test + fun swipeDown_stashedBar_swipeIgnored() { + setUpStashedBar() + getInstrumentation().runOnMainSync { + bubbleBarSwipeController.start() + bubbleBarSwipeController.swipeTo(DOWN_BELOW_UNSTASH) + } + verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleBarViewController, never()).setTranslationYForSwipe(any()) + verify(bubbleStashController, never()).showBubbleBar(any()) + } + + private fun setUpStashedBar() { + whenever(bubbleStashController.isStashed).thenReturn(true) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpCollapsedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(false) + } + + private fun setUpExpandedBar() { + whenever(bubbleStashController.isStashed).thenReturn(false) + whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true) + whenever(bubbleBarViewController.isExpanded).thenReturn(true) + } +}