1 /* <lambda>null2 * 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.keyguard.ui.binder 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.annotation.DrawableRes 22 import android.annotation.SuppressLint 23 import android.graphics.Point 24 import android.graphics.Rect 25 import android.util.Log 26 import android.view.HapticFeedbackConstants 27 import android.view.View 28 import android.view.View.OnLayoutChangeListener 29 import android.view.View.VISIBLE 30 import android.view.ViewGroup 31 import android.view.ViewGroup.OnHierarchyChangeListener 32 import android.view.ViewPropertyAnimator 33 import android.view.WindowInsets 34 import androidx.activity.OnBackPressedDispatcher 35 import androidx.activity.OnBackPressedDispatcherOwner 36 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner 37 import androidx.lifecycle.Lifecycle 38 import androidx.lifecycle.repeatOnLifecycle 39 import com.android.app.animation.Interpolators 40 import com.android.internal.jank.InteractionJankMonitor 41 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD 42 import com.android.systemui.Flags.newAodTransition 43 import com.android.systemui.common.shared.model.Icon 44 import com.android.systemui.common.shared.model.Text 45 import com.android.systemui.common.shared.model.TintedIcon 46 import com.android.systemui.common.ui.ConfigurationState 47 import com.android.systemui.common.ui.view.onApplyWindowInsets 48 import com.android.systemui.common.ui.view.onLayoutChanged 49 import com.android.systemui.common.ui.view.onTouchListener 50 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor 51 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor 52 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor 53 import com.android.systemui.keyguard.KeyguardViewMediator 54 import com.android.systemui.keyguard.MigrateClocksToBlueprint 55 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor 56 import com.android.systemui.keyguard.shared.ComposeLockscreen 57 import com.android.systemui.keyguard.shared.model.KeyguardState 58 import com.android.systemui.keyguard.shared.model.TransitionState 59 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters 60 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel 61 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel 62 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel 63 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel 64 import com.android.systemui.keyguard.ui.viewmodel.TransitionData 65 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor 66 import com.android.systemui.lifecycle.repeatWhenAttached 67 import com.android.systemui.plugins.FalsingManager 68 import com.android.systemui.res.R 69 import com.android.systemui.shade.domain.interactor.ShadeInteractor 70 import com.android.systemui.statusbar.CrossFadeHelper 71 import com.android.systemui.statusbar.VibratorHelper 72 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor 73 import com.android.systemui.statusbar.phone.ScreenOffAnimationController 74 import com.android.systemui.temporarydisplay.ViewPriority 75 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator 76 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo 77 import com.android.systemui.util.kotlin.DisposableHandles 78 import com.android.systemui.util.ui.AnimatedValue 79 import com.android.systemui.util.ui.isAnimating 80 import com.android.systemui.util.ui.stopAnimating 81 import com.android.systemui.util.ui.value 82 import kotlin.math.min 83 import kotlinx.coroutines.DisposableHandle 84 import kotlinx.coroutines.ExperimentalCoroutinesApi 85 import kotlinx.coroutines.coroutineScope 86 import kotlinx.coroutines.flow.Flow 87 import kotlinx.coroutines.flow.MutableStateFlow 88 import kotlinx.coroutines.flow.stateIn 89 import kotlinx.coroutines.flow.update 90 import kotlinx.coroutines.launch 91 92 /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ 93 @OptIn(ExperimentalCoroutinesApi::class) 94 object KeyguardRootViewBinder { 95 @SuppressLint("ClickableViewAccessibility") 96 @JvmStatic 97 fun bind( 98 view: ViewGroup, 99 viewModel: KeyguardRootViewModel, 100 blueprintViewModel: KeyguardBlueprintViewModel, 101 configuration: ConfigurationState, 102 occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?, 103 chipbarCoordinator: ChipbarCoordinator?, 104 screenOffAnimationController: ScreenOffAnimationController, 105 shadeInteractor: ShadeInteractor, 106 clockInteractor: KeyguardClockInteractor, 107 clockViewModel: KeyguardClockViewModel, 108 interactionJankMonitor: InteractionJankMonitor?, 109 deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, 110 vibratorHelper: VibratorHelper?, 111 falsingManager: FalsingManager?, 112 keyguardViewMediator: KeyguardViewMediator?, 113 ): DisposableHandle { 114 val disposables = DisposableHandles() 115 val childViews = mutableMapOf<Int, View>() 116 117 if (KeyguardBottomAreaRefactor.isEnabled) { 118 disposables += 119 view.onTouchListener { _, event -> 120 if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { 121 viewModel.setRootViewLastTapPosition( 122 Point(event.x.toInt(), event.y.toInt()) 123 ) 124 } 125 false 126 } 127 } 128 129 val burnInParams = MutableStateFlow(BurnInParameters()) 130 val viewState = ViewStateAccessor(alpha = { view.alpha }) 131 disposables += 132 view.repeatWhenAttached { 133 repeatOnLifecycle(Lifecycle.State.CREATED) { 134 if (ComposeLockscreen.isEnabled) { 135 view.setViewTreeOnBackPressedDispatcherOwner( 136 object : OnBackPressedDispatcherOwner { 137 override val onBackPressedDispatcher = 138 OnBackPressedDispatcher().apply { 139 setOnBackInvokedDispatcher( 140 view.viewRootImpl.onBackInvokedDispatcher 141 ) 142 } 143 144 override val lifecycle: Lifecycle = 145 this@repeatWhenAttached.lifecycle 146 } 147 ) 148 } 149 launch { 150 occludingAppDeviceEntryMessageViewModel?.message?.collect { biometricMessage 151 -> 152 if (biometricMessage?.message != null) { 153 chipbarCoordinator!!.displayView( 154 createChipbarInfo( 155 biometricMessage.message, 156 R.drawable.ic_lock, 157 ) 158 ) 159 } else { 160 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull") 161 } 162 } 163 } 164 165 if ( 166 KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled 167 ) { 168 launch { 169 viewModel.alpha(viewState).collect { alpha -> 170 view.alpha = alpha 171 if (KeyguardBottomAreaRefactor.isEnabled) { 172 childViews[statusViewId]?.alpha = alpha 173 childViews[burnInLayerId]?.alpha = alpha 174 } 175 } 176 } 177 } 178 179 if (MigrateClocksToBlueprint.isEnabled) { 180 launch { 181 viewModel.burnInLayerVisibility.collect { visibility -> 182 childViews[burnInLayerId]?.visibility = visibility 183 childViews[aodNotificationIconContainerId]?.visibility = visibility 184 } 185 } 186 187 launch { 188 viewModel.burnInLayerAlpha.collect { alpha -> 189 childViews[statusViewId]?.alpha = alpha 190 childViews[aodNotificationIconContainerId]?.alpha = alpha 191 } 192 } 193 194 launch { 195 val clipBounds = Rect() 196 viewModel.topClippingBounds.collect { clipTop -> 197 if (clipTop == null) { 198 view.setClipBounds(null) 199 } else { 200 clipBounds.apply { 201 top = clipTop 202 left = view.getLeft() 203 right = view.getRight() 204 bottom = view.getBottom() 205 } 206 view.setClipBounds(clipBounds) 207 } 208 } 209 } 210 211 launch { 212 viewModel.lockscreenStateAlpha(viewState).collect { alpha -> 213 childViews[statusViewId]?.alpha = alpha 214 } 215 } 216 217 launch { 218 // When translation happens in burnInLayer, it won't be weather clock 219 // large clock isn't added to burnInLayer due to its scale transition 220 // so we also need to add translation to it here 221 // same as translationX 222 viewModel.translationY.collect { y -> 223 childViews[burnInLayerId]?.translationY = y 224 childViews[largeClockId]?.translationY = y 225 childViews[aodNotificationIconContainerId]?.translationY = y 226 } 227 } 228 229 launch { 230 viewModel.translationX.collect { state -> 231 val px = state.value ?: return@collect 232 when { 233 state.isToOrFrom(KeyguardState.AOD) -> { 234 // Large Clock is not translated in the x direction 235 childViews[burnInLayerId]?.translationX = px 236 childViews[aodNotificationIconContainerId]?.translationX = 237 px 238 } 239 state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { 240 for ((key, childView) in childViews.entries) { 241 when (key) { 242 indicationArea, 243 startButton, 244 endButton, 245 lockIcon, 246 deviceEntryIcon -> { 247 // Do not move these views 248 } 249 else -> childView.translationX = px 250 } 251 } 252 } 253 } 254 } 255 } 256 257 launch { 258 viewModel.scale.collect { scaleViewModel -> 259 if (scaleViewModel.scaleClockOnly) { 260 // For clocks except weather clock, we have scale transition 261 // besides translate 262 childViews[largeClockId]?.let { 263 it.scaleX = scaleViewModel.scale 264 it.scaleY = scaleViewModel.scale 265 } 266 } 267 } 268 } 269 270 if (NotificationIconContainerRefactor.isEnabled) { 271 launch { 272 val iconsAppearTranslationPx = 273 configuration 274 .getDimensionPixelSize(R.dimen.shelf_appear_translation) 275 .stateIn(this) 276 viewModel.isNotifIconContainerVisible.collect { isVisible -> 277 childViews[aodNotificationIconContainerId] 278 ?.setAodNotifIconContainerIsVisible( 279 isVisible, 280 iconsAppearTranslationPx.value, 281 screenOffAnimationController, 282 ) 283 } 284 } 285 } 286 287 interactionJankMonitor?.let { jankMonitor -> 288 launch { 289 viewModel.goneToAodTransition.collect { 290 when (it.transitionState) { 291 TransitionState.STARTED -> { 292 val clockId = clockInteractor.renderedClockId 293 val builder = 294 InteractionJankMonitor.Configuration.Builder 295 .withView(CUJ_SCREEN_OFF_SHOW_AOD, view) 296 .setTag(clockId) 297 jankMonitor.begin(builder) 298 } 299 TransitionState.CANCELED -> 300 jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) 301 TransitionState.FINISHED -> { 302 if (MigrateClocksToBlueprint.isEnabled) { 303 keyguardViewMediator?.maybeHandlePendingLock() 304 } 305 jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) 306 } 307 TransitionState.RUNNING -> Unit 308 } 309 } 310 } 311 } 312 } 313 314 launch { 315 shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded -> 316 view.visibility = 317 if (isFullyAnyExpanded) { 318 View.INVISIBLE 319 } else { 320 View.VISIBLE 321 } 322 } 323 } 324 325 launch { burnInParams.collect { viewModel.updateBurnInParams(it) } } 326 327 if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { 328 launch { 329 deviceEntryHapticsInteractor.playSuccessHaptic.collect { 330 vibratorHelper.performHapticFeedback( 331 view, 332 HapticFeedbackConstants.CONFIRM, 333 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, 334 ) 335 } 336 } 337 338 launch { 339 deviceEntryHapticsInteractor.playErrorHaptic.collect { 340 vibratorHelper.performHapticFeedback( 341 view, 342 HapticFeedbackConstants.REJECT, 343 HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, 344 ) 345 } 346 } 347 } 348 } 349 } 350 351 if (MigrateClocksToBlueprint.isEnabled) { 352 burnInParams.update { current -> 353 current.copy(translationY = { childViews[burnInLayerId]?.translationY }) 354 } 355 } 356 357 disposables += 358 view.onLayoutChanged( 359 OnLayoutChange( 360 viewModel, 361 blueprintViewModel, 362 clockViewModel, 363 childViews, 364 burnInParams 365 ) 366 ) 367 368 // Views will be added or removed after the call to bind(). This is needed to avoid many 369 // calls to findViewById 370 view.setOnHierarchyChangeListener( 371 object : OnHierarchyChangeListener { 372 override fun onChildViewAdded(parent: View, child: View) { 373 childViews.put(child.id, child) 374 } 375 376 override fun onChildViewRemoved(parent: View, child: View) { 377 childViews.remove(child.id) 378 } 379 } 380 ) 381 disposables += DisposableHandle { 382 view.setOnHierarchyChangeListener(null) 383 childViews.clear() 384 } 385 386 disposables += 387 view.onApplyWindowInsets { _: View, insets: WindowInsets -> 388 val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() 389 burnInParams.update { current -> 390 current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) 391 } 392 insets 393 } 394 395 return disposables 396 } 397 398 /** 399 * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. 400 */ 401 private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo { 402 return ChipbarInfo( 403 startIcon = 404 TintedIcon( 405 Icon.Resource(icon, null), 406 ChipbarInfo.DEFAULT_ICON_TINT, 407 ), 408 text = Text.Loaded(message), 409 endItem = null, 410 vibrationEffect = null, 411 windowTitle = "OccludingAppUnlockMsgChip", 412 wakeReason = "OCCLUDING_APP_UNLOCK_MSG_CHIP", 413 timeoutMs = 3500, 414 id = ID, 415 priority = ViewPriority.CRITICAL, 416 instanceId = null, 417 ) 418 } 419 420 private class OnLayoutChange( 421 private val viewModel: KeyguardRootViewModel, 422 private val blueprintViewModel: KeyguardBlueprintViewModel, 423 private val clockViewModel: KeyguardClockViewModel, 424 private val childViews: Map<Int, View>, 425 private val burnInParams: MutableStateFlow<BurnInParameters>, 426 ) : OnLayoutChangeListener { 427 var prevTransition: TransitionData? = null 428 429 override fun onLayoutChange( 430 view: View, 431 left: Int, 432 top: Int, 433 right: Int, 434 bottom: Int, 435 oldLeft: Int, 436 oldTop: Int, 437 oldRight: Int, 438 oldBottom: Int 439 ) { 440 // After layout, ensure the notifications are positioned correctly 441 childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> 442 // Do not update a second time while a blueprint transition is running 443 val transition = blueprintViewModel.currentTransition.value 444 val shouldAnimate = transition != null && transition.config.type.animateNotifChanges 445 if (prevTransition == transition && shouldAnimate) { 446 if (DEBUG) Log.w(TAG, "Skipping; layout during transition") 447 return 448 } 449 450 prevTransition = transition 451 viewModel.onNotificationContainerBoundsChanged( 452 notificationListPlaceholder.top.toFloat(), 453 notificationListPlaceholder.bottom.toFloat(), 454 animate = shouldAnimate 455 ) 456 } 457 458 burnInParams.update { current -> 459 current.copy( 460 minViewY = 461 if (MigrateClocksToBlueprint.isEnabled) { 462 // To ensure burn-in doesn't enroach the top inset, get the min top Y 463 childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> 464 min( 465 currentMin, 466 if (!isUserVisible(view)) { 467 Int.MAX_VALUE 468 } else { 469 view.getTop() 470 } 471 ) 472 } 473 } else { 474 childViews[statusViewId]?.top ?: 0 475 } 476 ) 477 } 478 } 479 480 private fun isUserVisible(view: View): Boolean { 481 return view.id != burnInLayerId && 482 view.visibility == VISIBLE && 483 view.width > 0 && 484 view.height > 0 485 } 486 } 487 488 suspend fun bindAodNotifIconVisibility( 489 view: View, 490 isVisible: Flow<AnimatedValue<Boolean>>, 491 configuration: ConfigurationState, 492 screenOffAnimationController: ScreenOffAnimationController, 493 ) { 494 if (MigrateClocksToBlueprint.isEnabled) { 495 throw IllegalStateException("should only be called in legacy code paths") 496 } 497 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return 498 coroutineScope { 499 val iconAppearTranslationPx = 500 configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this) 501 isVisible.collect { isVisible -> 502 view.setAodNotifIconContainerIsVisible( 503 isVisible = isVisible, 504 iconsAppearTranslationPx = iconAppearTranslationPx.value, 505 screenOffAnimationController = screenOffAnimationController, 506 ) 507 } 508 } 509 } 510 511 private fun View.setAodNotifIconContainerIsVisible( 512 isVisible: AnimatedValue<Boolean>, 513 iconsAppearTranslationPx: Int, 514 screenOffAnimationController: ScreenOffAnimationController, 515 ) { 516 animate().cancel() 517 val animatorListener = 518 object : AnimatorListenerAdapter() { 519 override fun onAnimationEnd(animation: Animator) { 520 isVisible.stopAnimating() 521 } 522 } 523 when { 524 !isVisible.isAnimating -> { 525 if (!MigrateClocksToBlueprint.isEnabled) { 526 translationY = 0f 527 } 528 visibility = 529 if (isVisible.value) { 530 alpha = 1f 531 View.VISIBLE 532 } else { 533 alpha = 0f 534 View.INVISIBLE 535 } 536 } 537 newAodTransition() -> { 538 animateInIconTranslation() 539 if (isVisible.value) { 540 CrossFadeHelper.fadeIn(this, animatorListener) 541 } else { 542 CrossFadeHelper.fadeOut(this, animatorListener) 543 } 544 } 545 !isVisible.value -> { 546 // Let's make sure the icon are translated to 0, since we cancelled it above 547 animateInIconTranslation() 548 CrossFadeHelper.fadeOut(this, animatorListener) 549 } 550 visibility != View.VISIBLE -> { 551 // No fading here, let's just appear the icons instead! 552 visibility = View.VISIBLE 553 alpha = 1f 554 appearIcons( 555 animate = screenOffAnimationController.shouldAnimateAodIcons(), 556 iconsAppearTranslationPx, 557 animatorListener, 558 ) 559 } 560 else -> { 561 // Let's make sure the icons are translated to 0, since we cancelled it above 562 animateInIconTranslation() 563 // We were fading out, let's fade in instead 564 CrossFadeHelper.fadeIn(this, animatorListener) 565 } 566 } 567 } 568 569 private fun View.appearIcons( 570 animate: Boolean, 571 iconAppearTranslation: Int, 572 animatorListener: Animator.AnimatorListener, 573 ) { 574 if (animate) { 575 if (!MigrateClocksToBlueprint.isEnabled) { 576 translationY = -iconAppearTranslation.toFloat() 577 } 578 alpha = 0f 579 animate() 580 .alpha(1f) 581 .setInterpolator(Interpolators.LINEAR) 582 .setDuration(AOD_ICONS_APPEAR_DURATION) 583 .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() } 584 .setListener(animatorListener) 585 .start() 586 } else { 587 alpha = 1.0f 588 if (!MigrateClocksToBlueprint.isEnabled) { 589 translationY = 0f 590 } 591 } 592 } 593 594 private fun View.animateInIconTranslation() { 595 if (!MigrateClocksToBlueprint.isEnabled) { 596 animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() 597 } 598 } 599 600 private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator = 601 setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f) 602 603 private val statusViewId = R.id.keyguard_status_view 604 private val burnInLayerId = R.id.burn_in_layer 605 private val aodNotificationIconContainerId = R.id.aod_notification_icon_container 606 private val largeClockId = R.id.lockscreen_clock_view_large 607 private val smallClockId = R.id.lockscreen_clock_view 608 private val indicationArea = R.id.keyguard_indication_area 609 private val startButton = R.id.start_button 610 private val endButton = R.id.end_button 611 private val lockIcon = R.id.lock_icon_view 612 private val deviceEntryIcon = R.id.device_entry_icon_view 613 private val nsslPlaceholderId = R.id.nssl_placeholder 614 615 private const val ID = "occluding_app_device_entry_unlock_msg" 616 private const val AOD_ICONS_APPEAR_DURATION: Long = 200 617 private const val TAG = "KeyguardRootViewBinder" 618 private const val DEBUG = false 619 } 620