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