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.app.viewcapture.ViewCaptureAwareWindowManager 31 import com.android.internal.jank.Cuj 32 import com.android.internal.util.LatencyTracker 33 import com.android.systemui.SysuiTestCase 34 import com.android.systemui.jank.interactionJankMonitor 35 import com.android.systemui.plugins.NavigationEdgeBackPlugin 36 import com.android.systemui.statusbar.VibratorHelper 37 import com.android.systemui.statusbar.policy.ConfigurationController 38 import com.android.systemui.testKosmos 39 import com.android.systemui.util.time.FakeSystemClock 40 import com.google.common.truth.Truth.assertThat 41 import org.junit.Before 42 import org.junit.Test 43 import org.junit.runner.RunWith 44 import org.mockito.ArgumentMatchers.any 45 import org.mockito.ArgumentMatchers.eq 46 import org.mockito.Mock 47 import org.mockito.Mockito.clearInvocations 48 import org.mockito.Mockito.never 49 import org.mockito.Mockito.verify 50 import org.mockito.MockitoAnnotations 51 52 @SmallTest 53 @RunWith(AndroidJUnit4::class) 54 @TestableLooper.RunWithLooper(setAsMainLooper = true) 55 class BackPanelControllerTest : SysuiTestCase() { 56 companion object { 57 private const val START_X: Float = 0f 58 } 59 60 private val kosmos = testKosmos() 61 private lateinit var mBackPanelController: BackPanelController 62 private lateinit var systemClock: FakeSystemClock 63 private lateinit var testableLooper: TestableLooper 64 private var triggerThreshold: Float = 0.0f 65 private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop 66 @Mock private lateinit var vibratorHelper: VibratorHelper 67 @Mock private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager 68 @Mock private lateinit var configurationController: ConfigurationController 69 @Mock private lateinit var latencyTracker: LatencyTracker <lambda>null70 private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } 71 @Mock private lateinit var layoutParams: WindowManager.LayoutParams 72 @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback 73 74 @Before setupnull75 fun setup() { 76 MockitoAnnotations.initMocks(this) 77 testableLooper = TestableLooper.get(this) 78 systemClock = FakeSystemClock() 79 mBackPanelController = 80 BackPanelController( 81 context, 82 viewCaptureAwareWindowManager, 83 ViewConfiguration.get(context), 84 Handler.createAsync(testableLooper.looper), 85 systemClock, 86 vibratorHelper, 87 configurationController, 88 latencyTracker, 89 interactionJankMonitor, 90 ) 91 mBackPanelController.setLayoutParams(layoutParams) 92 mBackPanelController.setBackCallback(backCallback) 93 mBackPanelController.setIsLeftPanel(true) 94 triggerThreshold = mBackPanelController.params.staticTriggerThreshold 95 } 96 97 @Test handlesActionDownnull98 fun handlesActionDown() { 99 startTouch() 100 101 assertThat(mBackPanelController.currentState) 102 .isEqualTo(BackPanelController.GestureState.GONE) 103 } 104 105 @Test staysHiddenBeforeSlopCrossednull106 fun staysHiddenBeforeSlopCrossed() { 107 startTouch() 108 // Move just enough to not cross the touch slop 109 continueTouch(START_X + touchSlop - 1) 110 111 assertThat(mBackPanelController.currentState) 112 .isEqualTo(BackPanelController.GestureState.GONE) 113 verify(interactionJankMonitor, never()).begin(any()) 114 } 115 116 @Test handlesBackCommittednull117 fun handlesBackCommitted() { 118 startTouch() 119 // Move once to cross the touch slop 120 continueTouch(START_X + touchSlop.toFloat() + 1) 121 assertThat(mBackPanelController.currentState) 122 .isEqualTo(BackPanelController.GestureState.ENTRY) 123 verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW) 124 verify(interactionJankMonitor) 125 .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW) 126 // Move again to cross the back trigger threshold 127 continueTouch(START_X + touchSlop + triggerThreshold + 1) 128 // Wait threshold duration and hold touch past trigger threshold 129 moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) 130 continueTouch(START_X + touchSlop + triggerThreshold + 1) 131 132 assertThat(mBackPanelController.currentState) 133 .isEqualTo(BackPanelController.GestureState.ACTIVE) 134 verify(backCallback).setTriggerBack(true) 135 moveTimeForward(100) 136 verify(vibratorHelper) 137 .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) 138 finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) 139 assertThat(mBackPanelController.currentState) 140 .isEqualTo(BackPanelController.GestureState.COMMITTED) 141 verify(backCallback).triggerBack() 142 143 // Because the Handler that is typically used for transitioning the arrow state from 144 // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation, 145 // there is no way to meaningfully test that the state becomes GONE and that the tracked 146 // jank interaction is ended. So instead, manually trigger the failsafe, which does 147 // the same thing: 148 mBackPanelController.failsafeRunnable.run() 149 assertThat(mBackPanelController.currentState) 150 .isEqualTo(BackPanelController.GestureState.GONE) 151 verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW) 152 } 153 154 @Test handlesBackCancellednull155 fun handlesBackCancelled() { 156 startTouch() 157 // Move once to cross the touch slop 158 continueTouch(START_X + touchSlop.toFloat() + 1) 159 assertThat(mBackPanelController.currentState) 160 .isEqualTo(BackPanelController.GestureState.ENTRY) 161 // Move again to cross the back trigger threshold 162 continueTouch( 163 START_X + touchSlop + triggerThreshold - 164 mBackPanelController.params.deactivationTriggerThreshold 165 ) 166 // Wait threshold duration and hold touch before trigger threshold 167 moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) 168 continueTouch( 169 START_X + touchSlop + triggerThreshold - 170 mBackPanelController.params.deactivationTriggerThreshold 171 ) 172 clearInvocations(backCallback) 173 moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) 174 175 // Move in the opposite direction to cross the deactivation threshold and cancel back 176 continueTouch(START_X) 177 178 assertThat(mBackPanelController.currentState) 179 .isEqualTo(BackPanelController.GestureState.INACTIVE) 180 verify(backCallback).setTriggerBack(false) 181 verify(vibratorHelper) 182 .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE)) 183 184 finishTouchActionUp(START_X) 185 verify(backCallback).cancelBack() 186 } 187 startTouchnull188 private fun startTouch() { 189 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f)) 190 } 191 continueTouchnull192 private fun continueTouch(x: Float) { 193 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f)) 194 } 195 finishTouchActionUpnull196 private fun finishTouchActionUp(x: Float) { 197 mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f)) 198 } 199 createMotionEventnull200 private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent { 201 return MotionEvent.obtain(0L, 0L, action, x, y, 0) 202 } 203 moveTimeForwardnull204 private fun moveTimeForward(millis: Long) { 205 systemClock.advanceTime(millis) 206 testableLooper.moveTimeForward(millis) 207 testableLooper.processAllMessages() 208 } 209 } 210