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