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.view.ViewConfiguration
21 import kotlinx.coroutines.DisposableHandle
22 
23 /** Encapsulates logic to handle complex touch interactions with a [LongPressHandlingView]. */
24 class LongPressHandlingViewInteractionHandler(
25     /**
26      * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
27      * allowing the delayed runnable to be canceled before it is run.
28      */
29     private val postDelayed: (block: Runnable, delayMs: Long) -> DisposableHandle,
30     /** Callback to be queried to check if the view is attached to its window. */
31     private val isAttachedToWindow: () -> Boolean,
32     /** Callback reporting the a long-press gesture was detected at the given coordinates. */
33     private val onLongPressDetected: (x: Int, y: Int) -> Unit,
34     /** Callback reporting the a single tap gesture was detected at the given coordinates. */
35     private val onSingleTapDetected: () -> Unit,
36 ) {
37     sealed class MotionEventModel {
38         object Other : MotionEventModel()
39 
40         data class Down(
41             val x: Int,
42             val y: Int,
43         ) : MotionEventModel()
44 
45         data class Move(
46             val distanceMoved: Float,
47         ) : MotionEventModel()
48 
49         data class Up(
50             val distanceMoved: Float,
51             val gestureDuration: Long,
52         ) : MotionEventModel()
53 
54         object Cancel : MotionEventModel()
55     }
56 
57     var isLongPressHandlingEnabled: Boolean = false
58     var scheduledLongPressHandle: DisposableHandle? = null
59 
60     fun onTouchEvent(event: MotionEventModel?): Boolean {
61         if (!isLongPressHandlingEnabled) {
62             return false
63         }
64 
65         return when (event) {
66             is MotionEventModel.Down -> {
67                 scheduleLongPress(event.x, event.y)
68                 true
69             }
70             is MotionEventModel.Move -> {
71                 if (event.distanceMoved > ViewConfiguration.getTouchSlop()) {
72                     cancelScheduledLongPress()
73                 }
74                 false
75             }
76             is MotionEventModel.Up -> {
77                 cancelScheduledLongPress()
78                 if (
79                     event.distanceMoved <= ViewConfiguration.getTouchSlop() &&
80                         event.gestureDuration < ViewConfiguration.getLongPressTimeout()
81                 ) {
82                     dispatchSingleTap()
83                 }
84                 false
85             }
86             is MotionEventModel.Cancel -> {
87                 cancelScheduledLongPress()
88                 false
89             }
90             else -> false
91         }
92     }
93 
94     private fun scheduleLongPress(
95         x: Int,
96         y: Int,
97     ) {
98         scheduledLongPressHandle =
99             postDelayed(
100                 {
101                     dispatchLongPress(
102                         x = x,
103                         y = y,
104                     )
105                 },
106                 ViewConfiguration.getLongPressTimeout().toLong(),
107             )
108     }
109 
110     private fun dispatchLongPress(
111         x: Int,
112         y: Int,
113     ) {
114         if (!isAttachedToWindow()) {
115             return
116         }
117 
118         onLongPressDetected(x, y)
119     }
120 
121     private fun cancelScheduledLongPress() {
122         scheduledLongPressHandle?.dispose()
123     }
124 
125     private fun dispatchSingleTap() {
126         if (!isAttachedToWindow()) {
127             return
128         }
129 
130         onSingleTapDetected()
131     }
132 }
133