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