1 /* 2 * Copyright (C) 2022 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.keyguard.data.repository 18 19 import android.graphics.Point 20 import android.testing.TestableLooper 21 import androidx.test.ext.junit.runners.AndroidJUnit4 22 import androidx.test.filters.SmallTest 23 import com.android.systemui.RoboPilotTest 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.animation.AnimatorTestRule 26 import com.android.systemui.coroutines.collectLastValue 27 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel 28 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource 29 import com.android.systemui.keyguard.shared.model.WakeSleepReason 30 import com.android.systemui.keyguard.shared.model.WakefulnessModel 31 import com.android.systemui.keyguard.shared.model.WakefulnessState 32 import com.android.systemui.statusbar.CircleReveal 33 import com.android.systemui.statusbar.LightRevealEffect 34 import junit.framework.Assert.assertEquals 35 import junit.framework.Assert.assertFalse 36 import kotlinx.coroutines.ExperimentalCoroutinesApi 37 import kotlinx.coroutines.launch 38 import kotlinx.coroutines.test.UnconfinedTestDispatcher 39 import kotlinx.coroutines.test.runCurrent 40 import kotlinx.coroutines.test.runTest 41 import org.junit.Before 42 import org.junit.Rule 43 import org.junit.Test 44 import org.junit.runner.RunWith 45 import org.mockito.MockitoAnnotations 46 47 @SmallTest 48 @RoboPilotTest 49 @OptIn(ExperimentalCoroutinesApi::class) 50 @RunWith(AndroidJUnit4::class) 51 class LightRevealScrimRepositoryTest : SysuiTestCase() { 52 private lateinit var fakeKeyguardRepository: FakeKeyguardRepository 53 private lateinit var underTest: LightRevealScrimRepositoryImpl 54 55 @get:Rule val animatorTestRule = AnimatorTestRule() 56 57 @Before 58 fun setUp() { 59 MockitoAnnotations.initMocks(this) 60 fakeKeyguardRepository = FakeKeyguardRepository() 61 underTest = LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context) 62 } 63 64 @Test 65 fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest { 66 val values = mutableListOf<LightRevealEffect>() 67 val job = launch { underTest.revealEffect.collect { values.add(it) } } 68 69 fakeKeyguardRepository.setWakefulnessModel( 70 WakefulnessModel( 71 WakefulnessState.STARTING_TO_WAKE, 72 WakeSleepReason.OTHER, 73 WakeSleepReason.OTHER 74 ) 75 ) 76 // We should initially emit the default reveal effect. 77 runCurrent() 78 values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT }) 79 80 // The source and sensor locations are still null, so we should still be using the 81 // default reveal despite a biometric unlock. 82 fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) 83 84 runCurrent() 85 values.assertEffectsMatchPredicates( 86 { it == DEFAULT_REVEAL_EFFECT }, 87 ) 88 89 // We got a source but still have no sensor locations, so should be sticking with 90 // the default effect. 91 fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) 92 93 runCurrent() 94 values.assertEffectsMatchPredicates( 95 { it == DEFAULT_REVEAL_EFFECT }, 96 ) 97 98 // We got a location for the face sensor, but we unlocked with fingerprint. 99 val faceLocation = Point(250, 0) 100 fakeKeyguardRepository.setFaceSensorLocation(faceLocation) 101 102 runCurrent() 103 values.assertEffectsMatchPredicates( 104 { it == DEFAULT_REVEAL_EFFECT }, 105 ) 106 107 // Now we have fingerprint sensor locations, and wake and unlock via fingerprint. 108 val fingerprintLocation = Point(500, 500) 109 fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation) 110 fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) 111 fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING) 112 113 // We should now have switched to the circle reveal, at the fingerprint location. 114 runCurrent() 115 values.assertEffectsMatchPredicates( 116 { it == DEFAULT_REVEAL_EFFECT }, 117 { 118 it is CircleReveal && 119 it.centerX == fingerprintLocation.x && 120 it.centerY == fingerprintLocation.y 121 }, 122 ) 123 124 // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals. 125 val valuesPrevSize = values.size 126 fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING) 127 fakeKeyguardRepository.setBiometricUnlockState( 128 BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM 129 ) 130 assertEquals(valuesPrevSize, values.size) 131 132 // Non-biometric unlock, we should return to the default reveal. 133 fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) 134 135 runCurrent() 136 values.assertEffectsMatchPredicates( 137 { it == DEFAULT_REVEAL_EFFECT }, 138 { 139 it is CircleReveal && 140 it.centerX == fingerprintLocation.x && 141 it.centerY == fingerprintLocation.y 142 }, 143 { it == DEFAULT_REVEAL_EFFECT }, 144 ) 145 146 // We already have a face location, so switching to face source should update the 147 // CircleReveal. 148 fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) 149 runCurrent() 150 fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) 151 runCurrent() 152 153 values.assertEffectsMatchPredicates( 154 { it == DEFAULT_REVEAL_EFFECT }, 155 { 156 it is CircleReveal && 157 it.centerX == fingerprintLocation.x && 158 it.centerY == fingerprintLocation.y 159 }, 160 { it == DEFAULT_REVEAL_EFFECT }, 161 { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y }, 162 ) 163 164 job.cancel() 165 } 166 167 @Test 168 @TestableLooper.RunWithLooper(setAsMainLooper = true) 169 fun revealAmount_emitsTo1AfterAnimationStarted() = 170 runTest(UnconfinedTestDispatcher()) { 171 val value by collectLastValue(underTest.revealAmount) 172 underTest.startRevealAmountAnimator(true) 173 assertEquals(0.0f, value) 174 animatorTestRule.advanceTimeBy(500L) 175 assertEquals(1.0f, value) 176 } 177 @Test 178 @TestableLooper.RunWithLooper(setAsMainLooper = true) 179 fun revealAmount_emitsTo0AfterAnimationStartedReversed() = 180 runTest(UnconfinedTestDispatcher()) { 181 val value by collectLastValue(underTest.revealAmount) 182 underTest.startRevealAmountAnimator(false) 183 assertEquals(1.0f, value) 184 animatorTestRule.advanceTimeBy(500L) 185 assertEquals(0.0f, value) 186 } 187 188 /** 189 * Asserts that the list of LightRevealEffects satisfies the list of predicates, in order, with 190 * no leftover elements. 191 */ 192 private fun List<LightRevealEffect>.assertEffectsMatchPredicates( 193 vararg predicates: (LightRevealEffect) -> Boolean 194 ) { 195 println(this) 196 assertEquals(predicates.size, this.size) 197 198 assertFalse( 199 zip(predicates) { effect, predicate -> predicate(effect) }.any { matched -> !matched } 200 ) 201 } 202 } 203