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