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.view.HapticFeedbackConstants 26 import android.view.InputDevice 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.View.GONE 30 import android.view.View.INVISIBLE 31 import android.view.View.OnLayoutChangeListener 32 import android.view.View.VISIBLE 33 import android.view.ViewGroup 34 import android.view.ViewGroup.OnHierarchyChangeListener 35 import android.view.WindowInsets 36 import androidx.activity.OnBackPressedDispatcher 37 import androidx.activity.OnBackPressedDispatcherOwner 38 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner 39 import androidx.lifecycle.Lifecycle 40 import androidx.lifecycle.repeatOnLifecycle 41 import com.android.app.tracing.coroutines.launchTraced as launch 42 import com.android.keyguard.AuthInteractionProperties 43 import com.android.systemui.Flags 44 import com.android.systemui.Flags.msdlFeedback 45 import com.android.systemui.common.shared.model.Icon 46 import com.android.systemui.common.shared.model.Text 47 import com.android.systemui.common.shared.model.TintedIcon 48 import com.android.systemui.common.ui.ConfigurationState 49 import com.android.systemui.common.ui.view.onApplyWindowInsets 50 import com.android.systemui.common.ui.view.onLayoutChanged 51 import com.android.systemui.common.ui.view.onTouchListener 52 import com.android.systemui.customization.R as customR 53 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor 54 import com.android.systemui.keyguard.shared.model.KeyguardState 55 import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection 56 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters 57 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel 58 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel 59 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel 60 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel 61 import com.android.systemui.keyguard.ui.viewmodel.TransitionData 62 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor 63 import com.android.systemui.lifecycle.repeatWhenAttached 64 import com.android.systemui.log.LogBuffer 65 import com.android.systemui.log.core.Logger 66 import com.android.systemui.log.dagger.KeyguardBlueprintLog 67 import com.android.systemui.plugins.FalsingManager 68 import com.android.systemui.res.R 69 import com.android.systemui.scene.shared.flag.SceneContainerFlag 70 import com.android.systemui.shade.domain.interactor.ShadeInteractor 71 import com.android.systemui.shared.R as sharedR 72 import com.android.systemui.statusbar.CrossFadeHelper 73 import com.android.systemui.statusbar.VibratorHelper 74 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager 75 import com.android.systemui.temporarydisplay.ViewPriority 76 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator 77 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo 78 import com.android.systemui.util.kotlin.DisposableHandles 79 import com.android.systemui.util.ui.AnimatedValue 80 import com.android.systemui.util.ui.isAnimating 81 import com.android.systemui.util.ui.stopAnimating 82 import com.android.systemui.util.ui.value 83 import com.android.systemui.wallpapers.ui.viewmodel.WallpaperFocalAreaViewModel 84 import com.google.android.msdl.data.model.MSDLToken 85 import com.google.android.msdl.domain.MSDLPlayer 86 import kotlin.math.min 87 import kotlinx.coroutines.CoroutineDispatcher 88 import kotlinx.coroutines.DisposableHandle 89 import kotlinx.coroutines.flow.MutableStateFlow 90 import kotlinx.coroutines.flow.stateIn 91 import kotlinx.coroutines.flow.update 92 93 /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ 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 shadeInteractor: ShadeInteractor, 105 smartspaceViewModel: KeyguardSmartspaceViewModel, 106 deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, 107 vibratorHelper: VibratorHelper?, 108 falsingManager: FalsingManager?, 109 statusBarKeyguardViewManager: StatusBarKeyguardViewManager?, 110 mainImmediateDispatcher: CoroutineDispatcher, 111 msdlPlayer: MSDLPlayer?, 112 @KeyguardBlueprintLog blueprintLog: LogBuffer, 113 wallpaperFocalAreaViewModel: WallpaperFocalAreaViewModel, 114 ): DisposableHandle { 115 val disposables = DisposableHandles() 116 val childViews = mutableMapOf<Int, View>() 117 118 disposables += 119 view.onTouchListener { _, event -> 120 var consumed = false 121 if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { 122 // signifies a primary button click down has reached keyguardrootview 123 // we need to return true here otherwise an ACTION_UP will never arrive 124 if (Flags.nonTouchscreenDevicesBypassFalsing()) { 125 if ( 126 event.action == MotionEvent.ACTION_DOWN && 127 event.buttonState == MotionEvent.BUTTON_PRIMARY && 128 !event.isTouchscreenSource() 129 ) { 130 consumed = true 131 } else if ( 132 event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource() 133 ) { 134 statusBarKeyguardViewManager?.showBouncer( 135 true, 136 "KeyguardRootViewBinder: click on lockscreen", 137 ) 138 consumed = true 139 } 140 } 141 viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) 142 } 143 consumed 144 } 145 146 val burnInParams = MutableStateFlow(BurnInParameters()) 147 val viewState = ViewStateAccessor(alpha = { view.alpha }) 148 149 disposables += 150 view.repeatWhenAttached(mainImmediateDispatcher) { 151 repeatOnLifecycle(Lifecycle.State.CREATED) { 152 launch("$TAG#topClippingBounds") { 153 val clipBounds = Rect() 154 viewModel.topClippingBounds.collect { clipTop -> 155 if (clipTop == null) { 156 view.setClipBounds(null) 157 } else { 158 clipBounds.apply { 159 top = clipTop 160 left = view.getLeft() 161 right = view.getRight() 162 bottom = view.getBottom() 163 } 164 view.setClipBounds(clipBounds) 165 } 166 } 167 } 168 169 launch("$TAG#alpha") { 170 viewModel.alpha(viewState).collect { alpha -> 171 view.alpha = alpha 172 childViews[burnInLayerId]?.alpha = alpha 173 } 174 } 175 176 launch("$TAG#zoomOut") { 177 viewModel.scaleFromZoomOut.collect { scaleFromZoomOut -> 178 view.scaleX = scaleFromZoomOut 179 view.scaleY = scaleFromZoomOut 180 } 181 } 182 183 launch("$TAG#translationY") { 184 // When translation happens in burnInLayer, it won't be weather clock large 185 // clock isn't added to burnInLayer due to its scale transition so we also 186 // need to add translation to it here same as translationX 187 viewModel.translationY.collect { y -> 188 childViews[burnInLayerId]?.translationY = y 189 childViews[largeClockId]?.translationY = y 190 if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { 191 childViews[largeClockDateId]?.translationY = y 192 } 193 childViews[aodPromotedNotificationId]?.translationY = y 194 childViews[aodNotificationIconContainerId]?.translationY = y 195 } 196 } 197 198 launch("$TAG#translationX") { 199 viewModel.translationX.collect { state -> 200 val px = state.value ?: return@collect 201 when { 202 state.isToOrFrom(KeyguardState.AOD) -> { 203 // Large Clock is not translated in the x direction 204 childViews[burnInLayerId]?.translationX = px 205 childViews[aodPromotedNotificationId]?.translationX = px 206 childViews[aodNotificationIconContainerId]?.translationX = px 207 } 208 209 state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { 210 for ((key, childView) in childViews.entries) { 211 when (key) { 212 indicationArea, 213 startButton, 214 endButton, 215 deviceEntryIcon -> { 216 // Do not move these views 217 } 218 219 else -> childView.translationX = px 220 } 221 } 222 } 223 } 224 } 225 } 226 } 227 } 228 disposables += 229 view.repeatWhenAttached { 230 repeatOnLifecycle(Lifecycle.State.CREATED) { 231 if (SceneContainerFlag.isEnabled) { 232 view.setViewTreeOnBackPressedDispatcherOwner( 233 object : OnBackPressedDispatcherOwner { 234 override val onBackPressedDispatcher = 235 OnBackPressedDispatcher().apply { 236 setOnBackInvokedDispatcher( 237 view.viewRootImpl.onBackInvokedDispatcher 238 ) 239 } 240 241 override val lifecycle: Lifecycle = 242 this@repeatWhenAttached.lifecycle 243 } 244 ) 245 } 246 launch { 247 occludingAppDeviceEntryMessageViewModel?.message?.collect { biometricMessage 248 -> 249 if (biometricMessage?.message != null) { 250 chipbarCoordinator!!.displayView( 251 createChipbarInfo(biometricMessage.message, R.drawable.ic_lock) 252 ) 253 } else { 254 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull") 255 } 256 } 257 } 258 259 launch { 260 viewModel.burnInLayerVisibility.collect { visibility -> 261 childViews[burnInLayerId]?.visibility = visibility 262 } 263 } 264 265 launch { 266 viewModel.scale.collect { scaleViewModel -> 267 if (scaleViewModel.scaleClockOnly) { 268 // For clocks except weather clock, we have scale transition besides 269 // translate 270 childViews[largeClockId]?.let { 271 it.scaleX = scaleViewModel.scale 272 it.scaleY = scaleViewModel.scale 273 } 274 } 275 } 276 } 277 278 launch { 279 blueprintViewModel.currentTransition.collect { currentTransition -> 280 // When blueprint/clock transitions end (null), make sure NSSL is in the 281 // right place 282 if (currentTransition == null) { 283 childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> 284 viewModel.onNotificationContainerBoundsChanged( 285 notificationListPlaceholder.top.toFloat(), 286 notificationListPlaceholder.bottom.toFloat(), 287 animate = true, 288 ) 289 } 290 } 291 } 292 } 293 294 launch { 295 val iconsAppearTranslationPx = 296 configuration 297 .getDimensionPixelSize(R.dimen.shelf_appear_translation) 298 .stateIn(this) 299 viewModel.isNotifIconContainerVisible.collect { isVisible -> 300 if (isVisible.value) { 301 blueprintViewModel.refreshBlueprint() 302 } 303 childViews[aodNotificationIconContainerId] 304 ?.setAodNotifIconContainerIsVisible(isVisible) 305 } 306 } 307 308 launch { 309 viewModel.isAodPromotedNotifVisible.collect { isVisible -> 310 if (isVisible.value) { 311 blueprintViewModel.refreshBlueprint() 312 } 313 childViews[aodPromotedNotificationId]?.setAodPromotedNotifIsVisible( 314 isVisible 315 ) 316 } 317 } 318 319 launch { 320 shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded -> 321 view.visibility = 322 if (isFullyAnyExpanded) { 323 INVISIBLE 324 } else { 325 VISIBLE 326 } 327 } 328 } 329 330 launch { burnInParams.collect { viewModel.updateBurnInParams(it) } } 331 332 if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { 333 launch { 334 deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect { 335 if (msdlFeedback()) { 336 msdlPlayer?.playToken( 337 MSDLToken.UNLOCK, 338 authInteractionProperties, 339 ) 340 } else { 341 vibratorHelper.performHapticFeedback( 342 view, 343 HapticFeedbackConstants.BIOMETRIC_CONFIRM, 344 ) 345 } 346 } 347 } 348 349 launch { 350 deviceEntryHapticsInteractor.playErrorHaptic.collect { 351 if (msdlFeedback()) { 352 msdlPlayer?.playToken( 353 MSDLToken.FAILURE, 354 authInteractionProperties, 355 ) 356 } else { 357 vibratorHelper.performHapticFeedback( 358 view, 359 HapticFeedbackConstants.BIOMETRIC_REJECT, 360 ) 361 } 362 } 363 } 364 } 365 } 366 } 367 368 burnInParams.update { current -> 369 current.copy( 370 translationX = { childViews[burnInLayerId]?.translationX }, 371 translationY = { childViews[burnInLayerId]?.translationY }, 372 ) 373 } 374 375 disposables += 376 view.repeatWhenAttached { 377 repeatOnLifecycle(Lifecycle.State.STARTED) { 378 if (wallpaperFocalAreaViewModel.hasFocalArea.value) { 379 launch { 380 wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect { 381 wallpaperFocalAreaViewModel.setFocalAreaBounds(it) 382 } 383 } 384 } 385 } 386 } 387 388 disposables += 389 view.onLayoutChanged( 390 OnLayoutChange( 391 viewModel, 392 blueprintViewModel, 393 smartspaceViewModel, 394 childViews, 395 burnInParams, 396 Logger(blueprintLog, TAG), 397 ) 398 ) 399 400 // Views will be added or removed after the call to bind(). This is needed to avoid many 401 // calls to findViewById 402 view.setOnHierarchyChangeListener( 403 object : OnHierarchyChangeListener { 404 override fun onChildViewAdded(parent: View, child: View) { 405 childViews.put(child.id, child) 406 } 407 408 override fun onChildViewRemoved(parent: View, child: View) { 409 childViews.remove(child.id) 410 } 411 } 412 ) 413 disposables += DisposableHandle { 414 view.setOnHierarchyChangeListener(null) 415 childViews.clear() 416 } 417 418 disposables += 419 view.onApplyWindowInsets { _: View, insets: WindowInsets -> 420 val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() 421 burnInParams.update { current -> 422 current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) 423 } 424 insets 425 } 426 427 return disposables 428 } 429 430 /** 431 * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. 432 */ 433 private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo { 434 return ChipbarInfo( 435 startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT), 436 text = Text.Loaded(message), 437 endItem = null, 438 vibrationEffect = null, 439 windowTitle = "OccludingAppUnlockMsgChip", 440 wakeReason = "OCCLUDING_APP_UNLOCK_MSG_CHIP", 441 timeoutMs = 3500, 442 id = ID, 443 priority = ViewPriority.CRITICAL, 444 instanceId = null, 445 ) 446 } 447 448 private class OnLayoutChange( 449 private val viewModel: KeyguardRootViewModel, 450 private val blueprintViewModel: KeyguardBlueprintViewModel, 451 private val smartspaceViewModel: KeyguardSmartspaceViewModel, 452 private val childViews: Map<Int, View>, 453 private val burnInParams: MutableStateFlow<BurnInParameters>, 454 private val logger: Logger, 455 ) : OnLayoutChangeListener { 456 var prevTransition: TransitionData? = null 457 458 override fun onLayoutChange( 459 view: View, 460 left: Int, 461 top: Int, 462 right: Int, 463 bottom: Int, 464 oldLeft: Int, 465 oldTop: Int, 466 oldRight: Int, 467 oldBottom: Int, 468 ) { 469 val prevSmartspaceVisibility = smartspaceViewModel.bcSmartspaceVisibility.value 470 val smartspaceVisibility = childViews[bcSmartspaceId]?.visibility ?: GONE 471 val smartspaceVisibilityChanged = prevSmartspaceVisibility != smartspaceVisibility 472 473 // After layout, ensure the notifications are positioned correctly 474 childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> 475 // Do not update a second time while a blueprint transition is running 476 val transition = blueprintViewModel.currentTransition.value 477 val shouldAnimate = transition != null && transition.config.type.animateNotifChanges 478 if (prevTransition == transition && shouldAnimate && !smartspaceVisibilityChanged) { 479 logger.w("Skipping onNotificationContainerBoundsChanged during transition") 480 return 481 } 482 483 prevTransition = transition 484 viewModel.onNotificationContainerBoundsChanged( 485 notificationListPlaceholder.top.toFloat(), 486 notificationListPlaceholder.bottom.toFloat(), 487 animate = (shouldAnimate || smartspaceVisibilityChanged), 488 ) 489 } 490 491 burnInParams.update { current -> 492 current.copy( 493 minViewY = 494 // To ensure burn-in doesn't enroach the top inset, get the min top Y 495 childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> 496 min( 497 currentMin, 498 if (!isUserVisible(view)) { 499 Int.MAX_VALUE 500 } else { 501 view.getTop() 502 }, 503 ) 504 } 505 ) 506 } 507 } 508 509 private fun isUserVisible(view: View): Boolean { 510 return view.id != burnInLayerId && 511 view.visibility == VISIBLE && 512 view.width > 0 && 513 view.height > 0 514 } 515 } 516 517 private fun View.setAodNotifIconContainerIsVisible(isVisible: AnimatedValue<Boolean>) { 518 animate().cancel() 519 val animatorListener = 520 object : AnimatorListenerAdapter() { 521 override fun onAnimationEnd(animation: Animator) { 522 isVisible.stopAnimating() 523 } 524 } 525 when { 526 !isVisible.isAnimating -> { 527 visibility = 528 if (isVisible.value) { 529 alpha = 1f 530 VISIBLE 531 } else { 532 alpha = 0f 533 INVISIBLE 534 } 535 } 536 537 else -> { 538 if (isVisible.value) { 539 CrossFadeHelper.fadeIn(this, animatorListener) 540 } else { 541 CrossFadeHelper.fadeOut(this, animatorListener) 542 } 543 } 544 } 545 } 546 547 private fun View.setAodPromotedNotifIsVisible(isVisible: AnimatedValue<Boolean>) { 548 animate().cancel() 549 val animatorListener = 550 object : AnimatorListenerAdapter() { 551 override fun onAnimationEnd(animation: Animator) { 552 isVisible.stopAnimating() 553 } 554 } 555 556 if (isVisible.isAnimating) { 557 if (isVisible.value) { 558 alpha = 0f 559 visibility = VISIBLE 560 CrossFadeHelper.fadeIn(this, animatorListener) 561 } else { 562 CrossFadeHelper.fadeOut(this, animatorListener) 563 } 564 } else { 565 if (isVisible.value) { 566 alpha = 1f 567 visibility = VISIBLE 568 } else { 569 // Hide with GONE, not INVISIBLE, so there won't be a redundant bottom 570 // margin between the smart space and the shelf. 571 alpha = 0f 572 visibility = GONE 573 } 574 } 575 } 576 577 private fun MotionEvent.isTouchscreenSource(): Boolean { 578 return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true 579 } 580 581 private val burnInLayerId = R.id.burn_in_layer 582 private val aodPromotedNotificationId = AodPromotedNotificationSection.viewId 583 private val aodNotificationIconContainerId = R.id.aod_notification_icon_container 584 private val largeClockId = customR.id.lockscreen_clock_view_large 585 private val largeClockDateId = sharedR.id.date_smartspace_view_large 586 private val largeClockWeatherId = sharedR.id.weather_smartspace_view_large 587 private val bcSmartspaceId = sharedR.id.bc_smartspace_view 588 private val smallClockId = customR.id.lockscreen_clock_view 589 private val indicationArea = R.id.keyguard_indication_area 590 private val startButton = R.id.start_button 591 private val endButton = R.id.end_button 592 private val deviceEntryIcon = R.id.device_entry_icon_view 593 private val nsslPlaceholderId = R.id.nssl_placeholder 594 private val authInteractionProperties = AuthInteractionProperties() 595 596 private const val ID = "occluding_app_device_entry_unlock_msg" 597 private const val AOD_ICONS_APPEAR_DURATION: Long = 200 598 private const val TAG = "KeyguardRootViewBinder" 599 } 600