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