[SB][Chips] Allow swipe to open shade over status bar chip.
Typically, there's nothing touchable in the status bar. So,
PhoneStatusBarView#onInterceptTouchEvent is only called once for the
DOWN event and never again. When there's no status bar chip, that
behavior won't change.
When there *is* a status bar chip, there are now touchable items in
PhoneStatusBarView. When a gesture is occuring in the chip area,
PSBV#onInterceptTouchEvent will be called for all the events in
the gesture and all of them (DOWN, MOVE, etc.) will be sent to
ShadeViewController (aka NotificationPanelViewController).
If NotificationPanelViewController decides that the gesture is a swipe,
then the event will be intercepted (#onInterceptTouchEvent returns
true). The ongoing call chip will get a CANCEL event (so the chip's
click listener won't trigger) and NPVC will take over showing the shade
expansion animation.
Fixes: 185897191
Bug: 332662551
Flag: com.android.systemui.status_bar_swipe_over_chip
Most important tests:
Test: launcher, start swipe above chip -> verify QQS
expands
Test: in app, start swipe above chip -> verify QQS expands
Test: launcher, start swipe on chip -> verify QQS expands
Test: in app, start swipe on chip -> verify QQS
expands
Test: tap on ongoing call chip -> verify the app that posted the call
notification opens
Test: tap on screen share chip -> verify stop share dialog appears
Test: atest PhoneStatusBarViewTest PhoneStatusBarViewControllerTest
Other tests, pulled from the shade CUJ list:
Test: in immersive app, swipe once -> verify status bar shows. swipe
again -> verify QQS expands (swipe with both 1 and 2 fingers)
Test: keyguard, swipe down from top -> verify QS appears (both 1 and 2
fingers)
Test: launcher, 2 finger swipe down from top -> verify QS expands
Test: launcher, 2 finger swipe down from top with finger on ongoing
call chip -> verify QS expands
Test: HUN displayed, start swipe in different part of status bar ->
verify QQS expands
Change-Id: I555eb27d8f06480fee7f57aebb9aa8ce6b64c1c7
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0314992..23fa40c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -377,6 +377,16 @@
}
flag {
+ name: "status_bar_swipe_over_chip"
+ namespace: "systemui"
+ description: "Allow users to swipe over the status bar chip to open the shade"
+ bug: "185897191"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "compose_bouncer"
namespace: "systemui"
description: "Use the new compose bouncer in SystemUI"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 8115c36..f178708 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -36,6 +36,7 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.Dependency;
+import com.android.systemui.Flags;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
@@ -217,8 +218,12 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
- mTouchEventHandler.onInterceptTouchEvent(event);
- return super.onInterceptTouchEvent(event);
+ if (Flags.statusBarSwipeOverChip()) {
+ return mTouchEventHandler.onInterceptTouchEvent(event);
+ } else {
+ mTouchEventHandler.onInterceptTouchEvent(event);
+ return super.onInterceptTouchEvent(event);
+ }
}
public void updateResources() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 5206e46..a818c05 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -23,9 +23,10 @@
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
+import com.android.systemui.Flags
import com.android.systemui.Gefingerpoken
import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.flags.Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.ui.view.WindowRootView
@@ -83,22 +84,25 @@
statusContainer.setOnHoverListener(
statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)
)
- statusContainer.setOnTouchListener(object : View.OnTouchListener {
- override fun onTouch(v: View, event: MotionEvent): Boolean {
- // We want to handle only mouse events here to avoid stealing finger touches from
- // status bar which expands shade when swiped down on. We're using onTouchListener
- // instead of onClickListener as the later will lead to isClickable being set to
- // true and hence ALL touches always being intercepted. See [View.OnTouchEvent]
- if (event.source == InputDevice.SOURCE_MOUSE) {
- if (event.action == MotionEvent.ACTION_UP) {
- v.performClick()
- shadeController.animateExpandShade()
+ statusContainer.setOnTouchListener(
+ object : View.OnTouchListener {
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ // We want to handle only mouse events here to avoid stealing finger touches
+ // from status bar which expands shade when swiped down on. See b/326097469.
+ // We're using onTouchListener instead of onClickListener as the later will lead
+ // to isClickable being set to true and hence ALL touches always being
+ // intercepted. See [View.OnTouchEvent]
+ if (event.source == InputDevice.SOURCE_MOUSE) {
+ if (event.action == MotionEvent.ACTION_UP) {
+ v.performClick()
+ shadeController.animateExpandShade()
+ }
+ return true
}
- return true
+ return false
}
- return false
}
- })
+ )
progressProvider?.setReadyToHandleTransition(true)
configurationController.addCallback(configurationListener)
@@ -180,8 +184,12 @@
inner class PhoneStatusBarViewTouchHandler : Gefingerpoken {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
- onTouch(event)
- return false
+ return if (Flags.statusBarSwipeOverChip()) {
+ shadeViewController.handleExternalInterceptTouch(event)
+ } else {
+ onTouch(event)
+ false
+ }
}
override fun onTouchEvent(event: MotionEvent): Boolean {
@@ -280,7 +288,7 @@
) {
fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
val statusBarMoveFromCenterAnimationController =
- if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
+ if (featureFlags.isEnabled(ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
} else {
null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 25314f3..5b45781 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -20,6 +20,8 @@
import android.app.StatusBarManager.WINDOW_STATE_HIDING
import android.app.StatusBarManager.WINDOW_STATE_SHOWING
import android.app.StatusBarManager.WINDOW_STATUS_BAR
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.InputDevice
import android.view.LayoutInflater
import android.view.MotionEvent
@@ -104,7 +106,7 @@
val parent = FrameLayout(mContext) // add parent to keep layout params
view =
LayoutInflater.from(mContext).inflate(R.layout.status_bar, parent, false)
- as PhoneStatusBarView
+ as PhoneStatusBarView
controller = createAndInitController(view)
}
}
@@ -112,8 +114,8 @@
@Test
fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() {
val view = createViewMock()
- val argumentCaptor = ArgumentCaptor.forClass(
- ConfigurationController.ConfigurationListener::class.java)
+ val argumentCaptor =
+ ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
controller = createAndInitController(view)
}
@@ -159,7 +161,7 @@
fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false)
val returnVal =
- view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
+ view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0))
assertThat(returnVal).isFalse()
verify(shadeViewController, never()).handleExternalTouch(any())
}
@@ -169,7 +171,7 @@
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
val returnVal =
- view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
+ view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0))
assertThat(returnVal).isTrue()
verify(shadeViewController, never()).handleExternalTouch(any())
}
@@ -178,7 +180,7 @@
fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
- val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 2f, 0)
view.onTouchEvent(event)
@@ -208,6 +210,50 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOff_viewReturnsFalse() {
+ `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(false)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
+
+ val returnVal = view.onInterceptTouchEvent(event)
+
+ assertThat(returnVal).isFalse()
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOn_viewReturnsFalse() {
+ `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(false)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
+
+ val returnVal = view.onInterceptTouchEvent(event)
+
+ assertThat(returnVal).isFalse()
+ }
+
+ @Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOff_viewReturnsFalse() {
+ `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(true)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
+
+ val returnVal = view.onInterceptTouchEvent(event)
+
+ assertThat(returnVal).isFalse()
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOn_viewReturnsTrue() {
+ `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(true)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
+
+ val returnVal = view.onInterceptTouchEvent(event)
+
+ assertThat(returnVal).isTrue()
+ }
+
+ @Test
fun onTouch_windowHidden_centralSurfacesNotNotified() {
val callback = getCommandQueueCallback()
callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_HIDDEN)
@@ -244,9 +290,7 @@
controller = createAndInitController(view)
}
val statusContainer = view.requireViewById<View>(R.id.system_icons)
- statusContainer.dispatchTouchEvent(
- getActionUpEventFromSource(InputDevice.SOURCE_MOUSE)
- )
+ statusContainer.dispatchTouchEvent(getActionUpEventFromSource(InputDevice.SOURCE_MOUSE))
verify(shadeControllerImpl).animateExpandShade()
}
@@ -257,9 +301,10 @@
controller = createAndInitController(view)
}
val statusContainer = view.requireViewById<View>(R.id.system_icons)
- val handled = statusContainer.dispatchTouchEvent(
- getActionUpEventFromSource(InputDevice.SOURCE_TOUCHSCREEN)
- )
+ val handled =
+ statusContainer.dispatchTouchEvent(
+ getActionUpEventFromSource(InputDevice.SOURCE_TOUCHSCREEN)
+ )
assertThat(handled).isFalse()
}
@@ -295,21 +340,21 @@
private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController {
return PhoneStatusBarViewController.Factory(
- Optional.of(sysuiUnfoldComponent),
- Optional.of(progressProvider),
- featureFlags,
- userChipViewModel,
- centralSurfacesImpl,
- statusBarWindowStateController,
- shadeControllerImpl,
- shadeViewController,
- panelExpansionInteractor,
- windowRootView,
- shadeLogger,
- viewUtil,
- configurationController,
- mStatusOverlayHoverListenerFactory
- )
+ Optional.of(sysuiUnfoldComponent),
+ Optional.of(progressProvider),
+ featureFlags,
+ userChipViewModel,
+ centralSurfacesImpl,
+ statusBarWindowStateController,
+ shadeControllerImpl,
+ shadeViewController,
+ panelExpansionInteractor,
+ windowRootView,
+ shadeLogger,
+ viewUtil,
+ configurationController,
+ mStatusOverlayHoverListenerFactory
+ )
.create(view)
.also { it.init() }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index eae4f23..abc50bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -19,6 +19,8 @@
import android.content.res.Configuration
import android.graphics.Insets
import android.graphics.Rect
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper.RunWithLooper
import android.view.DisplayCutout
import android.view.DisplayShape
@@ -30,6 +32,7 @@
import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP
import com.android.systemui.Gefingerpoken
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.DarkIconDispatcher
@@ -82,7 +85,8 @@
}
@Test
- fun onInterceptTouchEvent_listenerNotified() {
+ @DisableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_flagOff_listenerNotified() {
val handler = TestTouchEventHandler()
view.setTouchEventHandler(handler)
@@ -93,6 +97,66 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_flagOn_listenerNotified() {
+ val handler = TestTouchEventHandler()
+ view.setTouchEventHandler(handler)
+
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+ view.onInterceptTouchEvent(event)
+
+ assertThat(handler.lastInterceptEvent).isEqualTo(event)
+ }
+
+ @Test
+ @DisableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_listenerReturnsFalse_flagOff_viewReturnsFalse() {
+ val handler = TestTouchEventHandler()
+ view.setTouchEventHandler(handler)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+ handler.handleTouchReturnValue = false
+
+ assertThat(view.onInterceptTouchEvent(event)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_listenerReturnsFalse_flagOn_viewReturnsFalse() {
+ val handler = TestTouchEventHandler()
+ view.setTouchEventHandler(handler)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+ handler.handleTouchReturnValue = false
+
+ assertThat(view.onInterceptTouchEvent(event)).isFalse()
+ }
+
+ @Test
+ @DisableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_listenerReturnsTrue_flagOff_viewReturnsFalse() {
+ val handler = TestTouchEventHandler()
+ view.setTouchEventHandler(handler)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+ handler.handleTouchReturnValue = true
+
+ assertThat(view.onInterceptTouchEvent(event)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_SWIPE_OVER_CHIP)
+ fun onInterceptTouchEvent_listenerReturnsTrue_flagOn_viewReturnsTrue() {
+ val handler = TestTouchEventHandler()
+ view.setTouchEventHandler(handler)
+ val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+
+ handler.handleTouchReturnValue = true
+
+ assertThat(view.onInterceptTouchEvent(event)).isTrue()
+ }
+
+ @Test
fun onTouchEvent_listenerReturnsTrue_viewReturnsTrue() {
val handler = TestTouchEventHandler()
view.setTouchEventHandler(handler)