1 /*
2  * Copyright (C) 2021 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 package com.android.systemui.statusbar.gesture
18 
19 import android.content.Context
20 import android.os.Looper
21 import android.view.Choreographer
22 import android.view.Display
23 import android.view.InputEvent
24 import android.view.MotionEvent
25 import android.view.MotionEvent.*
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.shared.system.InputChannelCompat
28 import com.android.systemui.shared.system.InputMonitorCompat
29 import com.android.systemui.statusbar.window.StatusBarWindowController
30 import javax.inject.Inject
31 
32 /**
33  * A class to detect when a user swipes away the status bar. To be notified when the swipe away
34  * gesture is detected, add a callback via [addOnGestureDetectedCallback].
35  */
36 @SysUISingleton
37 open class SwipeStatusBarAwayGestureHandler @Inject constructor(
38     context: Context,
39     private val statusBarWindowController: StatusBarWindowController,
40     private val logger: SwipeStatusBarAwayGestureLogger
41 ) {
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, () -> Unit> = mutableMapOf()
48 
49     private var startY: Float = 0f
50     private var startTime: Long = 0L
51     private var monitoringCurrentTouch: Boolean = false
52 
53     private var inputMonitor: InputMonitorCompat? = null
54     private var inputReceiver: InputChannelCompat.InputEventReceiver? = null
55 
56     private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
57         com.android.internal.R.dimen.system_gestures_start_threshold
58     )
59 
60     /** Adds a callback that will be triggered when the swipe away gesture is detected. */
61     fun addOnGestureDetectedCallback(tag: String, callback: () -> Unit) {
62         val callbacksWasEmpty = callbacks.isEmpty()
63         callbacks[tag] = callback
64         if (callbacksWasEmpty) {
65             startGestureListening()
66         }
67     }
68 
69     /** Removes the callback. */
70     fun removeOnGestureDetectedCallback(tag: String) {
71         callbacks.remove(tag)
72         if (callbacks.isEmpty()) {
73              stopGestureListening()
74         }
75     }
76 
77     private fun onInputEvent(ev: InputEvent) {
78         if (ev !is MotionEvent) {
79             return
80         }
81 
82         when (ev.actionMasked) {
83             ACTION_DOWN -> {
84                 if (
85                     // Gesture starts just below the status bar
86                     ev.y >= statusBarWindowController.statusBarHeight
87                     && ev.y <= 3 * statusBarWindowController.statusBarHeight
88                 ) {
89                     logger.logGestureDetectionStarted(ev.y.toInt())
90                     startY = ev.y
91                     startTime = ev.eventTime
92                     monitoringCurrentTouch = true
93                 } else {
94                     monitoringCurrentTouch = false
95                 }
96             }
97             ACTION_MOVE -> {
98                 if (!monitoringCurrentTouch) {
99                     return
100                 }
101                 if (
102                     // Gesture is up
103                     ev.y < startY
104                     // Gesture went far enough
105                     && (startY - ev.y) >= swipeDistanceThreshold
106                     // Gesture completed quickly enough
107                     && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
108                 ) {
109                     monitoringCurrentTouch = false
110                     logger.logGestureDetected(ev.y.toInt())
111                     callbacks.values.forEach { it.invoke() }
112                 }
113             }
114             ACTION_CANCEL, ACTION_UP -> {
115                 if (monitoringCurrentTouch) {
116                     logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
117                 }
118                 monitoringCurrentTouch = false
119             }
120         }
121     }
122 
123     /** Start listening for the swipe gesture. */
124     private fun startGestureListening() {
125         stopGestureListening()
126 
127         logger.logInputListeningStarted()
128         inputMonitor = InputMonitorCompat(TAG, Display.DEFAULT_DISPLAY).also {
129             inputReceiver = it.getInputReceiver(
130                 Looper.getMainLooper(),
131                 Choreographer.getInstance(),
132                 this::onInputEvent
133             )
134         }
135     }
136 
137     /** Stop listening for the swipe gesture. */
138     private fun stopGestureListening() {
139         inputMonitor?.let {
140             logger.logInputListeningStopped()
141             inputMonitor = null
142             it.dispose()
143         }
144         inputReceiver?.let {
145             inputReceiver = null
146             it.dispose()
147         }
148     }
149 }
150 
151 private const val SWIPE_TIMEOUT_MS: Long = 500
152 private val TAG = SwipeStatusBarAwayGestureHandler::class.simpleName
153