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