• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.common.ui.view
19 
20 import android.annotation.SuppressLint
21 import android.content.Context
22 import android.os.Bundle
23 import android.util.AttributeSet
24 import android.view.MotionEvent
25 import android.view.View
26 import android.view.ViewConfiguration
27 import android.view.accessibility.AccessibilityNodeInfo
28 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
29 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
30 import com.android.systemui.Flags.doubleTapToSleep
31 import com.android.systemui.log.TouchHandlingViewLogger
32 import com.android.systemui.shade.TouchLogger
33 import kotlinx.coroutines.DisposableHandle
34 
35 /**
36  * View designed to handle long-presses and double taps.
37  *
38  * The view will not handle any gestures by default. To set it up, set up a listener and, when ready
39  * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled],
40  * [setDoublePressHandlingEnabled]) to `true`.
41  */
42 class TouchHandlingView(
43     context: Context,
44     attrs: AttributeSet?,
45     longPressDuration: () -> Long,
46     allowedTouchSlop: Int = ViewConfiguration.getTouchSlop(),
47     logger: TouchHandlingViewLogger? = null,
48 ) : View(context, attrs) {
49 
50     init {
51         setupAccessibilityDelegate()
52     }
53 
54     constructor(
55         context: Context,
56         attrs: AttributeSet?,
57     ) : this(context, attrs, { ViewConfiguration.getLongPressTimeout().toLong() })
58 
59     interface Listener {
60         /** Notifies that a long-press has been detected by the given view. */
61         fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean = false)
62 
63         /** Notifies that the gesture was too short for a long press, it is actually a click. */
64         fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit
65 
66         /** Notifies that a double tap has been detected by the given view. */
67         fun onDoubleTapDetected(view: View) = Unit
68     }
69 
70     var listener: Listener? = null
71 
72     var accessibilityHintLongPressAction: AccessibilityAction? = null
73 
74     private val interactionHandler: TouchHandlingViewInteractionHandler by lazy {
75         TouchHandlingViewInteractionHandler(
76             context = context,
77             postDelayed = { block, timeoutMs ->
78                 val dispatchToken = Any()
79 
80                 handler.postDelayed(block, dispatchToken, timeoutMs)
81 
82                 DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) }
83             },
84             isAttachedToWindow = ::isAttachedToWindow,
85             onLongPressDetected = { x, y ->
86                 listener?.onLongPressDetected(view = this, x = x, y = y)
87             },
88             onSingleTapDetected = { x, y ->
89                 listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y)
90             },
91             onDoubleTapDetected = {
92                 if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView)
93             },
94             longPressDuration = longPressDuration,
95             allowedTouchSlop = allowedTouchSlop,
96             logger = logger,
97         )
98     }
99 
100     var longPressDuration: () -> Long
101         get() = interactionHandler.longPressDuration
102         set(longPressDuration) {
103             interactionHandler.longPressDuration = longPressDuration
104         }
105 
106     fun setLongPressHandlingEnabled(isEnabled: Boolean) {
107         interactionHandler.isLongPressHandlingEnabled = isEnabled
108     }
109 
110     fun setDoublePressHandlingEnabled(isEnabled: Boolean) {
111         interactionHandler.isDoubleTapHandlingEnabled = isEnabled
112     }
113 
114     override fun dispatchTouchEvent(event: MotionEvent): Boolean {
115         return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
116     }
117 
118     @SuppressLint("ClickableViewAccessibility")
119     override fun onTouchEvent(event: MotionEvent): Boolean {
120         return interactionHandler.onTouchEvent(event)
121     }
122 
123     private fun setupAccessibilityDelegate() {
124         accessibilityDelegate =
125             object : AccessibilityDelegate() {
126                 override fun onInitializeAccessibilityNodeInfo(
127                     v: View,
128                     info: AccessibilityNodeInfo,
129                 ) {
130                     super.onInitializeAccessibilityNodeInfo(v, info)
131                     if (
132                         interactionHandler.isLongPressHandlingEnabled &&
133                             accessibilityHintLongPressAction != null
134                     ) {
135                         info.addAction(accessibilityHintLongPressAction)
136                     }
137                 }
138 
139                 override fun performAccessibilityAction(
140                     host: View,
141                     action: Int,
142                     args: Bundle?,
143                 ): Boolean {
144                     return if (
145                         interactionHandler.isLongPressHandlingEnabled &&
146                             action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK
147                     ) {
148                         val touchHandlingView = host as? TouchHandlingView
149                         if (touchHandlingView != null) {
150                             // the coordinates are not available as it is an a11y long press
151                             listener?.onLongPressDetected(
152                                 view = touchHandlingView,
153                                 x = 0,
154                                 y = 0,
155                                 isA11yAction = true,
156                             )
157                             true
158                         } else {
159                             false
160                         }
161                     } else {
162                         super.performAccessibilityAction(host, action, args)
163                     }
164                 }
165             }
166     }
167 }
168