From de8b2434bb2b86c56d9d07f52724e90f8430bd49 Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Thu, 7 Oct 2021 20:18:43 +0000 Subject: [PATCH] [Ongoing Call] Hide the status bar in immersive mode if the user has swiped it away. Test: Manual (see video in b/195839150#comment6) Test: TODO unit tests if general structure is approved Bug: 195839150 Change-Id: Iee58416973becf9d66db6f50c7512cf65722766b --- .../dagger/StatusBarDependenciesModule.java | 21 ++- .../phone/StatusBarWindowController.java | 20 ++- .../ongoingcall/OngoingCallController.kt | 41 ++++- .../SwipeStatusBarAwayGestureHandler.kt | 153 ++++++++++++++++++ .../ongoingcall/OngoingCallControllerTest.kt | 28 +++- 5 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/SwipeStatusBarAwayGestureHandler.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 0352212ced1bd..452e7370fc301 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -69,6 +69,7 @@ import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; +import com.android.systemui.statusbar.phone.ongoingcall.SwipeStatusBarAwayGestureHandler; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -255,15 +256,29 @@ public interface StatusBarDependenciesModule { IActivityManager iActivityManager, OngoingCallLogger logger, DumpManager dumpManager, - StatusBarWindowController statusBarWindowController) { + StatusBarWindowController statusBarWindowController, + SwipeStatusBarAwayGestureHandler swipeStatusBarAwayGestureHandler) { Optional windowController = featureFlags.isOngoingCallInImmersiveEnabled() ? Optional.of(statusBarWindowController) : Optional.empty(); + Optional gestureHandler = + featureFlags.isOngoingCallInImmersiveEnabled() + ? Optional.of(swipeStatusBarAwayGestureHandler) + : Optional.empty(); OngoingCallController ongoingCallController = new OngoingCallController( - notifCollection, featureFlags, systemClock, activityStarter, mainExecutor, - iActivityManager, logger, dumpManager, windowController); + notifCollection, + featureFlags, + systemClock, + activityStarter, + mainExecutor, + iActivityManager, + logger, + dumpManager, + windowController, + gestureHandler + ); ongoingCallController.init(); return ongoingCallController; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java index b708861ecc744..235a8e85a223e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java @@ -208,9 +208,18 @@ public class StatusBarWindowController { apply(mCurrentState); } - /** Sets whether there is currently an ongoing call. */ - public void setIsCallOngoing(boolean isCallOngoing) { - mCurrentState.mIsCallOngoing = isCallOngoing; + /** + * Sets whether an ongoing process requires the status bar to be forced visible. + * + * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing + * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to + * false but this method is set to true, then the status bar **will** be visible. + * + * TODO(b/195839150): We should likely merge this method and + * {@link this#setForceStatusBarVisible} together and use some sort of ranking system instead. + */ + public void setOngoingProcessRequiresStatusBarVisible(boolean visible) { + mCurrentState.mOngoingProcessRequiresStatusBarVisible = visible; apply(mCurrentState); } @@ -254,13 +263,14 @@ public class StatusBarWindowController { private static class State { boolean mForceStatusBarVisible; boolean mIsLaunchAnimationRunning; - boolean mIsCallOngoing; + boolean mOngoingProcessRequiresStatusBarVisible; } private void applyForceStatusBarVisibleFlag(State state) { if (state.mForceStatusBarVisible || state.mIsLaunchAnimationRunning - || state.mIsCallOngoing) { + // Don't force-show the status bar if the user has already dismissed it. + || state.mOngoingProcessRequiresStatusBarVisible) { mLpChanged.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; } else { mLpChanged.privateFlags &= ~PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index b7c80def7dad4..c3c935e2ab7e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -60,6 +60,7 @@ class OngoingCallController @Inject constructor( private val logger: OngoingCallLogger, private val dumpManager: DumpManager, private val statusBarWindowController: Optional, + private val swipeStatusBarAwayGestureHandler: Optional, ) : CallbackController, Dumpable { /** Non-null if there's an active call notification. */ @@ -96,7 +97,8 @@ class OngoingCallController @Inject constructor( entry.sbn.notification.contentIntent?.intent, entry.sbn.uid, entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING + Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING, + statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false ) if (newOngoingCallInfo == callNotificationInfo) { return @@ -202,9 +204,16 @@ class OngoingCallController @Inject constructor( ) } } - setUpUidObserver(currentCallNotificationInfo) - statusBarWindowController.ifPresent { it.setIsCallOngoing(true) } + if (!currentCallNotificationInfo.statusBarSwipedAway) { + statusBarWindowController.ifPresent { + it.setOngoingProcessRequiresStatusBarVisible(true) + } + // TODO(b/195839150): Only listen for the gesture when in immersive mode. + swipeStatusBarAwayGestureHandler.ifPresent { + it.addOnGestureDetectedCallback(TAG, this::onSwipeAwayGestureDetected) + } + } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } } else { // If we failed to update the chip, don't store the call info. Then [hasOngoingCall] @@ -271,7 +280,8 @@ class OngoingCallController @Inject constructor( private fun removeChip() { callNotificationInfo = null tearDownChipView() - statusBarWindowController.ifPresent { it.setIsCallOngoing(false) } + statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) } + swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } if (uidObserver != null) { iActivityManager.unregisterUidObserver(uidObserver) @@ -286,13 +296,34 @@ class OngoingCallController @Inject constructor( return this.findViewById(R.id.ongoing_call_chip_time) } + /** + * If there's an active ongoing call, then we will force the status bar to always show, even if + * the user is in immersive mode. However, we also want to give users the ability to swipe away + * the status bar if they need to access the area under the status bar. + * + * This method updates the status bar window appropriately when the swipe away gesture is + * detected. + */ + private fun onSwipeAwayGestureDetected() { + if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") } + callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true) + statusBarWindowController.ifPresent { + it.setOngoingProcessRequiresStatusBarVisible(false) + } + swipeStatusBarAwayGestureHandler.ifPresent { + it.removeOnGestureDetectedCallback(TAG) + } + } + private data class CallNotificationInfo( val key: String, val callStartTime: Long, val intent: Intent?, val uid: Int, /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */ - val isOngoing: Boolean + val isOngoing: Boolean, + /** True if the user has swiped away the status bar while in this phone call. */ + val statusBarSwipedAway: Boolean ) { /** * Returns true if the notification information has a valid call start time. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/SwipeStatusBarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/SwipeStatusBarAwayGestureHandler.kt new file mode 100644 index 0000000000000..c97cf14b743d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/SwipeStatusBarAwayGestureHandler.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.ongoingcall + +import android.content.Context +import android.os.Looper +import android.util.Log +import android.view.Choreographer +import android.view.Display +import android.view.InputEvent +import android.view.MotionEvent +import android.view.MotionEvent.* +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shared.system.InputChannelCompat +import com.android.systemui.shared.system.InputMonitorCompat +import com.android.systemui.statusbar.phone.StatusBarWindowController +import javax.inject.Inject + +/** + * A class to detect when a user swipes away the status bar. To be notified when the swipe away + * gesture is detected, add a callback via [addOnGestureDetectedCallback]. + */ +@SysUISingleton +open class SwipeStatusBarAwayGestureHandler @Inject constructor( + context: Context, + private val statusBarWindowController: StatusBarWindowController, +) { + + /** + * Active callbacks, each associated with a tag. Gestures will only be monitored if + * [callbacks.size] > 0. + */ + private val callbacks: MutableMap Unit> = mutableMapOf() + + private var startY: Float = 0f + private var startTime: Long = 0L + private var monitoringCurrentTouch: Boolean = false + + private var inputMonitor: InputMonitorCompat? = null + private var inputReceiver: InputChannelCompat.InputEventReceiver? = null + + // TODO(b/195839150): Update this threshold when the config changes? + private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.system_gestures_start_threshold + ) + + /** Adds a callback that will be triggered when the swipe away gesture is detected. */ + fun addOnGestureDetectedCallback(tag: String, callback: () -> Unit) { + val callbacksWasEmpty = callbacks.isEmpty() + callbacks[tag] = callback + if (callbacksWasEmpty) { + startGestureListening() + } + } + + /** Removes the callback. */ + fun removeOnGestureDetectedCallback(tag: String) { + callbacks.remove(tag) + if (callbacks.isEmpty()) { + stopGestureListening() + } + } + + private fun onInputEvent(ev: InputEvent) { + if (ev !is MotionEvent) { + return + } + + when (ev.actionMasked) { + ACTION_DOWN -> { + if ( + // Gesture starts just below the status bar + // TODO(b/195839150): Is [statusBarHeight] the correct dimension to use for + // determining which down touches are valid? + ev.y >= statusBarWindowController.statusBarHeight + && ev.y <= 3 * statusBarWindowController.statusBarHeight + ) { + Log.d(TAG, "Beginning gesture detection, y=${ev.y}") + startY = ev.y + startTime = ev.eventTime + monitoringCurrentTouch = true + } else { + monitoringCurrentTouch = false + } + } + ACTION_MOVE -> { + if (!monitoringCurrentTouch) { + return + } + if ( + // Gesture is up + ev.y < startY + // Gesture went far enough + && (startY - ev.y) >= swipeDistanceThreshold + // Gesture completed quickly enough + && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS + ) { + Log.i(TAG, "Gesture detected; notifying callbacks") + callbacks.values.forEach { it.invoke() } + monitoringCurrentTouch = false + } + } + ACTION_CANCEL, ACTION_UP -> { + monitoringCurrentTouch = false + } + } + } + + /** Start listening for the swipe gesture. */ + private fun startGestureListening() { + stopGestureListening() + + if (DEBUG) { Log.d(TAG, "Input listening started") } + inputMonitor = InputMonitorCompat(TAG, Display.DEFAULT_DISPLAY).also { + inputReceiver = it.getInputReceiver( + Looper.getMainLooper(), + Choreographer.getInstance(), + this::onInputEvent + ) + } + } + + /** Stop listening for the swipe gesture. */ + private fun stopGestureListening() { + inputMonitor?.let { + if (DEBUG) { Log.d(TAG, "Input listening stopped") } + inputMonitor = null + it.dispose() + } + inputReceiver?.let { + inputReceiver = null + it.dispose() + } + } +} + +private const val SWIPE_TIMEOUT_MS: Long = 500 +private val TAG = SwipeStatusBarAwayGestureHandler::class.simpleName +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index e97aba2816e16..be2787670d37a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -50,8 +50,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.nullable +import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.eq @@ -83,6 +82,7 @@ class OngoingCallControllerTest : SysuiTestCase() { private lateinit var controller: OngoingCallController private lateinit var notifCollectionListener: NotifCollectionListener + @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockIActivityManager: IActivityManager @@ -112,6 +112,7 @@ class OngoingCallControllerTest : SysuiTestCase() { OngoingCallLogger(uiEventLoggerFake), DumpManager(), Optional.of(mockStatusBarWindowController), + Optional.of(mockSwipeStatusBarAwayGestureHandler), ) controller.init() controller.addCallback(mockOngoingCallListener) @@ -141,7 +142,15 @@ class OngoingCallControllerTest : SysuiTestCase() { fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - verify(mockStatusBarWindowController).setIsCallOngoing(true) + verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) + } + + @Test + fun onEntryUpdated_isOngoingCallNotif_swipeGestureCallbackAdded() { + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + verify(mockSwipeStatusBarAwayGestureHandler) + .addOnGestureDetectedCallback(anyString(), any()) } @Test @@ -242,7 +251,18 @@ class OngoingCallControllerTest : SysuiTestCase() { notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - verify(mockStatusBarWindowController).setIsCallOngoing(false) + verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) + } + + @Test + fun onEntryUpdated_callNotifAddedThenRemoved_swipeGestureCallbackRemoved() { + val ongoingCallNotifEntry = createOngoingCallNotifEntry() + notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) + + notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) + + verify(mockSwipeStatusBarAwayGestureHandler) + .removeOnGestureDetectedCallback(anyString()) } /** Regression test for b/188491504. */