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 
18 package com.android.systemui.keyguard.ui.binder
19 
20 import android.annotation.SuppressLint
21 import android.graphics.drawable.Animatable2
22 import android.util.Size
23 import android.view.View
24 import android.view.ViewGroup
25 import android.widget.ImageView
26 import androidx.core.animation.CycleInterpolator
27 import androidx.core.animation.ObjectAnimator
28 import androidx.core.view.isInvisible
29 import androidx.core.view.isVisible
30 import androidx.core.view.updateLayoutParams
31 import androidx.lifecycle.Lifecycle
32 import androidx.lifecycle.repeatOnLifecycle
33 import com.android.settingslib.Utils
34 import com.android.systemui.animation.Expandable
35 import com.android.systemui.animation.view.LaunchableImageView
36 import com.android.systemui.common.shared.model.Icon
37 import com.android.systemui.common.ui.binder.IconViewBinder
38 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
39 import com.android.systemui.lifecycle.repeatWhenAttached
40 import com.android.systemui.plugins.FalsingManager
41 import com.android.systemui.res.R
42 import com.android.systemui.statusbar.VibratorHelper
43 import com.android.systemui.util.doOnEnd
44 import kotlinx.coroutines.flow.Flow
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.combine
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.launch
49 
50 /** This is only for a SINGLE Quick affordance */
51 object KeyguardQuickAffordanceViewBinder {
52 
53     private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
54     private const val SCALE_SELECTED_BUTTON = 1.23f
55     private const val DIM_ALPHA = 0.3f
56 
57     /**
58      * Defines interface for an object that acts as the binding between the view and its view-model.
59      *
60      * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
61      * it is bound.
62      */
63     interface Binding {
64         /** Notifies that device configuration has changed. */
65         fun onConfigurationChanged()
66 
67         /** Destroys this binding, releases resources, and cancels any coroutines. */
68         fun destroy()
69     }
70 
71     fun bind(
72         view: LaunchableImageView,
73         viewModel: Flow<KeyguardQuickAffordanceViewModel>,
74         alpha: Flow<Float>,
75         falsingManager: FalsingManager?,
76         vibratorHelper: VibratorHelper?,
77         messageDisplayer: (Int) -> Unit,
78     ): Binding {
79         val button = view as ImageView
80         val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
81         val disposableHandle =
82             view.repeatWhenAttached {
83                 repeatOnLifecycle(Lifecycle.State.STARTED) {
84                     launch {
85                         viewModel.collect { buttonModel ->
86                             updateButton(
87                                 view = button,
88                                 viewModel = buttonModel,
89                                 falsingManager = falsingManager,
90                                 messageDisplayer = messageDisplayer,
91                                 vibratorHelper = vibratorHelper,
92                             )
93                         }
94                     }
95 
96                     launch {
97                         updateButtonAlpha(
98                             view = button,
99                             viewModel = viewModel,
100                             alphaFlow = alpha,
101                         )
102                     }
103 
104                     launch {
105                         configurationBasedDimensions.collect { dimensions ->
106                             button.updateLayoutParams<ViewGroup.LayoutParams> {
107                                 width = dimensions.buttonSizePx.width
108                                 height = dimensions.buttonSizePx.height
109                             }
110                         }
111                     }
112                 }
113             }
114 
115         return object : Binding {
116             override fun onConfigurationChanged() {
117                 configurationBasedDimensions.value = loadFromResources(view)
118             }
119 
120             override fun destroy() {
121                 view.setOnApplyWindowInsetsListener(null)
122                 disposableHandle.dispose()
123             }
124         }
125     }
126 
127     @SuppressLint("ClickableViewAccessibility")
128     private fun updateButton(
129         view: ImageView,
130         viewModel: KeyguardQuickAffordanceViewModel,
131         falsingManager: FalsingManager?,
132         messageDisplayer: (Int) -> Unit,
133         vibratorHelper: VibratorHelper?,
134     ) {
135         if (!viewModel.isVisible) {
136             view.isInvisible = true
137             return
138         }
139 
140         if (!view.isVisible) {
141             view.isVisible = true
142         }
143 
144         IconViewBinder.bind(viewModel.icon, view)
145 
146         (view.drawable as? Animatable2)?.let { animatable ->
147             (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId ->
148                 // Always start the animation (we do call stop() below, if we need to skip it).
149                 animatable.start()
150 
151                 if (view.tag != iconResourceId) {
152                     // Here when we haven't run the animation on a previous update.
153                     //
154                     // Save the resource ID for next time, so we know not to re-animate the same
155                     // animation again.
156                     view.tag = iconResourceId
157                 } else {
158                     // Here when we've already done this animation on a previous update and want to
159                     // skip directly to the final frame of the animation to avoid running it.
160                     //
161                     // By calling stop after start, we go to the final frame of the animation.
162                     animatable.stop()
163                 }
164             }
165         }
166 
167         view.isActivated = viewModel.isActivated
168         view.drawable.setTint(
169             Utils.getColorAttrDefaultColor(
170                 view.context,
171                 if (viewModel.isActivated) {
172                     com.android.internal.R.attr.materialColorOnPrimaryFixed
173                 } else {
174                     com.android.internal.R.attr.materialColorOnSurface
175                 },
176             )
177         )
178 
179         view.backgroundTintList =
180             if (!viewModel.isSelected) {
181                 Utils.getColorAttr(
182                     view.context,
183                     if (viewModel.isActivated) {
184                         com.android.internal.R.attr.materialColorPrimaryFixed
185                     } else {
186                         com.android.internal.R.attr.materialColorSurfaceContainerHigh
187                     }
188                 )
189             } else {
190                 null
191             }
192         view
193             .animate()
194             .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
195             .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
196             .start()
197 
198         view.isClickable = viewModel.isClickable
199         if (viewModel.isClickable) {
200             if (viewModel.useLongPress) {
201                 val onTouchListener =
202                     KeyguardQuickAffordanceOnTouchListener(
203                         view,
204                         viewModel,
205                         messageDisplayer,
206                         vibratorHelper,
207                         falsingManager,
208                     )
209                 view.setOnTouchListener(onTouchListener)
210                 view.setOnClickListener {
211                     messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
212                     val amplitude =
213                         view.context.resources
214                             .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude)
215                             .toFloat()
216                     val shakeAnimator =
217                         ObjectAnimator.ofFloat(
218                             view,
219                             "translationX",
220                             -amplitude / 2,
221                             amplitude / 2,
222                         )
223                     shakeAnimator.duration =
224                         KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
225                     shakeAnimator.interpolator =
226                         CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles)
227                     shakeAnimator.doOnEnd { view.translationX = 0f }
228                     shakeAnimator.start()
229 
230                     vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
231                 }
232                 view.onLongClickListener =
233                     OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
234             } else {
235                 view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
236             }
237         } else {
238             view.onLongClickListener = null
239             view.setOnClickListener(null)
240             view.setOnTouchListener(null)
241         }
242 
243         view.isSelected = viewModel.isSelected
244     }
245 
246     private suspend fun updateButtonAlpha(
247         view: View,
248         viewModel: Flow<KeyguardQuickAffordanceViewModel>,
249         alphaFlow: Flow<Float>,
250     ) {
251         combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha ->
252                 if (isDimmed) DIM_ALPHA else alpha
253             }
254             .collect { view.alpha = it }
255     }
256 
257     private fun loadFromResources(view: View): ConfigurationBasedDimensions {
258         return ConfigurationBasedDimensions(
259             buttonSizePx =
260                 Size(
261                     view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
262                     view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
263                 ),
264         )
265     }
266 
267     private class OnClickListener(
268         private val viewModel: KeyguardQuickAffordanceViewModel,
269         private val falsingManager: FalsingManager,
270     ) : View.OnClickListener {
271         override fun onClick(view: View) {
272             if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
273                 return
274             }
275 
276             if (viewModel.configKey != null) {
277                 viewModel.onClicked(
278                     KeyguardQuickAffordanceViewModel.OnClickedParameters(
279                         configKey = viewModel.configKey,
280                         expandable = Expandable.fromView(view),
281                         slotId = viewModel.slotId,
282                     )
283                 )
284             }
285         }
286     }
287 
288     private class OnLongClickListener(
289         private val falsingManager: FalsingManager?,
290         private val viewModel: KeyguardQuickAffordanceViewModel,
291         private val vibratorHelper: VibratorHelper?,
292         private val onTouchListener: KeyguardQuickAffordanceOnTouchListener
293     ) : View.OnLongClickListener {
294         override fun onLongClick(view: View): Boolean {
295             if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) {
296                 return true
297             }
298 
299             if (viewModel.configKey != null) {
300                 viewModel.onClicked(
301                     KeyguardQuickAffordanceViewModel.OnClickedParameters(
302                         configKey = viewModel.configKey,
303                         expandable = Expandable.fromView(view),
304                         slotId = viewModel.slotId,
305                     )
306                 )
307                 vibratorHelper?.vibrate(
308                     if (viewModel.isActivated) {
309                         KeyguardBottomAreaVibrations.Activated
310                     } else {
311                         KeyguardBottomAreaVibrations.Deactivated
312                     }
313                 )
314             }
315 
316             onTouchListener.cancel()
317             return true
318         }
319 
320         override fun onLongClickUseDefaultHapticFeedback(view: View) = false
321     }
322 
323     private data class ConfigurationBasedDimensions(
324         val buttonSizePx: Size,
325     )
326 }
327