diff options
13 files changed, 351 insertions, 3 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index f0416e595eba..b2d9e74d5b3f 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -341,6 +341,10 @@ object Flags { // TODO(b/263512203): Tracking Bug val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) + // TODO(b/265813373): Tracking Bug + val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE = + unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true) + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt index 8a565fa86b35..60504e43465a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt @@ -30,4 +30,8 @@ class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) /** Check whether the flag for the receiver success state is enabled. */ fun isMediaTttReceiverSuccessRippleEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE) + + /** True if the media transfer chip can be dismissed via a gesture. */ + fun isMediaTttDismissGestureEnabled(): Boolean = + featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE) } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index be93c54b498f..902a10a0cea9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -185,6 +185,7 @@ constructor( } }, vibrationEffect = chipStateSender.transferStatus.vibrationEffect, + allowSwipeToDismiss = true, windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER, wakeReason = MediaTttUtils.WAKE_REASON_SENDER, timeoutMs = chipStateSender.timeout, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index cf722ce8ecfa..df8d16142b8b 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -31,6 +31,7 @@ import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT import androidx.annotation.CallSuper +import androidx.annotation.VisibleForTesting import com.android.systemui.CoreStartable import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Main @@ -108,9 +109,10 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora * Whenever the current view disappears, the next-priority view will be displayed if it's still * valid. */ + @VisibleForTesting internal val activeViews: MutableList<DisplayInfo> = mutableListOf() - private fun getCurrentDisplayInfo(): DisplayInfo? { + internal fun getCurrentDisplayInfo(): DisplayInfo? { return activeViews.getOrNull(0) } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 04b1a5016989..9e0bbb7624bd 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -80,6 +80,7 @@ constructor( powerManager: PowerManager, private val falsingManager: FalsingManager, private val falsingCollector: FalsingCollector, + private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?, private val viewUtil: ViewUtil, private val vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, @@ -105,6 +106,8 @@ constructor( commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) { + updateGestureListening() + logger.logViewUpdate( newInfo.windowTitle, newInfo.text.loadText(context), @@ -228,6 +231,42 @@ constructor( includeMargins = true, onAnimationEnd, ) + + updateGestureListening() + } + + private fun updateGestureListening() { + if (swipeChipbarAwayGestureHandler == null) { + return + } + + val currentDisplayInfo = getCurrentDisplayInfo() + if (currentDisplayInfo != null && currentDisplayInfo.info.allowSwipeToDismiss) { + swipeChipbarAwayGestureHandler.setViewFetcher { currentDisplayInfo.view } + swipeChipbarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { + onSwipeUpGestureDetected() + } + } else { + swipeChipbarAwayGestureHandler.resetViewFetcher() + swipeChipbarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) + } + } + + private fun onSwipeUpGestureDetected() { + val currentDisplayInfo = getCurrentDisplayInfo() + if (currentDisplayInfo == null) { + logger.logSwipeGestureError(id = null, errorMsg = "No info is being displayed") + return + } + if (!currentDisplayInfo.info.allowSwipeToDismiss) { + logger.logSwipeGestureError( + id = currentDisplayInfo.info.id, + errorMsg = "This view prohibits swipe-to-dismiss", + ) + return + } + removeView(currentDisplayInfo.info.id, SWIPE_UP_GESTURE_REASON) + updateGestureListening() } private fun ViewGroup.getInnerView(): ViewGroup { @@ -250,3 +289,5 @@ constructor( private const val ANIMATION_IN_DURATION = 500L private const val ANIMATION_OUT_DURATION = 250L @IdRes private val INFO_TAG = R.id.tag_chipbar_info +private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED" +private const val TAG = "ChipbarCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt index dd4bd26e3bcd..fe46318daa30 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -33,12 +33,14 @@ import com.android.systemui.temporarydisplay.ViewPriority * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR * locales; on the left in RTL locales). * @property vibrationEffect an optional vibration effect when the chipbar is displayed + * @property allowSwipeToDismiss true if users are allowed to swipe up to dismiss this chipbar. */ data class ChipbarInfo( val startIcon: TintedIcon, val text: Text, val endItem: ChipbarEndItem?, val vibrationEffect: VibrationEffect? = null, + val allowSwipeToDismiss: Boolean = false, override val windowTitle: String, override val wakeReason: String, override val timeoutMs: Int, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt index fcfbe0aeedf6..f23942847e68 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt @@ -46,4 +46,16 @@ constructor( { "Chipbar updated. window=$str1 text=$str2 endItem=$str3" } ) } + + fun logSwipeGestureError(id: String?, errorMsg: String) { + buffer.log( + tag, + LogLevel.WARNING, + { + str1 = id + str2 = errorMsg + }, + { "Chipbar swipe gesture detected for incorrect state. id=$str1 error=$str2" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt new file mode 100644 index 000000000000..6e3cb4823afa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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.temporarydisplay.chipbar + +import android.content.Context +import android.view.MotionEvent +import android.view.View +import com.android.systemui.statusbar.gesture.SwipeUpGestureHandler +import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger +import com.android.systemui.util.boundsOnScreen + +/** + * A class to detect when a user has swiped the chipbar away. + * + * Effectively [SysUISingleton]. But, this shouldn't be created if the gesture isn't enabled. See + * [TemporaryDisplayModule.provideSwipeChipbarAwayGestureHandler]. + */ +class SwipeChipbarAwayGestureHandler( + context: Context, + logger: SwipeUpGestureLogger, +) : SwipeUpGestureHandler(context, logger, loggerTag = LOGGER_TAG) { + + private var viewFetcher: () -> View? = { null } + + override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean { + val view = viewFetcher.invoke() ?: return false + // Since chipbar is in its own window, we need to use [boundsOnScreen] to get an accurate + // bottom. ([view.bottom] would be relative to its window, which would be too small.) + val viewBottom = view.boundsOnScreen.bottom + // Allow the gesture to start a bit below the chipbar + return ev.y <= 1.5 * viewBottom + } + + /** + * Sets a fetcher that returns the current chipbar view. The fetcher will be invoked whenever a + * gesture starts to determine if the gesture is near the chipbar. + */ + fun setViewFetcher(fetcher: () -> View?) { + viewFetcher = fetcher + } + + /** Removes the current view fetcher. */ + fun resetViewFetcher() { + viewFetcher = { null } + } +} + +private const val LOGGER_TAG = "SwipeChipbarAway" diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt index cf0a1835c8e8..933c0604a3b9 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt @@ -16,22 +16,38 @@ package com.android.systemui.temporarydisplay.dagger +import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBufferFactory +import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger +import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler import dagger.Module import dagger.Provides @Module interface TemporaryDisplayModule { - @Module companion object { - @JvmStatic @Provides @SysUISingleton @ChipbarLog fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("ChipbarLog", 40) } + + @Provides + @SysUISingleton + fun provideSwipeChipbarAwayGestureHandler( + mediaTttFlags: MediaTttFlags, + context: Context, + logger: SwipeUpGestureLogger, + ): SwipeChipbarAwayGestureHandler? { + return if (mediaTttFlags.isMediaTttDismissGestureEnabled()) { + SwipeChipbarAwayGestureHandler(context, logger) + } else { + null + } + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 1cdce997b19b..54d4460af6e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -50,6 +50,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -98,6 +99,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var vibratorHelper: VibratorHelper + @Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLock: WakeLockFake private lateinit var chipbarCoordinator: ChipbarCoordinator @@ -148,6 +150,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { powerManager, falsingManager, falsingCollector, + swipeHandler, viewUtil, vibratorHelper, fakeWakeLockBuilder, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 90178c6a0096..45eb1f9ec431 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -20,6 +20,7 @@ import android.os.PowerManager import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -43,6 +44,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil @@ -54,6 +57,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -74,6 +78,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var vibratorHelper: VibratorHelper + @Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLock: WakeLockFake private lateinit var fakeClock: FakeSystemClock @@ -106,6 +111,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { powerManager, falsingManager, falsingCollector, + swipeGestureHandler, viewUtil, vibratorHelper, fakeWakeLockBuilder, @@ -430,17 +436,101 @@ class ChipbarCoordinatorTest : SysuiTestCase() { verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any()) } + @Test + fun swipeToDismiss_false_neverListensForGesture() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription = null), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + allowSwipeToDismiss = false, + ) + ) + + verify(swipeGestureHandler, never()).addOnGestureDetectedCallback(any(), any()) + } + + @Test + fun swipeToDismiss_true_listensForGesture() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription = null), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + allowSwipeToDismiss = true, + ) + ) + + verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), any()) + } + + @Test + fun swipeToDismiss_swipeOccurs_viewDismissed() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription = null), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + allowSwipeToDismiss = true, + ) + ) + val view = getChipbarView() + + val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>() + verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor)) + + callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0)) + + verify(windowManager).removeView(view) + } + + @Test + fun swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissed() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription = null), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + allowSwipeToDismiss = true, + ) + ) + val view = getChipbarView() + val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>() + verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor)) + + // WHEN the view is updated to not allow swipe-to-dismiss + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription = null), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + allowSwipeToDismiss = false, + ) + ) + + // THEN the callback is removed + verify(swipeGestureHandler).removeOnGestureDetectedCallback(any()) + + // And WHEN the old callback is invoked + callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0)) + + // THEN it is ignored and view isn't removed + verify(windowManager, never()).removeView(view) + } + private fun createChipbarInfo( startIcon: Icon, text: Text, endItem: ChipbarEndItem?, vibrationEffect: VibrationEffect? = null, + allowSwipeToDismiss: Boolean = false, ): ChipbarInfo { return ChipbarInfo( TintedIcon(startIcon, tintAttr = null), text, endItem, vibrationEffect, + allowSwipeToDismiss, windowTitle = WINDOW_TITLE, wakeReason = WAKE_REASON, timeoutMs = TIMEOUT, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 4ef4e6ca6540..ffac8f6aabe6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -43,6 +43,7 @@ class FakeChipbarCoordinator( powerManager: PowerManager, falsingManager: FalsingManager, falsingCollector: FalsingCollector, + swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler, viewUtil: ViewUtil, vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, @@ -59,6 +60,7 @@ class FakeChipbarCoordinator( powerManager, falsingManager, falsingCollector, + swipeChipbarAwayGestureHandler, viewUtil, vibratorHelper, wakeLockBuilder, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt new file mode 100644 index 000000000000..a87a95060a7e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 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.temporarydisplay.chipbar + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +@SmallTest +class SwipeChipbarAwayGestureHandlerTest : SysuiTestCase() { + + private lateinit var underTest: SwipeChipbarAwayGestureHandler + + @Before + fun setUp() { + underTest = SwipeChipbarAwayGestureHandler(context, mock()) + } + + @Test + fun startOfGestureIsWithinBounds_noViewFetcher_returnsFalse() { + assertThat(underTest.startOfGestureIsWithinBounds(createMotionEvent())).isFalse() + } + + @Test + fun startOfGestureIsWithinBounds_usesViewFetcher_aboveBottom_returnsTrue() { + val view = createMockView() + + underTest.setViewFetcher { view } + + val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f) + assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue() + } + + @Test + fun startOfGestureIsWithinBounds_usesViewFetcher_slightlyBelowBottom_returnsTrue() { + val view = createMockView() + + underTest.setViewFetcher { view } + + val motionEvent = createMotionEvent(y = VIEW_BOTTOM + 20f) + assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue() + } + + @Test + fun startOfGestureIsWithinBounds_usesViewFetcher_tooFarDown_returnsFalse() { + val view = createMockView() + + underTest.setViewFetcher { view } + + val motionEvent = createMotionEvent(y = VIEW_BOTTOM * 4f) + assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse() + } + + @Test + fun startOfGestureIsWithinBounds_viewFetcherReset_returnsFalse() { + val view = createMockView() + + underTest.setViewFetcher { view } + + val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f) + assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue() + + underTest.resetViewFetcher() + assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse() + } + + private fun createMotionEvent(y: Float = 0f): MotionEvent { + return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, y, 0) + } + + private fun createMockView(): View { + return mock<View>().also { + doAnswer { invocation -> + val out: Rect = invocation.getArgument(0) + out.set(0, 0, 0, VIEW_BOTTOM) + null + } + .whenever(it) + .getBoundsOnScreen(any()) + } + } + + private companion object { + const val VIEW_BOTTOM = 455 + } +} |