• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.res.ColorStateList
22 import android.util.Log
23 import android.util.StateSet
24 import android.view.HapticFeedbackConstants
25 import android.view.View
26 import androidx.compose.ui.graphics.Color
27 import androidx.compose.ui.graphics.toArgb
28 import androidx.core.view.isInvisible
29 import androidx.lifecycle.Lifecycle
30 import androidx.lifecycle.repeatOnLifecycle
31 import com.android.app.tracing.coroutines.launchTraced as launch
32 import com.android.systemui.Flags
33 import com.android.systemui.common.ui.view.TouchHandlingView
34 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
35 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
36 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
37 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
38 import com.android.systemui.lifecycle.repeatWhenAttached
39 import com.android.systemui.plugins.FalsingManager
40 import com.android.systemui.res.R
41 import com.android.systemui.statusbar.VibratorHelper
42 import com.android.systemui.util.kotlin.DisposableHandles
43 import com.google.android.msdl.data.model.MSDLToken
44 import com.google.android.msdl.domain.MSDLPlayer
45 import kotlinx.coroutines.CoroutineDispatcher
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.DisposableHandle
48 
49 object DeviceEntryIconViewBinder {
50     private const val TAG = "DeviceEntryIconViewBinder"
51 
52     /**
53      * Updates UI for:
54      * - device entry containing view (parent view for the below views)
55      *     - touch handling view (transparent, no UI)
56      *     - foreground icon view (lock/unlock/fingerprint)
57      *     - background view (optional)
58      */
59     @SuppressLint("ClickableViewAccessibility")
60     @JvmStatic
61     fun bind(
62         applicationScope: CoroutineScope,
63         mainImmediateDispatcher: CoroutineDispatcher,
64         view: DeviceEntryIconView,
65         viewModel: DeviceEntryIconViewModel,
66         fgViewModel: DeviceEntryForegroundViewModel,
67         bgViewModel: DeviceEntryBackgroundViewModel,
68         falsingManager: FalsingManager,
69         vibratorHelper: VibratorHelper,
70         msdlPlayer: MSDLPlayer,
71         overrideColor: Color? = null,
72     ): DisposableHandle {
73         val disposables = DisposableHandles()
74         val touchHandlingView = view.touchHandlingView
75         val fgIconView = view.iconView
76         val bgView = view.bgView
77         touchHandlingView.listener =
78             object : TouchHandlingView.Listener {
79                 override fun onLongPressDetected(
80                     view: View,
81                     x: Int,
82                     y: Int,
83                     isA11yAction: Boolean,
84                 ) {
85                     if (
86                         !isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
87                     ) {
88                         Log.d(
89                             TAG,
90                             "Long press rejected because it is not a11yAction " +
91                                 "and it is a falseLongTap",
92                         )
93                         return
94                     }
95                     if (!Flags.msdlFeedback()) {
96                         vibratorHelper.performHapticFeedback(view, HapticFeedbackConstants.CONFIRM)
97                     }
98                     applicationScope.launch {
99                         view.clearFocus()
100                         view.clearAccessibilityFocus()
101                         viewModel.onUserInteraction()
102                     }
103                 }
104             }
105 
106         disposables +=
107             view.repeatWhenAttached(mainImmediateDispatcher) {
108                 repeatOnLifecycle(Lifecycle.State.CREATED) {
109                     launch("$TAG#viewModel.useBackgroundProtection") {
110                         viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
111                             if (useBackgroundProtection) {
112                                 bgView.visibility = View.VISIBLE
113                             } else {
114                                 bgView.visibility = View.GONE
115                             }
116                         }
117                     }
118                     launch("$TAG#viewModel.burnInOffsets") {
119                         viewModel.burnInOffsets.collect { burnInOffsets ->
120                             view.translationX = burnInOffsets.x.toFloat()
121                             view.translationY = burnInOffsets.y.toFloat()
122                             view.aodFpDrawable.progress = burnInOffsets.progress
123                         }
124                     }
125 
126                     launch("$TAG#viewModel.deviceEntryViewAlpha") {
127                         viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha }
128                     }
129                 }
130             }
131 
132         disposables +=
133             view.repeatWhenAttached {
134                 // Repeat on CREATED so that the view will always observe the entire
135                 // GONE => AOD transition (even though the view may not be visible until the middle
136                 // of the transition.
137                 repeatOnLifecycle(Lifecycle.State.CREATED) {
138                     launch("$TAG#viewModel.isVisible") {
139                         viewModel.isVisible.collect { isVisible ->
140                             touchHandlingView.isInvisible = !isVisible
141                             view.isClickable = isVisible
142                         }
143                     }
144                     launch("$TAG#viewModel.isLongPressEnabled") {
145                         viewModel.isLongPressEnabled.collect { isEnabled ->
146                             touchHandlingView.setLongPressHandlingEnabled(isEnabled)
147                         }
148                     }
149                     launch("$TAG#viewModel.isUdfpsSupported") {
150                         viewModel.isUdfpsSupported.collect { udfpsSupported ->
151                             touchHandlingView.longPressDuration =
152                                 if (udfpsSupported) {
153                                     {
154                                         view.resources
155                                             .getInteger(
156                                                 R.integer.config_udfpsDeviceEntryIconLongPress
157                                             )
158                                             .toLong()
159                                     }
160                                 } else {
161                                     {
162                                         view.resources
163                                             .getInteger(R.integer.config_lockIconLongPress)
164                                             .toLong()
165                                     }
166                                 }
167                         }
168                     }
169                     launch("$TAG#viewModel.accessibilityDelegateHint") {
170                         viewModel.accessibilityDelegateHint.collect { hint ->
171                             view.accessibilityHintType = hint
172                             if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) {
173                                 view.setOnClickListener {
174                                     if (Flags.msdlFeedback()) {
175                                         val token =
176                                             if (
177                                                 hint ==
178                                                     DeviceEntryIconView.AccessibilityHintType.ENTER
179                                             ) {
180                                                 MSDLToken.UNLOCK
181                                             } else {
182                                                 MSDLToken.LONG_PRESS
183                                             }
184                                         msdlPlayer.playToken(token)
185                                     } else {
186                                         vibratorHelper.performHapticFeedback(
187                                             view,
188                                             HapticFeedbackConstants.CONFIRM,
189                                         )
190                                     }
191                                     applicationScope.launch {
192                                         view.clearFocus()
193                                         view.clearAccessibilityFocus()
194                                         viewModel.onUserInteraction()
195                                     }
196                                 }
197                             } else {
198                                 view.setOnClickListener(null)
199                             }
200                         }
201                     }
202 
203                     if (Flags.msdlFeedback()) {
204                         launch("$TAG#viewModel.isPrimaryBouncerShowing") {
205                             viewModel.deviceDidNotEnterFromDeviceEntryIcon.collect {
206                                 // If we did not enter from the icon, we did not play device entry
207                                 // haptics. Therefore, we play the token for long-press instead.
208                                 msdlPlayer.playToken(MSDLToken.LONG_PRESS)
209                             }
210                         }
211                     }
212                 }
213             }
214 
215         disposables +=
216             fgIconView.repeatWhenAttached {
217                 repeatOnLifecycle(Lifecycle.State.STARTED) {
218                     // Start with an empty state
219                     Log.d(TAG, "Initializing device entry fgIconView")
220                     fgIconView.setImageState(StateSet.NOTHING, /* merge */ false)
221                     launch("$TAG#fpIconView.viewModel") {
222                         fgViewModel.viewModel.collect { viewModel ->
223                             Log.d(TAG, "Updating device entry icon image state $viewModel")
224                             if (viewModel.type.contentDescriptionResId != -1) {
225                                 fgIconView.contentDescription =
226                                     fgIconView.resources.getString(
227                                         viewModel.type.contentDescriptionResId
228                                     )
229                             }
230                             fgIconView.imageTintList =
231                                 ColorStateList.valueOf(overrideColor?.toArgb() ?: viewModel.tint)
232                             fgIconView.setPadding(
233                                 viewModel.padding,
234                                 viewModel.padding,
235                                 viewModel.padding,
236                                 viewModel.padding,
237                             )
238                             // Set image state at the end after updating other view state. This
239                             // method forces the ImageView to recompute the bounds of the drawable.
240                             fgIconView.setImageState(
241                                 view.getIconState(viewModel.type, viewModel.useAodVariant),
242                                 /* merge */ false,
243                             )
244                             // Invalidate, just in case the padding changes just after icon changes
245                             fgIconView.invalidate()
246                         }
247                     }
248                 }
249             }
250 
251         disposables +=
252             bgView.repeatWhenAttached(mainImmediateDispatcher) {
253                 repeatOnLifecycle(Lifecycle.State.CREATED) {
254                     launch("$TAG#bgViewModel.alpha") {
255                         bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha }
256                     }
257                     launch("$TAG#bgViewModel.color") {
258                         bgViewModel.color.collect { color ->
259                             bgView.imageTintList = ColorStateList.valueOf(color)
260                         }
261                     }
262                 }
263             }
264 
265         return disposables
266     }
267 }
268