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