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 package com.android.systemui.keyguard.ui.binder 18 19 import android.annotation.SuppressLint 20 import android.graphics.PointF 21 import android.view.MotionEvent 22 import android.view.View 23 import android.view.ViewConfiguration 24 import android.view.ViewPropertyAnimator 25 import androidx.core.animation.CycleInterpolator 26 import androidx.core.animation.ObjectAnimator 27 import com.android.systemui.res.R 28 import com.android.systemui.animation.Expandable 29 import com.android.systemui.common.ui.view.rawDistanceFrom 30 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel 31 import com.android.systemui.plugins.FalsingManager 32 import com.android.systemui.statusbar.VibratorHelper 33 34 class KeyguardQuickAffordanceOnTouchListener( 35 private val view: View, 36 private val viewModel: KeyguardQuickAffordanceViewModel, 37 private val messageDisplayer: (Int) -> Unit, 38 private val vibratorHelper: VibratorHelper?, 39 private val falsingManager: FalsingManager?, 40 ) : View.OnTouchListener { 41 42 private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() 43 private var longPressAnimator: ViewPropertyAnimator? = null <lambda>null44 private val downDisplayCoords: PointF by lazy { PointF() } 45 46 @SuppressLint("ClickableViewAccessibility") onTouchnull47 override fun onTouch(v: View, event: MotionEvent): Boolean { 48 return when (event.actionMasked) { 49 MotionEvent.ACTION_DOWN -> { 50 if (viewModel.configKey != null) { 51 downDisplayCoords.set(event.rawX, event.rawY) 52 if (isUsingAccurateTool(event)) { 53 // For accurate tool types (stylus, mouse, etc.), we don't require a 54 // long-press. 55 } else { 56 // When not using a stylus, we require a long-press to activate the 57 // quick affordance, mostly to do "falsing" (e.g. protect from false 58 // clicks in the pocket/bag). 59 longPressAnimator = 60 view 61 .animate() 62 .scaleX(PRESSED_SCALE) 63 .scaleY(PRESSED_SCALE) 64 .setDuration(longPressDurationMs) 65 } 66 } 67 false 68 } 69 MotionEvent.ACTION_MOVE -> { 70 if (!isUsingAccurateTool(event)) { 71 // Moving too far while performing a long-press gesture cancels that 72 // gesture. 73 if ( 74 event 75 .rawDistanceFrom( 76 downDisplayCoords.x, 77 downDisplayCoords.y, 78 ) > ViewConfiguration.getTouchSlop() 79 ) { 80 cancel() 81 } 82 } 83 false 84 } 85 MotionEvent.ACTION_UP -> { 86 if (isUsingAccurateTool(event)) { 87 // When using an accurate tool type (stylus, mouse, etc.), we don't require 88 // a long-press gesture to activate the quick affordance. Therefore, lifting 89 // the pointer performs a click. 90 if ( 91 viewModel.configKey != null && 92 event.rawDistanceFrom(downDisplayCoords.x, downDisplayCoords.y) <= 93 ViewConfiguration.getTouchSlop() && 94 falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false 95 ) { 96 dispatchClick(viewModel.configKey) 97 } 98 } else { 99 // When not using a stylus, lifting the finger/pointer will actually cancel 100 // the long-press gesture. Calling cancel after the quick affordance was 101 // already long-press activated is a no-op, so it's safe to call from here. 102 cancel() 103 } 104 false 105 } 106 MotionEvent.ACTION_CANCEL -> { 107 cancel() 108 true 109 } 110 else -> false 111 } 112 } 113 dispatchClicknull114 private fun dispatchClick( 115 configKey: String, 116 ) { 117 view.setOnClickListener { 118 vibratorHelper?.vibrate( 119 if (viewModel.isActivated) { 120 KeyguardBottomAreaVibrations.Activated 121 } else { 122 KeyguardBottomAreaVibrations.Deactivated 123 } 124 ) 125 viewModel.onClicked( 126 KeyguardQuickAffordanceViewModel.OnClickedParameters( 127 configKey = configKey, 128 expandable = Expandable.fromView(view), 129 slotId = viewModel.slotId, 130 ) 131 ) 132 } 133 view.performClick() 134 view.setOnClickListener(null) 135 } 136 cancelnull137 fun cancel() { 138 longPressAnimator?.cancel() 139 longPressAnimator = null 140 view.animate().scaleX(1f).scaleY(1f) 141 } 142 143 companion object { 144 private const val PRESSED_SCALE = 1.5f 145 146 /** 147 * Returns `true` if the tool type at the given pointer index is an accurate tool (like 148 * stylus or mouse), which means we can trust it to not be a false click; `false` otherwise. 149 */ isUsingAccurateToolnull150 private fun isUsingAccurateTool( 151 event: MotionEvent, 152 pointerIndex: Int = 0, 153 ): Boolean { 154 return when (event.getToolType(pointerIndex)) { 155 MotionEvent.TOOL_TYPE_STYLUS -> true 156 MotionEvent.TOOL_TYPE_MOUSE -> true 157 else -> false 158 } 159 } 160 } 161 } 162