1 /* <lambda>null2 * Copyright (C) 2024 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.statusbar.notification 18 19 import androidx.compose.foundation.gestures.FlingBehavior 20 import androidx.compose.foundation.gestures.ScrollScope 21 import androidx.compose.ui.geometry.Offset 22 import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput 23 import androidx.test.ext.junit.runners.AndroidJUnit4 24 import androidx.test.filters.SmallTest 25 import com.android.systemui.SysuiTestCase 26 import com.android.systemui.notifications.ui.composable.NotificationScrimNestedScrollConnection 27 import com.google.common.truth.Truth.assertThat 28 import kotlinx.coroutines.test.runTest 29 import org.junit.Test 30 import org.junit.runner.RunWith 31 32 @SmallTest 33 @RunWith(AndroidJUnit4::class) 34 class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 35 private var isStarted = false 36 private var wasStarted = false 37 private var scrimOffset = 0f 38 private var contentHeight = 0f 39 private var isCurrentGestureOverscroll = false 40 private val customFlingBehavior = 41 object : FlingBehavior { 42 override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { 43 scrollBy(initialVelocity) 44 return initialVelocity / 2f 45 } 46 } 47 48 private val scrollConnection = 49 NotificationScrimNestedScrollConnection( 50 scrimOffset = { scrimOffset }, 51 snapScrimOffset = { _ -> }, 52 animateScrimOffset = { _ -> }, 53 minScrimOffset = { MIN_SCRIM_OFFSET }, 54 maxScrimOffset = MAX_SCRIM_OFFSET, 55 contentHeight = { contentHeight }, 56 minVisibleScrimHeight = { MIN_VISIBLE_SCRIM_HEIGHT }, 57 isCurrentGestureOverscroll = { isCurrentGestureOverscroll }, 58 onStart = { isStarted = true }, 59 onStop = { 60 wasStarted = true 61 isStarted = false 62 }, 63 flingBehavior = customFlingBehavior, 64 ) 65 66 @Test 67 fun onScrollUp_canStartPreScroll_contentNotExpanded_ignoreScroll() = runTest { 68 contentHeight = COLLAPSED_CONTENT_HEIGHT 69 70 val offsetConsumed = 71 scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput) 72 73 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 74 assertThat(isStarted).isEqualTo(false) 75 } 76 77 @Test 78 fun onScrollUp_canStartPreScroll_contentExpandedAtMinOffset_ignoreScroll() = runTest { 79 contentHeight = EXPANDED_CONTENT_HEIGHT 80 scrimOffset = MIN_SCRIM_OFFSET 81 82 val offsetConsumed = 83 scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput) 84 85 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 86 assertThat(isStarted).isEqualTo(false) 87 } 88 89 @Test 90 fun onScrollUp_canStartPreScroll_contentExpanded_consumeScroll() = runTest { 91 contentHeight = EXPANDED_CONTENT_HEIGHT 92 93 val availableOffset = Offset(x = 0f, y = -1f) 94 val offsetConsumed = 95 scrollConnection.onPreScroll(available = availableOffset, source = UserInput) 96 97 assertThat(offsetConsumed).isEqualTo(availableOffset) 98 assertThat(isStarted).isEqualTo(true) 99 } 100 101 @Test 102 fun onScrollUp_canStartPreScroll_contentExpanded_consumeScrollWithRemainder() = runTest { 103 contentHeight = EXPANDED_CONTENT_HEIGHT 104 scrimOffset = MIN_SCRIM_OFFSET + 1 105 106 val availableOffset = Offset(x = 0f, y = -2f) 107 val consumableOffset = Offset(x = 0f, y = -1f) 108 val offsetConsumed = 109 scrollConnection.onPreScroll(available = availableOffset, source = UserInput) 110 111 assertThat(offsetConsumed).isEqualTo(consumableOffset) 112 assertThat(isStarted).isEqualTo(true) 113 } 114 115 @Test 116 fun onScrollUp_canStartPostScroll_ignoreScroll() = runTest { 117 val offsetConsumed = 118 scrollConnection.onPostScroll( 119 consumed = Offset.Zero, 120 available = Offset(x = 0f, y = -1f), 121 source = UserInput, 122 ) 123 124 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 125 assertThat(isStarted).isEqualTo(false) 126 } 127 128 @Test 129 fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest { 130 val offsetConsumed = 131 scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput) 132 133 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 134 assertThat(isStarted).isEqualTo(false) 135 } 136 137 @Test 138 fun onScrollDown_canStartPostScroll_consumeScroll() = runTest { 139 scrimOffset = MIN_SCRIM_OFFSET 140 141 val availableOffset = Offset(x = 0f, y = 1f) 142 val offsetConsumed = 143 scrollConnection.onPostScroll( 144 consumed = Offset.Zero, 145 available = availableOffset, 146 source = UserInput, 147 ) 148 149 assertThat(offsetConsumed).isEqualTo(availableOffset) 150 assertThat(isStarted).isEqualTo(true) 151 } 152 153 @Test 154 fun onScrollDown_canStartPostScroll_consumeScrollWithRemainder() = runTest { 155 scrimOffset = MAX_SCRIM_OFFSET - 1 156 157 val availableOffset = Offset(x = 0f, y = 2f) 158 val consumableOffset = Offset(x = 0f, y = 1f) 159 val offsetConsumed = 160 scrollConnection.onPostScroll( 161 consumed = Offset.Zero, 162 available = availableOffset, 163 source = UserInput, 164 ) 165 166 assertThat(offsetConsumed).isEqualTo(consumableOffset) 167 assertThat(isStarted).isEqualTo(true) 168 } 169 170 @Test 171 fun canStartPostScroll_atMaxOffset_ignoreScroll() = runTest { 172 scrimOffset = MAX_SCRIM_OFFSET 173 174 val offsetConsumed = 175 scrollConnection.onPostScroll( 176 consumed = Offset.Zero, 177 available = Offset(x = 0f, y = 1f), 178 source = UserInput, 179 ) 180 181 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 182 assertThat(wasStarted).isEqualTo(false) 183 assertThat(isStarted).isEqualTo(false) 184 } 185 186 @Test 187 fun canStartPostScroll_externalOverscrollGesture_startButIgnoreScroll() = runTest { 188 scrimOffset = MAX_SCRIM_OFFSET 189 isCurrentGestureOverscroll = true 190 191 val offsetConsumed = 192 scrollConnection.onPostScroll( 193 consumed = Offset.Zero, 194 available = Offset(x = 0f, y = 1f), 195 source = UserInput, 196 ) 197 198 assertThat(offsetConsumed).isEqualTo(Offset.Zero) 199 // Returning 0 offset will immediately stop the connection 200 assertThat(wasStarted).isEqualTo(true) 201 assertThat(isStarted).isEqualTo(false) 202 } 203 204 @Test 205 fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest { 206 scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f 207 contentHeight = EXPANDED_CONTENT_HEIGHT 208 scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput) 209 210 assertThat(isStarted).isEqualTo(true) 211 212 scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput) 213 214 assertThat(isStarted).isEqualTo(true) 215 } 216 217 @Test 218 fun canContinueScroll_atMaxOffset_false() = runTest { 219 scrimOffset = MAX_SCRIM_OFFSET 220 contentHeight = EXPANDED_CONTENT_HEIGHT 221 scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput) 222 223 assertThat(isStarted).isEqualTo(true) 224 225 scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput) 226 227 assertThat(isStarted).isEqualTo(false) 228 } 229 230 companion object { 231 const val MIN_SCRIM_OFFSET = -100f 232 const val MAX_SCRIM_OFFSET = 0f 233 234 const val EXPANDED_CONTENT_HEIGHT = 200f 235 const val COLLAPSED_CONTENT_HEIGHT = 40f 236 237 const val MIN_VISIBLE_SCRIM_HEIGHT = 50f 238 } 239 } 240