1 /* 2 * Copyright (C) 2021 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.biometrics 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.graphics.PointF 22 import android.hardware.biometrics.BiometricSourceType 23 import androidx.annotation.VisibleForTesting 24 import com.android.keyguard.KeyguardUpdateMonitor 25 import com.android.keyguard.KeyguardUpdateMonitorCallback 26 import com.android.settingslib.Utils 27 import com.android.systemui.statusbar.CircleReveal 28 import com.android.systemui.statusbar.LightRevealEffect 29 import com.android.systemui.statusbar.NotificationShadeWindowController 30 import com.android.systemui.statusbar.commandline.Command 31 import com.android.systemui.statusbar.commandline.CommandRegistry 32 import com.android.systemui.statusbar.phone.BiometricUnlockController 33 import com.android.systemui.statusbar.phone.KeyguardBypassController 34 import com.android.systemui.statusbar.phone.StatusBar 35 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope 36 import com.android.systemui.statusbar.policy.ConfigurationController 37 import com.android.systemui.util.ViewController 38 import java.io.PrintWriter 39 import javax.inject.Inject 40 41 /*** 42 * Controls the ripple effect that shows when authentication is successful. 43 * The ripple uses the accent color of the current theme. 44 */ 45 @StatusBarScope 46 class AuthRippleController @Inject constructor( 47 private val statusBar: StatusBar, 48 private val sysuiContext: Context, 49 private val authController: AuthController, 50 private val configurationController: ConfigurationController, 51 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 52 private val commandRegistry: CommandRegistry, 53 private val notificationShadeWindowController: NotificationShadeWindowController, 54 private val bypassController: KeyguardBypassController, 55 private val biometricUnlockController: BiometricUnlockController, 56 rippleView: AuthRippleView? 57 ) : ViewController<AuthRippleView>(rippleView) { 58 var fingerprintSensorLocation: PointF? = null 59 private var faceSensorLocation: PointF? = null 60 private var circleReveal: LightRevealEffect? = null 61 62 @VisibleForTesting onViewAttachednull63 public override fun onViewAttached() { 64 updateRippleColor() 65 updateSensorLocation() 66 authController.addCallback(authControllerCallback) 67 configurationController.addCallback(configurationChangedListener) 68 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 69 commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } 70 } 71 72 @VisibleForTesting onViewDetachednull73 public override fun onViewDetached() { 74 authController.removeCallback(authControllerCallback) 75 keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) 76 configurationController.removeCallback(configurationChangedListener) 77 commandRegistry.unregisterCommand("auth-ripple") 78 79 notificationShadeWindowController.setForcePluginOpen(false, this) 80 } 81 showRipplenull82 private fun showRipple(biometricSourceType: BiometricSourceType?) { 83 if (!keyguardUpdateMonitor.isKeyguardVisible || 84 keyguardUpdateMonitor.userNeedsStrongAuth()) { 85 return 86 } 87 88 if (biometricSourceType == BiometricSourceType.FINGERPRINT && 89 fingerprintSensorLocation != null) { 90 mView.setSensorLocation(fingerprintSensorLocation!!) 91 showRipple() 92 } else if (biometricSourceType == BiometricSourceType.FACE && 93 faceSensorLocation != null) { 94 if (!bypassController.canBypass()) { 95 return 96 } 97 mView.setSensorLocation(faceSensorLocation!!) 98 showRipple() 99 } 100 } 101 showRipplenull102 private fun showRipple() { 103 notificationShadeWindowController.setForcePluginOpen(true, this) 104 val biometricUnlockMode = biometricUnlockController.mode 105 val useCircleReveal = circleReveal != null && 106 (biometricUnlockMode == BiometricUnlockController.MODE_WAKE_AND_UNLOCK || 107 biometricUnlockMode == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING || 108 biometricUnlockMode == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM) 109 val lightRevealScrim = statusBar.lightRevealScrim 110 if (useCircleReveal) { 111 lightRevealScrim?.revealEffect = circleReveal!! 112 } 113 114 mView.startRipple( 115 /* end runnable */ 116 Runnable { 117 notificationShadeWindowController.setForcePluginOpen(false, this) 118 }, 119 /* circleReveal */ 120 if (useCircleReveal) { 121 lightRevealScrim 122 } else { 123 null 124 } 125 ) 126 } 127 updateSensorLocationnull128 fun updateSensorLocation() { 129 fingerprintSensorLocation = authController.fingerprintSensorLocation 130 faceSensorLocation = authController.faceAuthSensorLocation 131 fingerprintSensorLocation?.let { 132 circleReveal = CircleReveal( 133 it.x, 134 it.y, 135 0f, 136 Math.max( 137 Math.max(it.x, statusBar.displayWidth - it.x), 138 Math.max(it.y, statusBar.displayHeight - it.y) 139 ) 140 ) 141 } 142 } 143 updateRippleColornull144 private fun updateRippleColor() { 145 mView.setColor( 146 Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor) 147 } 148 149 val keyguardUpdateMonitorCallback = 150 object : KeyguardUpdateMonitorCallback() { onBiometricAuthenticatednull151 override fun onBiometricAuthenticated( 152 userId: Int, 153 biometricSourceType: BiometricSourceType?, 154 isStrongBiometric: Boolean 155 ) { 156 showRipple(biometricSourceType) 157 } 158 } 159 160 val configurationChangedListener = 161 object : ConfigurationController.ConfigurationListener { onConfigChangednull162 override fun onConfigChanged(newConfig: Configuration?) { 163 updateSensorLocation() 164 } onUiModeChangednull165 override fun onUiModeChanged() { 166 updateRippleColor() 167 } onThemeChangednull168 override fun onThemeChanged() { 169 updateRippleColor() 170 } onOverlayChangednull171 override fun onOverlayChanged() { 172 updateRippleColor() 173 } 174 } 175 <lambda>null176 private val authControllerCallback = AuthController.Callback { updateSensorLocation() } 177 178 inner class AuthRippleCommand : Command { executenull179 override fun execute(pw: PrintWriter, args: List<String>) { 180 if (args.isEmpty()) { 181 invalidCommand(pw) 182 } else { 183 when (args[0]) { 184 "fingerprint" -> { 185 pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") 186 showRipple(BiometricSourceType.FINGERPRINT) 187 } 188 "face" -> { 189 pw.println("face ripple sensorLocation=$faceSensorLocation") 190 showRipple(BiometricSourceType.FACE) 191 } 192 "custom" -> { 193 if (args.size != 3 || 194 args[1].toFloatOrNull() == null || 195 args[2].toFloatOrNull() == null) { 196 invalidCommand(pw) 197 return 198 } 199 pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " + 200 args[2].toFloat()) 201 mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat())) 202 showRipple() 203 } 204 else -> invalidCommand(pw) 205 } 206 } 207 } 208 helpnull209 override fun help(pw: PrintWriter) { 210 pw.println("Usage: adb shell cmd statusbar auth-ripple <command>") 211 pw.println("Available commands:") 212 pw.println(" fingerprint") 213 pw.println(" face") 214 pw.println(" custom <x-location: int> <y-location: int>") 215 } 216 invalidCommandnull217 fun invalidCommand(pw: PrintWriter) { 218 pw.println("invalid command") 219 help(pw) 220 } 221 } 222 } 223