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.common.ui.view
19 
20 import android.annotation.SuppressLint
21 import android.content.Context
22 import android.util.AttributeSet
23 import android.view.MotionEvent
24 import android.view.View
25 import com.android.systemui.shade.TouchLogger
26 import kotlin.math.pow
27 import kotlin.math.sqrt
28 import kotlinx.coroutines.DisposableHandle
29 
30 /**
31  * View designed to handle long-presses.
32  *
33  * The view will not handle any long pressed by default. To set it up, set up a listener and, when
34  * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
35  */
36 class LongPressHandlingView(
37     context: Context,
38     attrs: AttributeSet?,
39 ) :
40     View(
41         context,
42         attrs,
43     ) {
44     interface Listener {
45         /** Notifies that a long-press has been detected by the given view. */
46         fun onLongPressDetected(
47             view: View,
48             x: Int,
49             y: Int,
50         )
51 
52         /** Notifies that the gesture was too short for a long press, it is actually a click. */
53         fun onSingleTapDetected(view: View) = Unit
54     }
55 
56     var listener: Listener? = null
57 
58     private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
59         LongPressHandlingViewInteractionHandler(
60             postDelayed = { block, timeoutMs ->
61                 val dispatchToken = Any()
62 
63                 handler.postDelayed(
64                     block,
65                     dispatchToken,
66                     timeoutMs,
67                 )
68 
69                 DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) }
70             },
71             isAttachedToWindow = ::isAttachedToWindow,
72             onLongPressDetected = { x, y ->
73                 listener?.onLongPressDetected(
74                     view = this,
75                     x = x,
76                     y = y,
77                 )
78             },
79             onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) },
80         )
81     }
82 
83     fun setLongPressHandlingEnabled(isEnabled: Boolean) {
84         interactionHandler.isLongPressHandlingEnabled = isEnabled
85     }
86 
87     override fun dispatchTouchEvent(event: MotionEvent): Boolean {
88         return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
89     }
90 
91     @SuppressLint("ClickableViewAccessibility")
92     override fun onTouchEvent(event: MotionEvent?): Boolean {
93         return interactionHandler.onTouchEvent(event?.toModel())
94     }
95 }
96 
97 private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
98     return when (actionMasked) {
99         MotionEvent.ACTION_DOWN ->
100             LongPressHandlingViewInteractionHandler.MotionEventModel.Down(
101                 x = x.toInt(),
102                 y = y.toInt(),
103             )
104         MotionEvent.ACTION_MOVE ->
105             LongPressHandlingViewInteractionHandler.MotionEventModel.Move(
106                 distanceMoved = distanceMoved(),
107             )
108         MotionEvent.ACTION_UP ->
109             LongPressHandlingViewInteractionHandler.MotionEventModel.Up(
110                 distanceMoved = distanceMoved(),
111                 gestureDuration = gestureDuration(),
112             )
113         MotionEvent.ACTION_CANCEL -> LongPressHandlingViewInteractionHandler.MotionEventModel.Cancel
114         else -> LongPressHandlingViewInteractionHandler.MotionEventModel.Other
115     }
116 }
117 
118 private fun MotionEvent.distanceMoved(): Float {
119     return if (historySize > 0) {
120         sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
121     } else {
122         0f
123     }
124 }
125 
126 private fun MotionEvent.gestureDuration(): Long {
127     return eventTime - downTime
128 }
129