1 /* <lambda>null2 * 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.keyguard.ui.binder 18 19 import android.annotation.SuppressLint 20 import android.graphics.Rect 21 import android.graphics.drawable.Animatable2 22 import android.util.Size 23 import android.view.View 24 import android.view.ViewGroup 25 import android.view.ViewGroup.MarginLayoutParams 26 import android.view.WindowInsets 27 import android.widget.ImageView 28 import androidx.core.animation.CycleInterpolator 29 import androidx.core.animation.ObjectAnimator 30 import androidx.core.view.isInvisible 31 import androidx.core.view.isVisible 32 import androidx.core.view.marginLeft 33 import androidx.core.view.marginRight 34 import androidx.core.view.marginTop 35 import androidx.core.view.updateLayoutParams 36 import androidx.lifecycle.Lifecycle 37 import androidx.lifecycle.repeatOnLifecycle 38 import com.android.app.animation.Interpolators 39 import com.android.app.tracing.coroutines.launch 40 import com.android.settingslib.Utils 41 import com.android.systemui.animation.ActivityTransitionAnimator 42 import com.android.systemui.animation.Expandable 43 import com.android.systemui.animation.view.LaunchableLinearLayout 44 import com.android.systemui.common.shared.model.Icon 45 import com.android.systemui.common.ui.binder.IconViewBinder 46 import com.android.systemui.common.ui.binder.TextViewBinder 47 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel 48 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel 49 import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils 50 import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD 51 import com.android.systemui.lifecycle.repeatWhenAttached 52 import com.android.systemui.plugins.ActivityStarter 53 import com.android.systemui.plugins.FalsingManager 54 import com.android.systemui.res.R 55 import com.android.systemui.statusbar.VibratorHelper 56 import com.android.systemui.util.doOnEnd 57 import kotlinx.coroutines.ExperimentalCoroutinesApi 58 import kotlinx.coroutines.flow.Flow 59 import kotlinx.coroutines.flow.MutableStateFlow 60 import kotlinx.coroutines.flow.combine 61 import kotlinx.coroutines.flow.distinctUntilChanged 62 import kotlinx.coroutines.flow.filter 63 import kotlinx.coroutines.flow.flatMapLatest 64 import kotlinx.coroutines.flow.map 65 66 /** 67 * Binds a keyguard bottom area view to its view-model. 68 * 69 * To use this properly, users should maintain a one-to-one relationship between the [View] and the 70 * view-binding, binding each view only once. It is okay and expected for the same instance of the 71 * view-model to be reused for multiple view/view-binder bindings. 72 */ 73 @OptIn(ExperimentalCoroutinesApi::class) 74 @Deprecated("Deprecated as part of b/278057014") 75 object KeyguardBottomAreaViewBinder { 76 77 private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L 78 private const val SCALE_SELECTED_BUTTON = 1.23f 79 private const val DIM_ALPHA = 0.3f 80 private const val TAG = "KeyguardBottomAreaViewBinder" 81 82 /** 83 * Defines interface for an object that acts as the binding between the view and its view-model. 84 * 85 * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after 86 * it is bound. 87 */ 88 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 89 @Deprecated("Deprecated as part of b/278057014") 90 interface Binding { 91 /** Notifies that device configuration has changed. */ 92 fun onConfigurationChanged() 93 94 /** 95 * Returns whether the keyguard bottom area should be constrained to the top of the lock 96 * icon 97 */ 98 fun shouldConstrainToTopOfLockIcon(): Boolean 99 100 /** Destroys this binding, releases resources, and cancels any coroutines. */ 101 fun destroy() 102 } 103 104 /** Binds the view to the view-model, continuing to update the former based on the latter. */ 105 @Deprecated("Deprecated as part of b/278057014") 106 @SuppressLint("ClickableViewAccessibility") 107 @JvmStatic 108 fun bind( 109 view: ViewGroup, 110 viewModel: KeyguardBottomAreaViewModel, 111 falsingManager: FalsingManager?, 112 vibratorHelper: VibratorHelper?, 113 activityStarter: ActivityStarter?, 114 messageDisplayer: (Int) -> Unit, 115 ): Binding { 116 val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container) 117 val startButton: ImageView = view.requireViewById(R.id.start_button) 118 val endButton: ImageView = view.requireViewById(R.id.end_button) 119 val overlayContainer: View = view.requireViewById(R.id.overlay_container) 120 val settingsMenu: LaunchableLinearLayout = 121 view.requireViewById(R.id.keyguard_settings_button) 122 123 startButton.setOnApplyWindowInsetsListener { inView, windowInsets -> 124 val bottomInset = windowInsets.displayCutout?.safeInsetBottom ?: 0 125 val marginBottom = 126 inView.resources.getDimension(R.dimen.keyguard_affordance_vertical_offset).toInt() 127 inView.layoutParams = 128 (inView.layoutParams as MarginLayoutParams).apply { 129 setMargins( 130 inView.marginLeft, 131 inView.marginTop, 132 inView.marginRight, 133 marginBottom + bottomInset 134 ) 135 } 136 WindowInsets.CONSUMED 137 } 138 139 endButton.setOnApplyWindowInsetsListener { inView, windowInsets -> 140 val bottomInset = windowInsets.displayCutout?.safeInsetBottom ?: 0 141 val marginBottom = 142 inView.resources.getDimension(R.dimen.keyguard_affordance_vertical_offset).toInt() 143 inView.layoutParams = 144 (inView.layoutParams as MarginLayoutParams).apply { 145 setMargins( 146 inView.marginLeft, 147 inView.marginTop, 148 inView.marginRight, 149 marginBottom + bottomInset 150 ) 151 } 152 WindowInsets.CONSUMED 153 } 154 155 view.clipChildren = false 156 view.clipToPadding = false 157 view.setOnTouchListener { _, event -> 158 if (settingsMenu.isVisible) { 159 val hitRect = Rect() 160 settingsMenu.getHitRect(hitRect) 161 if (!hitRect.contains(event.x.toInt(), event.y.toInt())) { 162 viewModel.onTouchedOutsideLockScreenSettingsMenu() 163 } 164 } 165 166 false 167 } 168 169 val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) 170 171 val disposableHandle = 172 view.repeatWhenAttached { 173 repeatOnLifecycle(Lifecycle.State.STARTED) { 174 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 175 launch("$TAG#viewModel.startButton") { 176 viewModel.startButton.collect { buttonModel -> 177 updateButton( 178 view = startButton, 179 viewModel = buttonModel, 180 falsingManager = falsingManager, 181 messageDisplayer = messageDisplayer, 182 vibratorHelper = vibratorHelper, 183 ) 184 } 185 } 186 187 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 188 launch("$TAG#viewModel.endButton") { 189 viewModel.endButton.collect { buttonModel -> 190 updateButton( 191 view = endButton, 192 viewModel = buttonModel, 193 falsingManager = falsingManager, 194 messageDisplayer = messageDisplayer, 195 vibratorHelper = vibratorHelper, 196 ) 197 } 198 } 199 200 launch("$TAG#viewModel.isOverlayContainerVisible") { 201 viewModel.isOverlayContainerVisible.collect { isVisible -> 202 overlayContainer.visibility = 203 if (isVisible) { 204 View.VISIBLE 205 } else { 206 View.INVISIBLE 207 } 208 } 209 } 210 211 launch("$TAG#viewModel.alpha") { 212 viewModel.alpha.collect { alpha -> 213 ambientIndicationArea?.apply { 214 this.importantForAccessibility = 215 if (alpha == 0f) { 216 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS 217 } else { 218 View.IMPORTANT_FOR_ACCESSIBILITY_AUTO 219 } 220 this.alpha = alpha 221 } 222 } 223 } 224 225 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 226 launch("$TAG#updateButtonAlpha") { 227 updateButtonAlpha( 228 view = startButton, 229 viewModel = viewModel.startButton, 230 alphaFlow = viewModel.alpha, 231 ) 232 } 233 234 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 235 launch("$TAG#updateButtonAlpha") { 236 updateButtonAlpha( 237 view = endButton, 238 viewModel = viewModel.endButton, 239 alphaFlow = viewModel.alpha, 240 ) 241 } 242 243 launch("$TAG#viewModel.indicationAreaTranslationX") { 244 viewModel.indicationAreaTranslationX.collect { translationX -> 245 ambientIndicationArea?.translationX = translationX 246 } 247 } 248 249 launch("$TAG#viewModel.indicationAreaTranslationY") { 250 configurationBasedDimensions 251 .map { it.defaultBurnInPreventionYOffsetPx } 252 .flatMapLatest { defaultBurnInOffsetY -> 253 viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) 254 } 255 .collect { translationY -> 256 ambientIndicationArea?.translationY = translationY 257 } 258 } 259 260 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 261 launch("$TAG#startButton.updateLayoutParams<ViewGroup") { 262 configurationBasedDimensions.collect { dimensions -> 263 startButton.updateLayoutParams<ViewGroup.LayoutParams> { 264 width = dimensions.buttonSizePx.width 265 height = dimensions.buttonSizePx.height 266 } 267 endButton.updateLayoutParams<ViewGroup.LayoutParams> { 268 width = dimensions.buttonSizePx.width 269 height = dimensions.buttonSizePx.height 270 } 271 } 272 } 273 274 launch("$TAG#viewModel.settingsMenuViewModel") { 275 viewModel.settingsMenuViewModel.isVisible.distinctUntilChanged().collect { 276 isVisible -> 277 settingsMenu.animateVisibility(visible = isVisible) 278 if (isVisible) { 279 vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated) 280 settingsMenu.setOnTouchListener( 281 KeyguardSettingsButtonOnTouchListener( 282 viewModel = viewModel.settingsMenuViewModel, 283 ) 284 ) 285 IconViewBinder.bind( 286 icon = viewModel.settingsMenuViewModel.icon, 287 view = settingsMenu.requireViewById(R.id.icon), 288 ) 289 TextViewBinder.bind( 290 view = settingsMenu.requireViewById(R.id.text), 291 viewModel = viewModel.settingsMenuViewModel.text, 292 ) 293 } 294 } 295 } 296 297 // activityStarter will only be null when rendering the preview that 298 // shows up in the Wallpaper Picker app. If we do that, then the 299 // settings menu should never be visible. 300 if (activityStarter != null) { 301 launch("$TAG#viewModel.settingsMenuViewModel") { 302 viewModel.settingsMenuViewModel.shouldOpenSettings 303 .filter { it } 304 .collect { 305 navigateToLockScreenSettings( 306 activityStarter = activityStarter, 307 view = settingsMenu, 308 ) 309 viewModel.settingsMenuViewModel.onSettingsShown() 310 } 311 } 312 } 313 } 314 } 315 316 return object : Binding { 317 override fun onConfigurationChanged() { 318 configurationBasedDimensions.value = loadFromResources(view) 319 } 320 321 override fun shouldConstrainToTopOfLockIcon(): Boolean = 322 viewModel.shouldConstrainToTopOfLockIcon() 323 324 override fun destroy() { 325 disposableHandle.dispose() 326 } 327 } 328 } 329 330 @Deprecated("Deprecated as part of b/278057014") 331 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 332 @SuppressLint("ClickableViewAccessibility") 333 private fun updateButton( 334 view: ImageView, 335 viewModel: KeyguardQuickAffordanceViewModel, 336 falsingManager: FalsingManager?, 337 messageDisplayer: (Int) -> Unit, 338 vibratorHelper: VibratorHelper?, 339 ) { 340 if (!viewModel.isVisible) { 341 view.isInvisible = true 342 return 343 } 344 345 if (!view.isVisible) { 346 view.isVisible = true 347 if (viewModel.animateReveal) { 348 view.alpha = 0f 349 view.translationY = view.height / 2f 350 view 351 .animate() 352 .alpha(1f) 353 .translationY(0f) 354 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 355 .setDuration(EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS) 356 .start() 357 } 358 } 359 360 IconViewBinder.bind(viewModel.icon, view) 361 362 (view.drawable as? Animatable2)?.let { animatable -> 363 (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId -> 364 // Always start the animation (we do call stop() below, if we need to skip it). 365 animatable.start() 366 367 if (view.tag != iconResourceId) { 368 // Here when we haven't run the animation on a previous update. 369 // 370 // Save the resource ID for next time, so we know not to re-animate the same 371 // animation again. 372 view.tag = iconResourceId 373 } else { 374 // Here when we've already done this animation on a previous update and want to 375 // skip directly to the final frame of the animation to avoid running it. 376 // 377 // By calling stop after start, we go to the final frame of the animation. 378 animatable.stop() 379 } 380 } 381 } 382 383 view.isActivated = viewModel.isActivated 384 view.drawable.setTint( 385 Utils.getColorAttrDefaultColor( 386 view.context, 387 if (viewModel.isActivated) { 388 com.android.internal.R.attr.materialColorOnPrimaryFixed 389 } else { 390 com.android.internal.R.attr.materialColorOnSurface 391 }, 392 ) 393 ) 394 395 view.backgroundTintList = 396 if (!viewModel.isSelected) { 397 Utils.getColorAttr( 398 view.context, 399 if (viewModel.isActivated) { 400 com.android.internal.R.attr.materialColorPrimaryFixed 401 } else { 402 com.android.internal.R.attr.materialColorSurfaceContainerHigh 403 } 404 ) 405 } else { 406 null 407 } 408 view 409 .animate() 410 .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f) 411 .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f) 412 .start() 413 414 view.isClickable = viewModel.isClickable 415 if (viewModel.isClickable) { 416 if (viewModel.useLongPress) { 417 val onTouchListener = 418 KeyguardQuickAffordanceOnTouchListener( 419 view, 420 viewModel, 421 messageDisplayer, 422 vibratorHelper, 423 falsingManager, 424 ) 425 view.setOnTouchListener(onTouchListener) 426 view.setOnClickListener { 427 messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) 428 val amplitude = 429 view.context.resources 430 .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude) 431 .toFloat() 432 val shakeAnimator = 433 ObjectAnimator.ofFloat( 434 view, 435 "translationX", 436 -amplitude / 2, 437 amplitude / 2, 438 ) 439 shakeAnimator.duration = 440 KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds 441 shakeAnimator.interpolator = 442 CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles) 443 shakeAnimator.doOnEnd { view.translationX = 0f } 444 shakeAnimator.start() 445 446 vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake) 447 } 448 view.onLongClickListener = 449 OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener) 450 } else { 451 view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) 452 } 453 } else { 454 view.onLongClickListener = null 455 view.setOnClickListener(null) 456 view.setOnTouchListener(null) 457 } 458 459 view.isSelected = viewModel.isSelected 460 } 461 462 @Deprecated("Deprecated as part of b/278057014") 463 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 464 private suspend fun updateButtonAlpha( 465 view: View, 466 viewModel: Flow<KeyguardQuickAffordanceViewModel>, 467 alphaFlow: Flow<Float>, 468 ) { 469 combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha -> 470 if (isDimmed) DIM_ALPHA else alpha 471 } 472 .collect { view.alpha = it } 473 } 474 475 @Deprecated("Deprecated as part of b/278057014") 476 private fun View.animateVisibility(visible: Boolean) { 477 animate() 478 .withStartAction { 479 if (visible) { 480 alpha = 0f 481 isVisible = true 482 } 483 } 484 .alpha(if (visible) 1f else 0f) 485 .withEndAction { 486 if (!visible) { 487 isVisible = false 488 } 489 } 490 .start() 491 } 492 493 @Deprecated("Deprecated as part of b/278057014") 494 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 495 private class OnLongClickListener( 496 private val falsingManager: FalsingManager?, 497 private val viewModel: KeyguardQuickAffordanceViewModel, 498 private val vibratorHelper: VibratorHelper?, 499 private val onTouchListener: KeyguardQuickAffordanceOnTouchListener 500 ) : View.OnLongClickListener { 501 override fun onLongClick(view: View): Boolean { 502 if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) { 503 return true 504 } 505 506 if (viewModel.configKey != null) { 507 viewModel.onClicked( 508 KeyguardQuickAffordanceViewModel.OnClickedParameters( 509 configKey = viewModel.configKey, 510 expandable = Expandable.fromView(view), 511 slotId = viewModel.slotId, 512 ) 513 ) 514 vibratorHelper?.vibrate( 515 if (viewModel.isActivated) { 516 KeyguardBottomAreaVibrations.Activated 517 } else { 518 KeyguardBottomAreaVibrations.Deactivated 519 } 520 ) 521 } 522 523 onTouchListener.cancel() 524 return true 525 } 526 527 override fun onLongClickUseDefaultHapticFeedback(view: View) = false 528 } 529 530 @Deprecated("Deprecated as part of b/278057014") 531 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 532 private class OnClickListener( 533 private val viewModel: KeyguardQuickAffordanceViewModel, 534 private val falsingManager: FalsingManager, 535 ) : View.OnClickListener { 536 override fun onClick(view: View) { 537 if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 538 return 539 } 540 541 if (viewModel.configKey != null) { 542 viewModel.onClicked( 543 KeyguardQuickAffordanceViewModel.OnClickedParameters( 544 configKey = viewModel.configKey, 545 expandable = Expandable.fromView(view), 546 slotId = viewModel.slotId, 547 ) 548 ) 549 } 550 } 551 } 552 553 @Deprecated("Deprecated as part of b/278057014") 554 private fun loadFromResources(view: View): ConfigurationBasedDimensions { 555 return ConfigurationBasedDimensions( 556 defaultBurnInPreventionYOffsetPx = 557 view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset), 558 buttonSizePx = 559 Size( 560 view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), 561 view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), 562 ), 563 ) 564 } 565 566 @Deprecated("Deprecated as part of b/278057014") 567 /** Opens the wallpaper picker screen after the device is unlocked by the user. */ 568 private fun navigateToLockScreenSettings( 569 activityStarter: ActivityStarter, 570 view: View, 571 ) { 572 activityStarter.postStartActivityDismissingKeyguard( 573 WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD), 574 /* delay= */ 0, 575 /* animationController= */ ActivityTransitionAnimator.Controller.fromView(view), 576 /* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls) 577 ) 578 } 579 580 @Deprecated("Deprecated as part of b/278057014") 581 // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt] 582 private data class ConfigurationBasedDimensions( 583 val defaultBurnInPreventionYOffsetPx: Int, 584 val buttonSizePx: Size, 585 ) 586 } 587