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.biometrics.ui.binder 18 19 import android.animation.Animator 20 import android.annotation.SuppressLint 21 import android.content.Context 22 import android.hardware.biometrics.BiometricAuthenticator 23 import android.hardware.biometrics.BiometricConstants 24 import android.hardware.biometrics.BiometricPrompt 25 import android.hardware.face.FaceManager 26 import android.os.Bundle 27 import android.text.method.ScrollingMovementMethod 28 import android.util.Log 29 import android.view.HapticFeedbackConstants 30 import android.view.MotionEvent 31 import android.view.View 32 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO 33 import android.view.accessibility.AccessibilityManager 34 import android.widget.Button 35 import android.widget.TextView 36 import androidx.lifecycle.DefaultLifecycleObserver 37 import androidx.lifecycle.Lifecycle 38 import androidx.lifecycle.LifecycleOwner 39 import androidx.lifecycle.lifecycleScope 40 import androidx.lifecycle.repeatOnLifecycle 41 import com.airbnb.lottie.LottieAnimationView 42 import com.android.systemui.R 43 import com.android.systemui.biometrics.AuthBiometricFaceIconController 44 import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController 45 import com.android.systemui.biometrics.AuthBiometricFingerprintIconController 46 import com.android.systemui.biometrics.AuthBiometricView 47 import com.android.systemui.biometrics.AuthBiometricView.Callback 48 import com.android.systemui.biometrics.AuthBiometricViewAdapter 49 import com.android.systemui.biometrics.AuthIconController 50 import com.android.systemui.biometrics.AuthPanelController 51 import com.android.systemui.biometrics.domain.model.BiometricModalities 52 import com.android.systemui.biometrics.shared.model.BiometricModality 53 import com.android.systemui.biometrics.shared.model.PromptKind 54 import com.android.systemui.biometrics.shared.model.asBiometricModality 55 import com.android.systemui.biometrics.ui.BiometricPromptLayout 56 import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode 57 import com.android.systemui.biometrics.ui.viewmodel.PromptMessage 58 import com.android.systemui.biometrics.ui.viewmodel.PromptSize 59 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel 60 import com.android.systemui.flags.FeatureFlags 61 import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION 62 import com.android.systemui.lifecycle.repeatWhenAttached 63 import com.android.systemui.statusbar.VibratorHelper 64 import kotlinx.coroutines.CoroutineScope 65 import kotlinx.coroutines.delay 66 import kotlinx.coroutines.flow.collect 67 import kotlinx.coroutines.flow.combine 68 import kotlinx.coroutines.flow.first 69 import kotlinx.coroutines.flow.map 70 import kotlinx.coroutines.launch 71 72 private const val TAG = "BiometricViewBinder" 73 74 /** Top-most view binder for BiometricPrompt views. */ 75 object BiometricViewBinder { 76 77 /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ 78 @SuppressLint("ClickableViewAccessibility") 79 @JvmStatic 80 fun bind( 81 view: BiometricPromptLayout, 82 viewModel: PromptViewModel, 83 panelViewController: AuthPanelController, 84 jankListener: BiometricJankListener, 85 backgroundView: View, 86 legacyCallback: Callback, 87 applicationScope: CoroutineScope, 88 vibratorHelper: VibratorHelper, 89 featureFlags: FeatureFlags, 90 ): AuthBiometricViewAdapter { 91 val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! 92 93 val textColorError = 94 view.resources.getColor(R.color.biometric_dialog_error, view.context.theme) 95 val textColorHint = 96 view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) 97 98 val titleView = view.requireViewById<TextView>(R.id.title) 99 val subtitleView = view.requireViewById<TextView>(R.id.subtitle) 100 val descriptionView = view.requireViewById<TextView>(R.id.description) 101 102 // set selected to enable marquee unless a screen reader is enabled 103 titleView.isSelected = 104 !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled 105 subtitleView.isSelected = 106 !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled 107 descriptionView.movementMethod = ScrollingMovementMethod() 108 109 val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) 110 val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) 111 112 PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel) 113 114 val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) 115 116 // Negative-side (left) buttons 117 val negativeButton = view.requireViewById<Button>(R.id.button_negative) 118 val cancelButton = view.requireViewById<Button>(R.id.button_cancel) 119 val credentialFallbackButton = view.requireViewById<Button>(R.id.button_use_credential) 120 121 // Positive-side (right) buttons 122 val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) 123 val retryButton = view.requireViewById<Button>(R.id.button_try_again) 124 125 // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers 126 val adapter = 127 Spaghetti( 128 view = view, 129 viewModel = viewModel, 130 applicationContext = view.context.applicationContext, 131 applicationScope = applicationScope, 132 ) 133 134 // bind to prompt 135 var boundSize = false 136 view.repeatWhenAttached { 137 // these do not change and need to be set before any size transitions 138 val modalities = viewModel.modalities.first() 139 titleView.text = viewModel.title.first() 140 descriptionView.text = viewModel.description.first() 141 subtitleView.text = viewModel.subtitle.first() 142 143 // set button listeners 144 negativeButton.setOnClickListener { 145 legacyCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE) 146 } 147 cancelButton.setOnClickListener { 148 legacyCallback.onAction(Callback.ACTION_USER_CANCELED) 149 } 150 credentialFallbackButton.setOnClickListener { 151 viewModel.onSwitchToCredential() 152 legacyCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL) 153 } 154 confirmationButton.setOnClickListener { viewModel.confirmAuthenticated() } 155 retryButton.setOnClickListener { 156 viewModel.showAuthenticating(isRetry = true) 157 legacyCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN) 158 } 159 160 // TODO(b/251476085): migrate legacy icon controllers and remove 161 var legacyState: Int = viewModel.legacyState.value 162 val iconController = 163 modalities.asIconController( 164 view.context, 165 iconView, 166 iconViewOverlay, 167 ) 168 adapter.attach(this, iconController, modalities, legacyCallback) 169 if (iconController is AuthBiometricFingerprintIconController) { 170 view.updateFingerprintAffordanceSize(iconController) 171 } 172 if (iconController is HackyCoexIconController) { 173 iconController.faceMode = !viewModel.isConfirmationRequired.first() 174 } 175 176 // the icon controller must be created before this happens for the legacy 177 // sizing code in BiometricPromptLayout to work correctly. Simplify this 178 // when those are also migrated. (otherwise the icon size may not be set to 179 // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly 180 // used as part of the measure spec) 181 if (!boundSize) { 182 boundSize = true 183 BiometricViewSizeBinder.bind( 184 view = view, 185 viewModel = viewModel, 186 viewsToHideWhenSmall = 187 listOf( 188 titleView, 189 subtitleView, 190 descriptionView, 191 ), 192 viewsToFadeInOnSizeChange = 193 listOf( 194 titleView, 195 subtitleView, 196 descriptionView, 197 indicatorMessageView, 198 negativeButton, 199 cancelButton, 200 retryButton, 201 confirmationButton, 202 credentialFallbackButton, 203 ), 204 panelViewController = panelViewController, 205 jankListener = jankListener, 206 ) 207 } 208 209 // TODO(b/251476085): migrate legacy icon controllers and remove 210 // The fingerprint sensor is started by the legacy 211 // AuthContainerView#onDialogAnimatedIn in all cases but the implicit coex flow 212 // (delayed mode). In that case, start it on the first transition to delayed 213 // which will be triggered by any auth failure. 214 lifecycleScope.launch { 215 val oldMode = viewModel.fingerprintStartMode.first() 216 viewModel.fingerprintStartMode.collect { newMode -> 217 // trigger sensor to start 218 if ( 219 oldMode == FingerprintStartMode.Pending && 220 newMode == FingerprintStartMode.Delayed 221 ) { 222 legacyCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR) 223 } 224 225 if (newMode.isStarted) { 226 // do wonky switch from implicit to explicit flow 227 (iconController as? HackyCoexIconController)?.faceMode = false 228 viewModel.showAuthenticating( 229 modalities.asDefaultHelpMessage(view.context), 230 ) 231 } 232 } 233 } 234 235 repeatOnLifecycle(Lifecycle.State.STARTED) { 236 // handle background clicks 237 launch { 238 combine(viewModel.isAuthenticated, viewModel.size) { (authenticated, _), size -> 239 when { 240 authenticated -> false 241 size == PromptSize.SMALL -> false 242 size == PromptSize.LARGE -> false 243 else -> true 244 } 245 } 246 .collect { dismissOnClick -> 247 backgroundView.setOnClickListener { 248 if (dismissOnClick) { 249 legacyCallback.onAction(Callback.ACTION_USER_CANCELED) 250 } else { 251 Log.w(TAG, "Ignoring background click") 252 } 253 } 254 } 255 } 256 257 // set messages 258 launch { 259 viewModel.isIndicatorMessageVisible.collect { show -> 260 indicatorMessageView.visibility = show.asVisibleOrHidden() 261 } 262 } 263 264 // set padding 265 launch { 266 viewModel.promptPadding.collect { promptPadding -> 267 view.setPadding( 268 promptPadding.left, 269 promptPadding.top, 270 promptPadding.right, 271 promptPadding.bottom 272 ) 273 } 274 } 275 276 // configure & hide/disable buttons 277 launch { 278 viewModel.credentialKind 279 .map { kind -> 280 when (kind) { 281 PromptKind.Pin -> 282 view.resources.getString(R.string.biometric_dialog_use_pin) 283 PromptKind.Password -> 284 view.resources.getString(R.string.biometric_dialog_use_password) 285 PromptKind.Pattern -> 286 view.resources.getString(R.string.biometric_dialog_use_pattern) 287 else -> "" 288 } 289 } 290 .collect { credentialFallbackButton.text = it } 291 } 292 launch { viewModel.negativeButtonText.collect { negativeButton.text = it } } 293 launch { 294 viewModel.isConfirmButtonVisible.collect { show -> 295 confirmationButton.visibility = show.asVisibleOrGone() 296 } 297 } 298 launch { 299 viewModel.isCancelButtonVisible.collect { show -> 300 cancelButton.visibility = show.asVisibleOrGone() 301 } 302 } 303 launch { 304 viewModel.isNegativeButtonVisible.collect { show -> 305 negativeButton.visibility = show.asVisibleOrGone() 306 } 307 } 308 launch { 309 viewModel.isTryAgainButtonVisible.collect { show -> 310 retryButton.visibility = show.asVisibleOrGone() 311 } 312 } 313 launch { 314 viewModel.isCredentialButtonVisible.collect { show -> 315 credentialFallbackButton.visibility = show.asVisibleOrGone() 316 } 317 } 318 319 // reuse the icon as a confirm button 320 launch { 321 viewModel.isIconConfirmButton 322 .map { isPending -> 323 when { 324 isPending && iconController.actsAsConfirmButton -> 325 View.OnTouchListener { _: View, event: MotionEvent -> 326 viewModel.onOverlayTouch(event) 327 } 328 else -> null 329 } 330 } 331 .collect { onTouch -> 332 iconViewOverlay.setOnTouchListener(onTouch) 333 iconView.setOnTouchListener(onTouch) 334 } 335 } 336 337 // TODO(b/251476085): remove w/ legacy icon controllers 338 // set icon affordance using legacy states 339 // like the old code, this causes animations to repeat on config changes :( 340 // but keep behavior for now as no one has complained... 341 launch { 342 viewModel.legacyState.collect { newState -> 343 iconController.updateState(legacyState, newState) 344 legacyState = newState 345 } 346 } 347 348 // dismiss prompt when authenticated and confirmed 349 launch { 350 viewModel.isAuthenticated.collect { authState -> 351 // Disable background view for cancelling authentication once authenticated, 352 // and remove from talkback 353 if (authState.isAuthenticated) { 354 // Prevents Talkback from speaking subtitle after already authenticated 355 subtitleView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO 356 backgroundView.setOnClickListener(null) 357 backgroundView.importantForAccessibility = 358 IMPORTANT_FOR_ACCESSIBILITY_NO 359 360 // Allow icon to be used as confirmation button with a11y enabled 361 if (accessibilityManager.isTouchExplorationEnabled) { 362 iconViewOverlay.setOnClickListener { 363 viewModel.confirmAuthenticated() 364 } 365 iconView.setOnClickListener { viewModel.confirmAuthenticated() } 366 } 367 } 368 if (authState.isAuthenticatedAndConfirmed) { 369 view.announceForAccessibility( 370 view.resources.getString(R.string.biometric_dialog_authenticated) 371 ) 372 373 launch { 374 delay(authState.delay) 375 legacyCallback.onAction( 376 if (authState.isAuthenticatedAndExplicitlyConfirmed) { 377 Callback.ACTION_AUTHENTICATED_AND_CONFIRMED 378 } else { 379 Callback.ACTION_AUTHENTICATED 380 } 381 ) 382 } 383 } 384 } 385 } 386 387 // show error & help messages 388 launch { 389 viewModel.message.collect { promptMessage -> 390 val isError = promptMessage is PromptMessage.Error 391 392 indicatorMessageView.text = promptMessage.message 393 indicatorMessageView.setTextColor( 394 if (isError) textColorError else textColorHint 395 ) 396 397 // select to enable marquee unless a screen reader is enabled 398 // TODO(wenhuiy): this may have recently changed per UX - verify and remove 399 indicatorMessageView.isSelected = 400 !accessibilityManager.isEnabled || 401 !accessibilityManager.isTouchExplorationEnabled 402 403 /** 404 * Note: Talkback 14.0 has new rate-limitation design to reduce frequency of 405 * TYPE_WINDOW_CONTENT_CHANGED events to once every 30 seconds. (context: 406 * b/281765653#comment18) Using {@link View#announceForAccessibility} 407 * instead as workaround since sending events exceeding this frequency is 408 * required. 409 */ 410 indicatorMessageView?.text?.let { 411 if (it.isNotBlank()) { 412 view.announceForAccessibility(it) 413 } 414 } 415 } 416 } 417 418 // Play haptics 419 if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { 420 launch { 421 viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> 422 if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) { 423 vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant) 424 viewModel.clearHaptics() 425 } 426 } 427 } 428 } 429 } 430 } 431 432 return adapter 433 } 434 } 435 436 /** 437 * Adapter for legacy events. Remove once legacy controller can be replaced by flagged code. 438 * 439 * These events can be dispatched when the view is being recreated so they need to be delivered to 440 * the view model (which will be retained) via the application scope. 441 * 442 * Do not reference the [view] for anything other than [asView]. 443 * 444 * TODO(b/251476085): remove after replacing AuthContainerView 445 */ 446 private class Spaghetti( 447 private val view: View, 448 private val viewModel: PromptViewModel, 449 private val applicationContext: Context, 450 private val applicationScope: CoroutineScope, 451 ) : AuthBiometricViewAdapter { 452 453 private var lifecycleScope: CoroutineScope? = null 454 private var modalities: BiometricModalities = BiometricModalities() 455 private var legacyCallback: Callback? = null 456 457 override var legacyIconController: AuthIconController? = null 458 private set 459 460 // hacky way to suppress lockout errors 461 private val lockoutErrorStrings = 462 listOf( 463 BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, 464 BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT, 465 ) 466 .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) } 467 468 fun attach( 469 lifecycleOwner: LifecycleOwner, 470 iconController: AuthIconController, 471 activeModalities: BiometricModalities, 472 callback: Callback, 473 ) { 474 modalities = activeModalities 475 legacyIconController = iconController 476 legacyCallback = callback 477 478 lifecycleOwner.lifecycle.addObserver( 479 object : DefaultLifecycleObserver { 480 override fun onCreate(owner: LifecycleOwner) { 481 lifecycleScope = owner.lifecycleScope 482 iconController.deactivated = false 483 } 484 485 override fun onDestroy(owner: LifecycleOwner) { 486 lifecycleScope = null 487 iconController.deactivated = true 488 } 489 } 490 ) 491 } 492 493 override fun onDialogAnimatedIn(fingerprintWasStarted: Boolean) { 494 if (fingerprintWasStarted) { 495 viewModel.ensureFingerprintHasStarted(isDelayed = false) 496 viewModel.showAuthenticating(modalities.asDefaultHelpMessage(applicationContext)) 497 } else { 498 viewModel.showAuthenticating() 499 } 500 } 501 502 override fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int) { 503 applicationScope.launch { 504 val authenticatedModality = modality.asBiometricModality() 505 val msgId = getHelpForSuccessfulAuthentication(authenticatedModality) 506 viewModel.showAuthenticated( 507 modality = authenticatedModality, 508 dismissAfterDelay = 500, 509 helpMessage = if (msgId != null) applicationContext.getString(msgId) else "" 510 ) 511 } 512 } 513 514 private suspend fun getHelpForSuccessfulAuthentication( 515 authenticatedModality: BiometricModality, 516 ): Int? = 517 when { 518 // for coex, show a message when face succeeds after fingerprint has also started 519 modalities.hasFaceAndFingerprint && 520 (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) && 521 (authenticatedModality == BiometricModality.Face) -> 522 R.string.biometric_dialog_tap_confirm_with_face_alt_1 523 else -> null 524 } 525 526 override fun onAuthenticationFailed( 527 @BiometricAuthenticator.Modality modality: Int, 528 failureReason: String, 529 ) { 530 val failedModality = modality.asBiometricModality() 531 viewModel.ensureFingerprintHasStarted(isDelayed = true) 532 533 applicationScope.launch { 534 viewModel.showTemporaryError( 535 failureReason, 536 messageAfterError = modalities.asDefaultHelpMessage(applicationContext), 537 authenticateAfterError = modalities.hasFingerprint, 538 suppressIf = { currentMessage, history -> 539 modalities.hasFaceAndFingerprint && 540 failedModality == BiometricModality.Face && 541 (currentMessage.isError || history.faceFailed) 542 }, 543 failedModality = failedModality, 544 ) 545 } 546 } 547 548 override fun onError(modality: Int, error: String) { 549 val errorModality = modality.asBiometricModality() 550 if (ignoreUnsuccessfulEventsFrom(errorModality, error)) { 551 return 552 } 553 554 applicationScope.launch { 555 viewModel.showTemporaryError( 556 error, 557 messageAfterError = modalities.asDefaultHelpMessage(applicationContext), 558 authenticateAfterError = modalities.hasFingerprint, 559 ) 560 delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong()) 561 legacyCallback?.onAction(Callback.ACTION_ERROR) 562 } 563 } 564 565 override fun onHelp(modality: Int, help: String) { 566 if (ignoreUnsuccessfulEventsFrom(modality.asBiometricModality(), "")) { 567 return 568 } 569 570 applicationScope.launch { 571 // help messages from the HAL should be displayed as temporary (i.e. soft) errors 572 viewModel.showTemporaryError( 573 help, 574 messageAfterError = modalities.asDefaultHelpMessage(applicationContext), 575 authenticateAfterError = modalities.hasFingerprint, 576 hapticFeedback = false, 577 ) 578 } 579 } 580 581 private fun ignoreUnsuccessfulEventsFrom(modality: BiometricModality, message: String) = 582 when { 583 modalities.hasFaceAndFingerprint -> 584 (modality == BiometricModality.Face) && 585 !(modalities.isFaceStrong && lockoutErrorStrings.contains(message)) 586 else -> false 587 } 588 589 override fun startTransitionToCredentialUI(isError: Boolean) { 590 applicationScope.launch { 591 viewModel.onSwitchToCredential() 592 legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL) 593 } 594 } 595 596 override fun requestLayout() { 597 // nothing, for legacy view... 598 } 599 600 override fun restoreState(bundle: Bundle?) { 601 // nothing, for legacy view... 602 } 603 604 override fun onSaveState(bundle: Bundle?) { 605 // nothing, for legacy view... 606 } 607 608 override fun onOrientationChanged() { 609 // nothing, for legacy view... 610 } 611 612 override fun cancelAnimation() { 613 view.animate()?.cancel() 614 } 615 616 override fun isCoex() = modalities.hasFaceAndFingerprint 617 618 override fun asView() = view 619 } 620 621 private fun BiometricModalities.asDefaultHelpMessage(context: Context): String = 622 when { 623 hasFingerprint -> context.getString(R.string.fingerprint_dialog_touch_sensor) 624 else -> "" 625 } 626 627 private fun BiometricModalities.asIconController( 628 context: Context, 629 iconView: LottieAnimationView, 630 iconViewOverlay: LottieAnimationView, 631 ): AuthIconController = 632 when { 633 hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay) 634 hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) 635 hasFace -> AuthBiometricFaceIconController(context, iconView) 636 else -> throw IllegalStateException("unexpected view type :$this") 637 } 638 639 private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE 640 641 private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE 642 643 // TODO(b/251476085): proper type? 644 typealias BiometricJankListener = Animator.AnimatorListener 645 646 // TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced 647 private class HackyCoexIconController( 648 context: Context, 649 iconView: LottieAnimationView, 650 iconViewOverlay: LottieAnimationView, 651 ) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) { 652 653 private var state: Int? = null 654 private val faceController = AuthBiometricFaceIconController(context, iconView) 655 656 var faceMode: Boolean = true 657 set(value) { 658 if (field != value) { 659 field = value 660 661 faceController.deactivated = !value 662 iconView.setImageIcon(null) 663 iconViewOverlay.setImageIcon(null) 664 state?.let { updateIcon(AuthBiometricView.STATE_IDLE, it) } 665 } 666 } 667 668 override fun updateIcon(lastState: Int, newState: Int) { 669 if (deactivated) { 670 return 671 } 672 673 if (faceMode) { 674 faceController.updateIcon(lastState, newState) 675 } else { 676 super.updateIcon(lastState, newState) 677 } 678 679 state = newState 680 } 681 } 682