1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.navigationbar.gestural 18 19 import android.os.Handler 20 import android.testing.TestableLooper 21 import android.view.HapticFeedbackConstants 22 import android.view.MotionEvent 23 import android.view.MotionEvent.ACTION_DOWN 24 import android.view.MotionEvent.ACTION_MOVE 25 import android.view.MotionEvent.ACTION_UP 26 import android.view.ViewConfiguration 27 import android.view.WindowManager 28 import androidx.test.ext.junit.runners.AndroidJUnit4 29 import androidx.test.filters.SmallTest 30 import com.android.internal.jank.Cuj 31 import com.android.internal.util.LatencyTracker 32 import com.android.systemui.SysuiTestCase 33 import com.android.systemui.jank.interactionJankMonitor 34 import com.android.systemui.plugins.NavigationEdgeBackPlugin 35 import com.android.systemui.statusbar.VibratorHelper 36 import com.android.systemui.statusbar.policy.ConfigurationController 37 import com.android.systemui.testKosmos 38 import com.android.systemui.util.time.FakeSystemClock 39 import com.google.common.truth.Truth.assertThat 40 import org.junit.Before 41 import org.junit.Test 42 import org.junit.runner.RunWith 43 import org.mockito.ArgumentMatchers.any 44 import org.mockito.ArgumentMatchers.eq 45 import org.mockito.Mock 46 import org.mockito.Mockito.clearInvocations 47 import org.mockito.Mockito.never 48 import org.mockito.Mockito.verify 49 import org.mockito.MockitoAnnotations 50 51 @SmallTest 52 @RunWith(AndroidJUnit4::class) 53 @TestableLooper.RunWithLooper(setAsMainLooper = true) 54 class BackPanelControllerTest : SysuiTestCase() { 55 companion object { 56 private const val START_X: Float = 0f 57 } 58 59 private val kosmos = testKosmos() 60 private lateinit var mBackPanelController: BackPanelController 61 private lateinit var systemClock: FakeSystemClock 62 private lateinit var testableLooper: TestableLooper 63 private var triggerThreshold: Float = 0.0f 64 private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop 65 @Mock private lateinit var vibratorHelper: VibratorHelper 66 @Mock private lateinit var windowManager: WindowManager 67 @Mock private lateinit var configurationController: ConfigurationController 68 @Mock private lateinit var latencyTracker: LatencyTracker <lambda>null69 private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } 70 @Mock private lateinit var layoutParams: WindowManager.LayoutParams 71 @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback 72 73 @Before setupnull74 fun setup() { 75 MockitoAnnotations.initMocks(this) 76 testableLooper = TestableLooper.get(this) 77 systemClock = FakeSystemClock() 78 mBackPanelController = 79 BackPanelController( 80 context, 81 windowManager, 82 ViewConfiguration.get(context), 83 Handler.createAsync(testableLooper.looper), 84 systemClock, 85 vibratorHelper, 86 configurationController, 87 latencyTracker, 88 interactionJankMonitor, 89 ) 90 mBackPanelController.setLayoutParams(layoutParams) 91 mBackPanelController.setBackCallback(backCallback) 92 mBackPanelController.setIsLeftPanel(true) 93 triggerThreshold = mBackPanelController.params.staticTriggerThreshold 94 } 95 96 @Test handlesActionDownnull97 fun handlesActionDown() { 98 startTouch() 99 100 assertThat(mBackPanelController.currentState) 101 .isEqualTo(BackPanelController.GestureState.GONE) 102 } 103 104 @Test staysHiddenBeforeSlopCrossednull105 fun staysHiddenBeforeSlopCrossed() { 106 startTouch() 107 // Move just enough to not cross the touch slop 108 continueTouch(START_X + touchSlop - 1) 109 110 assertThat(mBackPanelController.currentState) 111 .isEqualTo(BackPanelController.GestureState.GONE) 112 verify(interactionJankMonitor, never()).begin(any()) 113 } 114 115 @Test handlesBackCommittednull116 fun handlesBackCommitted() { 117 startTouch() 118 // Move once to cross the touch slop 119 continueTouch(START_X + touchSlop.toFloat() + 1) 120 assertThat(mBackPanelController.currentState) 121 .isEqualTo(BackPanelController.GestureState.ENTRY) 122 verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW) 123 verify(interactionJankMonitor) 124 .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW) 125 // Move again to cross the back trigger threshold 126 continueTouch(START_X + touchSlop + triggerThreshold + 1) 127 // Wait threshold duration and hold touch past trigger threshold 128 moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) 129 continueTouch(START_X + touchSlop + triggerThreshold + 1) 130 131 assertThat(mBackPanelController.currentState) 132 .isEqualTo(BackPanelController.GestureState.ACTIVE) 133 verify(backCallback).setTriggerBack(true) 134 moveTimeForward(100) 135 verify(vibratorHelper) 136 .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) 137 finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) 138 assertThat(mBackPanelController.currentState) 139 .isEqualTo(BackPanelController.GestureState.COMMITTED) 140 verify(backCallback).triggerBack() 141 142 // Because the Handler that is typically used for transitioning the arrow state from 143 // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation, 144 // there is no way to meaningfully test that the state becomes GONE and that the tracked 145 // jank interaction is ended. So instead, manually trigger the failsafe, which does 146 // the same thing: 147 mBackPanelController.failsafeRunnable.run() 148 assertThat(mBackPanelController.currentState) 149 .isEqualTo(BackPanelController.GestureState.GONE) 150 verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW) 151 } 152 153 @Test handlesBackCancellednull154 fun handlesBackCancelled() { 155 startTouch() 156 // Move once to cross the touch slop 157 continueTouch(START_X + touchSlop.toFloat() + 1) 158 assertThat(mBackPanelController.currentState) 159 .isEqualTo(BackPanelController.GestureState.ENTRY) 160 // Move again to cross the back trigger threshold 161 continueTouch( 162 START_X + touchSlop + triggerThreshold - 163 mBackPanelController.params.deactivationTriggerThreshold 164 ) 165 // Wait threshold duration and hold touch before trigger threshold 166 moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) 167 continueTouch( 168 START_X + touchSlop + triggerThreshold - 169 mBackPanelController.params.deactivationTriggerThreshold 170 ) 171 clearInvocations(backCallback) 172 moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) 173 174 // Move in the opposite direction to cross the deactivation threshold and cancel back 175 continueTouch(START_X) 176 177 assertThat(mBackPanelController.currentState) 178 .isEqualTo(BackPanelController.GestureState.INACTIVE) 179 verify(backCallback).setTriggerBack(false) 180 verify(vibratorHelper) 181 .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE)) 182 183 finishTouchActionUp(START_X) 184 verify(backCallback).cancelBack() 185 } 186 startTouchnull187 private fun startTouch() { 188 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f)) 189 } 190 continueTouchnull191 private fun continueTouch(x: Float) { 192 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f)) 193 } 194 finishTouchActionUpnull195 private fun finishTouchActionUp(x: Float) { 196 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f)) 197 } 198 createMotionEventnull199 private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent { 200 return MotionEvent.obtain(0L, 0L, action, x, y, 0) 201 } 202 moveTimeForwardnull203 private fun moveTimeForward(millis: Long) { 204 systemClock.advanceTime(millis) 205 testableLooper.moveTimeForward(millis) 206 testableLooper.processAllMessages() 207 } 208 } 209