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) + } +}