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