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