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.bouncer.domain.interactor 18 19 import android.content.Context 20 import android.content.res.ColorStateList 21 import android.hardware.biometrics.BiometricSourceType 22 import android.os.Handler 23 import android.os.Trace 24 import android.util.Log 25 import android.view.View 26 import com.android.keyguard.KeyguardConstants 27 import com.android.keyguard.KeyguardSecurityModel 28 import com.android.keyguard.KeyguardUpdateMonitor 29 import com.android.keyguard.KeyguardUpdateMonitorCallback 30 import com.android.systemui.DejankUtils 31 import com.android.systemui.R 32 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository 33 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants 34 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN 35 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel 36 import com.android.systemui.bouncer.ui.BouncerView 37 import com.android.systemui.classifier.FalsingCollector 38 import com.android.systemui.dagger.SysUISingleton 39 import com.android.systemui.dagger.qualifiers.Application 40 import com.android.systemui.dagger.qualifiers.Main 41 import com.android.systemui.flags.FeatureFlags 42 import com.android.systemui.flags.Flags 43 import com.android.systemui.keyguard.DismissCallbackRegistry 44 import com.android.systemui.keyguard.data.repository.TrustRepository 45 import com.android.systemui.plugins.ActivityStarter 46 import com.android.systemui.shared.system.SysUiStatsLog 47 import com.android.systemui.statusbar.policy.KeyguardStateController 48 import javax.inject.Inject 49 import kotlinx.coroutines.CoroutineScope 50 import kotlinx.coroutines.flow.Flow 51 import kotlinx.coroutines.flow.StateFlow 52 import kotlinx.coroutines.flow.combine 53 import kotlinx.coroutines.flow.filter 54 import kotlinx.coroutines.flow.filterNotNull 55 import kotlinx.coroutines.flow.map 56 import kotlinx.coroutines.launch 57 58 /** 59 * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password) 60 * bouncer. 61 */ 62 @SysUISingleton 63 class PrimaryBouncerInteractor 64 @Inject 65 constructor( 66 private val repository: KeyguardBouncerRepository, 67 private val primaryBouncerView: BouncerView, 68 @Main private val mainHandler: Handler, 69 private val keyguardStateController: KeyguardStateController, 70 private val keyguardSecurityModel: KeyguardSecurityModel, 71 private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor, 72 private val falsingCollector: FalsingCollector, 73 private val dismissCallbackRegistry: DismissCallbackRegistry, 74 private val context: Context, 75 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 76 private val trustRepository: TrustRepository, 77 private val featureFlags: FeatureFlags, 78 @Application private val applicationScope: CoroutineScope, 79 ) { 80 private val passiveAuthBouncerDelay = 81 context.resources.getInteger(R.integer.primary_bouncer_passive_auth_delay).toLong() 82 /** Runnable to show the primary bouncer. */ 83 val showRunnable = Runnable { 84 repository.setPrimaryShow(true) 85 repository.setPrimaryShowingSoon(false) 86 primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE) 87 } 88 89 val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull() 90 val isShowing: StateFlow<Boolean> = repository.primaryBouncerShow 91 val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {} 92 val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull() 93 val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull() 94 val startingDisappearAnimation: Flow<Runnable> = 95 repository.primaryBouncerStartingDisappearAnimation.filterNotNull() 96 val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it } 97 val keyguardPosition: Flow<Float> = repository.keyguardPosition.filterNotNull() 98 val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount 99 /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */ 100 val bouncerExpansion: Flow<Float> = 101 combine(repository.panelExpansionAmount, repository.primaryBouncerShow) { 102 panelExpansion, 103 primaryBouncerIsShowing -> 104 if (primaryBouncerIsShowing) { 105 1f - panelExpansion 106 } else { 107 0f 108 } 109 } 110 /** Allow for interaction when just about fully visible */ 111 val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 } 112 val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing 113 private var currentUserActiveUnlockRunning = false 114 115 /** This callback needs to be a class field so it does not get garbage collected. */ 116 val keyguardUpdateMonitorCallback = 117 object : KeyguardUpdateMonitorCallback() { 118 override fun onBiometricRunningStateChanged( 119 running: Boolean, 120 biometricSourceType: BiometricSourceType? 121 ) { 122 updateSideFpsVisibility() 123 } 124 125 override fun onStrongAuthStateChanged(userId: Int) { 126 updateSideFpsVisibility() 127 } 128 } 129 130 init { 131 keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) 132 if (featureFlags.isEnabled(Flags.DELAY_BOUNCER)) { 133 applicationScope.launch { 134 trustRepository.isCurrentUserActiveUnlockRunning.collect { 135 currentUserActiveUnlockRunning = it 136 } 137 } 138 } 139 } 140 141 // TODO(b/243685699): Move isScrimmed logic to data layer. 142 // TODO(b/243695312): Encapsulate all of the show logic for the bouncer. 143 /** Show the bouncer if necessary and set the relevant states. */ 144 @JvmOverloads 145 fun show(isScrimmed: Boolean) { 146 // Reset some states as we show the bouncer. 147 repository.setKeyguardAuthenticated(null) 148 repository.setPrimaryStartingToHide(false) 149 150 val resumeBouncer = 151 (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) && 152 needsFullscreenBouncer() 153 154 Trace.beginSection("KeyguardBouncer#show") 155 repository.setPrimaryScrimmed(isScrimmed) 156 if (isScrimmed) { 157 setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) 158 } 159 160 // In this special case, we want to hide the bouncer and show it again. We want to emit 161 // show(true) again so that we can reinflate the new view. 162 if (resumeBouncer) { 163 repository.setPrimaryShow(false) 164 } 165 166 if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) { 167 // Keyguard is done. 168 return 169 } 170 171 repository.setPrimaryShowingSoon(true) 172 if (usePrimaryBouncerPassiveAuthDelay()) { 173 Log.d(TAG, "delay bouncer, passive auth may succeed") 174 mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay) 175 } else { 176 DejankUtils.postAfterTraversal(showRunnable) 177 } 178 keyguardStateController.notifyPrimaryBouncerShowing(true) 179 primaryBouncerCallbackInteractor.dispatchStartingToShow() 180 Trace.endSection() 181 } 182 183 /** Sets the correct bouncer states to hide the bouncer. */ 184 fun hide() { 185 Trace.beginSection("KeyguardBouncer#hide") 186 if (isFullyShowing()) { 187 SysUiStatsLog.write( 188 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, 189 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN 190 ) 191 dismissCallbackRegistry.notifyDismissCancelled() 192 } 193 194 repository.setPrimaryStartDisappearAnimation(null) 195 falsingCollector.onBouncerHidden() 196 keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */) 197 cancelShowRunnable() 198 repository.setPrimaryShowingSoon(false) 199 repository.setPrimaryShow(false) 200 repository.setPanelExpansion(EXPANSION_HIDDEN) 201 primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE) 202 Trace.endSection() 203 } 204 205 /** 206 * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f 207 * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the 208 * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show. 209 * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion 210 * of 0f represents the bouncer fully showing. 211 */ 212 fun setPanelExpansion(expansion: Float) { 213 val oldExpansion = repository.panelExpansionAmount.value 214 val expansionChanged = oldExpansion != expansion 215 if (repository.primaryBouncerStartingDisappearAnimation.value == null) { 216 repository.setPanelExpansion(expansion) 217 } 218 219 if ( 220 expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE && 221 oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE 222 ) { 223 falsingCollector.onBouncerShown() 224 primaryBouncerCallbackInteractor.dispatchFullyShown() 225 } else if ( 226 expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN && 227 oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN 228 ) { 229 /* 230 * There are cases where #hide() was not invoked, such as when 231 * NotificationPanelViewController controls the hide animation. Make sure the state gets 232 * updated by calling #hide() directly. 233 */ 234 hide() 235 DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() } 236 primaryBouncerCallbackInteractor.dispatchFullyHidden() 237 } else if ( 238 expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE && 239 oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE 240 ) { 241 primaryBouncerCallbackInteractor.dispatchStartingToHide() 242 repository.setPrimaryStartingToHide(true) 243 } 244 if (expansionChanged) { 245 primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion) 246 } 247 } 248 249 /** Set the initial keyguard message to show when bouncer is shown. */ 250 fun showMessage(message: String?, colorStateList: ColorStateList?) { 251 repository.setShowMessage(BouncerShowMessageModel(message, colorStateList)) 252 } 253 254 /** 255 * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is 256 * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we 257 * call cancelAction. 258 */ 259 fun setDismissAction( 260 onDismissAction: ActivityStarter.OnDismissAction?, 261 cancelAction: Runnable? 262 ) { 263 primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction) 264 } 265 266 /** Update the resources of the views. */ 267 fun updateResources() { 268 repository.setResourceUpdateRequests(true) 269 } 270 271 /** Tell the bouncer that keyguard is authenticated. */ 272 fun notifyKeyguardAuthenticated(strongAuth: Boolean) { 273 repository.setKeyguardAuthenticated(strongAuth) 274 } 275 276 /** Update the position of the bouncer when showing. */ 277 fun setKeyguardPosition(position: Float) { 278 repository.setKeyguardPosition(position) 279 } 280 281 /** Notifies that the state change was handled. */ 282 fun notifyKeyguardAuthenticatedHandled() { 283 repository.setKeyguardAuthenticated(null) 284 } 285 286 /** Notifies that the message was shown. */ 287 fun onMessageShown() { 288 repository.setShowMessage(null) 289 } 290 291 /** Notify that the resources have been updated */ 292 fun notifyUpdatedResources() { 293 repository.setResourceUpdateRequests(false) 294 } 295 296 /** Set whether back button is enabled when on the bouncer screen. */ 297 fun setBackButtonEnabled(enabled: Boolean) { 298 repository.setIsBackButtonEnabled(enabled) 299 } 300 301 /** Tell the bouncer to start the pre hide animation. */ 302 fun startDisappearAnimation(runnable: Runnable) { 303 if (willRunDismissFromKeyguard()) { 304 runnable.run() 305 return 306 } 307 308 repository.setPrimaryStartDisappearAnimation(runnable) 309 } 310 311 /** Determine whether to show the side fps animation. */ 312 fun updateSideFpsVisibility() { 313 val sfpsEnabled: Boolean = 314 context.resources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer) 315 val fpsDetectionRunning: Boolean = keyguardUpdateMonitor.isFingerprintDetectionRunning 316 val isUnlockingWithFpAllowed: Boolean = 317 keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed 318 val toShow = 319 (isBouncerShowing() && 320 sfpsEnabled && 321 fpsDetectionRunning && 322 isUnlockingWithFpAllowed && 323 !isAnimatingAway()) 324 325 if (KeyguardConstants.DEBUG) { 326 Log.d( 327 TAG, 328 ("sideFpsToShow=$toShow\n" + 329 "isBouncerShowing=${isBouncerShowing()}\n" + 330 "configEnabled=$sfpsEnabled\n" + 331 "fpsDetectionRunning=$fpsDetectionRunning\n" + 332 "isUnlockingWithFpAllowed=$isUnlockingWithFpAllowed\n" + 333 "isAnimatingAway=${isAnimatingAway()}") 334 ) 335 } 336 repository.setSideFpsShowing(toShow) 337 } 338 339 /** Returns whether bouncer is fully showing. */ 340 fun isFullyShowing(): Boolean { 341 return (repository.primaryBouncerShowingSoon.value || isBouncerShowing()) && 342 repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE && 343 repository.primaryBouncerStartingDisappearAnimation.value == null 344 } 345 346 /** Returns whether bouncer is scrimmed. */ 347 fun isScrimmed(): Boolean { 348 return repository.primaryBouncerScrimmed.value 349 } 350 351 /** If bouncer expansion is between 0f and 1f non-inclusive. */ 352 fun isInTransit(): Boolean { 353 return repository.primaryBouncerShowingSoon.value || 354 repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN && 355 repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE 356 } 357 358 /** Return whether bouncer is animating away. */ 359 fun isAnimatingAway(): Boolean { 360 return repository.primaryBouncerStartingDisappearAnimation.value != null 361 } 362 363 /** Return whether bouncer will dismiss with actions */ 364 fun willDismissWithAction(): Boolean { 365 return primaryBouncerView.delegate?.willDismissWithActions() == true 366 } 367 368 /** Will the dismissal run from the keyguard layout (instead of from bouncer) */ 369 fun willRunDismissFromKeyguard(): Boolean { 370 return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true 371 } 372 373 /** Returns whether the bouncer should be full screen. */ 374 private fun needsFullscreenBouncer(): Boolean { 375 val mode: KeyguardSecurityModel.SecurityMode = 376 keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser()) 377 return mode == KeyguardSecurityModel.SecurityMode.SimPin || 378 mode == KeyguardSecurityModel.SecurityMode.SimPuk 379 } 380 381 /** Remove the show runnable from the main handler queue to improve performance. */ 382 private fun cancelShowRunnable() { 383 DejankUtils.removeCallbacks(showRunnable) 384 mainHandler.removeCallbacks(showRunnable) 385 } 386 387 /** Returns whether the primary bouncer is currently showing. */ 388 fun isBouncerShowing(): Boolean { 389 return isShowing.value 390 } 391 392 /** Whether we want to wait to show the bouncer in case passive auth succeeds. */ 393 private fun usePrimaryBouncerPassiveAuthDelay(): Boolean { 394 val canRunFaceAuth = 395 keyguardStateController.isFaceAuthEnabled && 396 keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) && 397 keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth() 398 val canRunActiveUnlock = 399 currentUserActiveUnlockRunning && 400 keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState() 401 402 return featureFlags.isEnabled(Flags.DELAY_BOUNCER) && 403 !needsFullscreenBouncer() && 404 (canRunFaceAuth || canRunActiveUnlock) 405 } 406 407 companion object { 408 private const val TAG = "PrimaryBouncerInteractor" 409 } 410 } 411