1 /*
2  * Copyright (C) 2022 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.statusbar.gesture
19 
20 import android.annotation.CallSuper
21 import android.os.Looper
22 import android.view.Choreographer
23 import android.view.InputEvent
24 import android.view.MotionEvent
25 import com.android.systemui.shared.system.InputChannelCompat
26 import com.android.systemui.shared.system.InputMonitorCompat
27 
28 /**
29  * An abstract class to help detect gestures that occur anywhere on the display (not specific to a
30  * certain view).
31  *
32  * This class handles starting/stopping the gesture detection system as well as
33  * registering/unregistering callbacks for when gestures occur. Note that the class will only listen
34  * for gestures when there's at least one callback registered.
35  *
36  * Subclasses should implement [onInputEvent] to detect their specific gesture. Once a specific
37  * gesture is detected, they should call [onGestureDetected] (which will notify the callbacks).
38  */
39 abstract class GenericGestureDetector(
40     private val tag: String,
41     private val displayId: Int,
42 ) {
43     /**
44      * Active callbacks, each associated with a tag. Gestures will only be monitored if
45      * [callbacks.size] > 0.
46      */
47     private val callbacks: MutableMap<String, (MotionEvent) -> Unit> = mutableMapOf()
48 
49     private var inputMonitor: InputMonitorCompat? = null
50     private var inputReceiver: InputChannelCompat.InputEventReceiver? = null
51 
52     /**
53      * Adds a callback that will be triggered when the tap gesture is detected.
54      *
55      * The callback receive the last motion event in the gesture.
56      */
addOnGestureDetectedCallbacknull57     fun addOnGestureDetectedCallback(tag: String, callback: (MotionEvent) -> Unit) {
58         val callbacksWasEmpty = callbacks.isEmpty()
59         callbacks[tag] = callback
60         if (callbacksWasEmpty) {
61             startGestureListening()
62         }
63     }
64 
65     /** Removes the callback. */
removeOnGestureDetectedCallbacknull66     fun removeOnGestureDetectedCallback(tag: String) {
67         callbacks.remove(tag)
68         if (callbacks.isEmpty()) {
69             stopGestureListening()
70         }
71     }
72 
73     /** Triggered each time a touch event occurs (and at least one callback is registered). */
onInputEventnull74     abstract fun onInputEvent(ev: InputEvent)
75 
76     /**
77      * Should be called by subclasses when their specific gesture is detected with the last motion
78      * event in the gesture.
79      */
80     internal fun onGestureDetected(e: MotionEvent) {
81         callbacks.values.forEach { it.invoke(e) }
82     }
83 
84     /** Start listening to touch events. */
85     @CallSuper
startGestureListeningnull86     internal open fun startGestureListening() {
87         stopGestureListening()
88 
89         inputMonitor = InputMonitorCompat(tag, displayId).also {
90             inputReceiver = it.getInputReceiver(
91                 Looper.getMainLooper(),
92                 Choreographer.getInstance(),
93                 this::onInputEvent
94             )
95         }
96     }
97 
98     /** Stop listening to touch events. */
99     @CallSuper
stopGestureListeningnull100     internal open fun stopGestureListening() {
101         inputMonitor?.let {
102             inputMonitor = null
103             it.dispose()
104         }
105         inputReceiver?.let {
106             inputReceiver = null
107             it.dispose()
108         }
109     }
110 }
111