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 }