[Media TTT] Don't hide the chip if the user taps the chip.

Fixes: 214274529
Fixes: 211487971
Test: manual (verify tapping chip doesn't hide chip)
Test: media.taptotransfer tests
Change-Id: Iba58f1d4adb694502a2f847c2d75779ff7fa92c0
This commit is contained in:
Caitlin Cassidy
2022-02-23 22:42:24 +00:00
parent 3aa5b9ed5a
commit 708c94fb2a
15 changed files with 203 additions and 95 deletions

View File

@@ -22,6 +22,7 @@ import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
@@ -31,6 +32,7 @@ import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.gesture.TapGestureDetector
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.view.ViewUtil
/**
* A superclass controller that provides common functionality for showing chips on the sender device
@@ -42,6 +44,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor
abstract class MediaTttChipControllerCommon<T : MediaTttChipState>(
internal val context: Context,
private val windowManager: WindowManager,
private val viewUtil: ViewUtil,
@Main private val mainExecutor: DelayableExecutor,
private val tapGestureDetector: TapGestureDetector,
@LayoutRes private val chipLayoutRes: Int
@@ -84,7 +87,7 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>(
// Add view if necessary
if (oldChipView == null) {
tapGestureDetector.addOnGestureDetectedCallback(TAG, this::removeChip)
tapGestureDetector.addOnGestureDetectedCallback(TAG, this::onScreenTapped)
windowManager.addView(chipView, windowLayoutParams)
}
@@ -127,6 +130,15 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>(
appIconView.setImageDrawable(appIcon)
appIconView.visibility = visibility
}
private fun onScreenTapped(e: MotionEvent) {
val view = chipView ?: return
// If the tap is within the chip bounds, we shouldn't hide the chip (in case users think the
// chip is tappable).
if (!viewUtil.touchIsWithinView(view, e.x, e.y)) {
removeChip()
}
}
}
// Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and

View File

@@ -31,6 +31,7 @@ import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCom
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.gesture.TapGestureDetector
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.view.ViewUtil
import javax.inject.Inject
/**
@@ -43,11 +44,17 @@ class MediaTttChipControllerReceiver @Inject constructor(
commandQueue: CommandQueue,
context: Context,
windowManager: WindowManager,
viewUtil: ViewUtil,
mainExecutor: DelayableExecutor,
tapGestureDetector: TapGestureDetector,
@Main private val mainHandler: Handler,
) : MediaTttChipControllerCommon<ChipStateReceiver>(
context, windowManager, mainExecutor, tapGestureDetector, R.layout.media_ttt_chip_receiver
context,
windowManager,
viewUtil,
mainExecutor,
tapGestureDetector,
R.layout.media_ttt_chip_receiver
) {
private val commandQueueCallbacks = object : CommandQueue.Callbacks {
override fun updateMediaTapToTransferReceiverDisplay(

View File

@@ -32,6 +32,7 @@ import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCom
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.gesture.TapGestureDetector
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.view.ViewUtil
import javax.inject.Inject
/**
@@ -43,10 +44,11 @@ class MediaTttChipControllerSender @Inject constructor(
commandQueue: CommandQueue,
context: Context,
windowManager: WindowManager,
viewUtil: ViewUtil,
@Main mainExecutor: DelayableExecutor,
tapGestureDetector: TapGestureDetector,
) : MediaTttChipControllerCommon<ChipStateSender>(
context, windowManager, mainExecutor, tapGestureDetector, R.layout.media_ttt_chip
context, windowManager, viewUtil, mainExecutor, tapGestureDetector, R.layout.media_ttt_chip
) {
private val commandQueueCallbacks = object : CommandQueue.Callbacks {
override fun updateMediaTapToTransferSenderDisplay(

View File

@@ -22,6 +22,7 @@ import android.os.Looper
import android.view.Choreographer
import android.view.Display
import android.view.InputEvent
import android.view.MotionEvent
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.shared.system.InputMonitorCompat
@@ -43,13 +44,17 @@ abstract class GenericGestureDetector(
* Active callbacks, each associated with a tag. Gestures will only be monitored if
* [callbacks.size] > 0.
*/
private val callbacks: MutableMap<String, () -> Unit> = mutableMapOf()
private val callbacks: MutableMap<String, (MotionEvent) -> Unit> = mutableMapOf()
private var inputMonitor: InputMonitorCompat? = null
private var inputReceiver: InputChannelCompat.InputEventReceiver? = null
/** Adds a callback that will be triggered when the tap gesture is detected. */
fun addOnGestureDetectedCallback(tag: String, callback: () -> Unit) {
/**
* Adds a callback that will be triggered when the tap gesture is detected.
*
* The callback receive the last motion event in the gesture.
*/
fun addOnGestureDetectedCallback(tag: String, callback: (MotionEvent) -> Unit) {
val callbacksWasEmpty = callbacks.isEmpty()
callbacks[tag] = callback
if (callbacksWasEmpty) {
@@ -68,9 +73,12 @@ abstract class GenericGestureDetector(
/** Triggered each time a touch event occurs (and at least one callback is registered). */
abstract fun onInputEvent(ev: InputEvent)
/** Should be called by subclasses when their specific gesture is detected. */
internal fun onGestureDetected() {
callbacks.values.forEach { it.invoke() }
/**
* Should be called by subclasses when their specific gesture is detected with the last motion
* event in the gesture.
*/
internal fun onGestureDetected(e: MotionEvent) {
callbacks.values.forEach { it.invoke(e) }
}
/** Start listening to touch events. */

View File

@@ -80,7 +80,7 @@ open class SwipeStatusBarAwayGestureHandler @Inject constructor(
) {
monitoringCurrentTouch = false
logger.logGestureDetected(ev.y.toInt())
onGestureDetected()
onGestureDetected(ev)
}
}
ACTION_CANCEL, ACTION_UP -> {

View File

@@ -33,8 +33,8 @@ class TapGestureDetector @Inject constructor(
) : GenericGestureDetector(TapGestureDetector::class.simpleName!!) {
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
onGestureDetected()
override fun onSingleTapUp(e: MotionEvent): Boolean {
onGestureDetected(e)
return true
}
}

View File

@@ -31,6 +31,7 @@ import com.android.systemui.unfold.UNFOLD_STATUS_BAR
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.util.ViewController
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.view.ViewUtil
import java.util.Optional
@@ -43,6 +44,7 @@ class PhoneStatusBarViewController private constructor(
@Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
private val userSwitcherController: StatusBarUserSwitcherController,
private val viewUtil: ViewUtil,
touchEventHandler: PhoneStatusBarView.TouchEventHandler,
private val configurationController: ConfigurationController
) : ViewController<PhoneStatusBarView>(view) {
@@ -118,12 +120,7 @@ class PhoneStatusBarViewController private constructor(
* view's range and false otherwise.
*/
fun touchIsWithinView(x: Float, y: Float): Boolean {
val left = mView.locationOnScreen[0]
val top = mView.locationOnScreen[1]
return left <= x &&
x <= left + mView.width &&
top <= y &&
y <= top + mView.height
return viewUtil.touchIsWithinView(mView, x, y)
}
class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
@@ -163,6 +160,7 @@ class PhoneStatusBarViewController private constructor(
@Named(UNFOLD_STATUS_BAR)
private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
private val userSwitcherController: StatusBarUserSwitcherController,
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController
) {
fun create(
@@ -174,6 +172,7 @@ class PhoneStatusBarViewController private constructor(
progressProvider.getOrNull(),
unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController(),
userSwitcherController,
viewUtil,
touchEventHandler,
configurationController
)

View File

@@ -295,7 +295,7 @@ class OngoingCallController @Inject constructor(
swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
} else {
swipeStatusBarAwayGestureHandler.ifPresent {
it.addOnGestureDetectedCallback(TAG, this::onSwipeAwayGestureDetected)
it.addOnGestureDetectedCallback(TAG) { _ -> onSwipeAwayGestureDetected() }
}
}
}

View File

@@ -0,0 +1,26 @@
package com.android.systemui.util.view
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
/**
* A class with generic view utility methods.
*
* Doesn't use static methods so that it can be easily mocked out in tests.
*/
@SysUISingleton
class ViewUtil @Inject constructor() {
/**
* Returns true if the given (x, y) point (in screen coordinates) is within the status bar
* view's range and false otherwise.
*/
fun touchIsWithinView(view: View, x: Float, y: Float): Boolean {
val left = view.locationOnScreen[0]
val top = view.locationOnScreen[1]
return left <= x &&
x <= left + view.width &&
top <= y &&
y <= top + view.height
}
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer.common
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
@@ -30,7 +31,10 @@ import com.android.systemui.statusbar.gesture.TapGestureDetector
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -39,6 +43,7 @@ import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@@ -52,6 +57,8 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() {
@Mock
private lateinit var windowManager: WindowManager
@Mock
private lateinit var viewUtil: ViewUtil
@Mock
private lateinit var tapGestureDetector: TapGestureDetector
@Before
@@ -62,7 +69,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() {
fakeExecutor = FakeExecutor(fakeClock)
controllerCommon = TestControllerCommon(
context, windowManager, fakeExecutor, tapGestureDetector
context, windowManager, viewUtil, fakeExecutor, tapGestureDetector
)
}
@@ -169,6 +176,40 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() {
.isEqualTo(state.getAppName(context))
}
@Test
fun tapGestureDetected_outsideViewBounds_viewHidden() {
controllerCommon.displayChip(getState())
whenever(viewUtil.touchIsWithinView(any(), any(), any())).thenReturn(false)
val gestureCallbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
verify(tapGestureDetector).addOnGestureDetectedCallback(
any(), capture(gestureCallbackCaptor)
)
val callback = gestureCallbackCaptor.value!!
callback.invoke(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
)
verify(windowManager).removeView(any())
}
@Test
fun tapGestureDetected_insideViewBounds_viewNotHidden() {
controllerCommon.displayChip(getState())
whenever(viewUtil.touchIsWithinView(any(), any(), any())).thenReturn(true)
val gestureCallbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
verify(tapGestureDetector).addOnGestureDetectedCallback(
any(), capture(gestureCallbackCaptor)
)
val callback = gestureCallbackCaptor.value!!
callback.invoke(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
)
verify(windowManager, never()).removeView(any())
}
private fun getState() = MediaTttChipState(PACKAGE_NAME)
private fun getChipView(): ViewGroup {
@@ -182,10 +223,11 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() {
inner class TestControllerCommon(
context: Context,
windowManager: WindowManager,
viewUtil: ViewUtil,
@Main mainExecutor: DelayableExecutor,
tapGestureDetector: TapGestureDetector,
) : MediaTttChipControllerCommon<MediaTttChipState>(
context, windowManager, mainExecutor, tapGestureDetector, R.layout.media_ttt_chip
context, windowManager, viewUtil, mainExecutor, tapGestureDetector, R.layout.media_ttt_chip
) {
override fun updateChipView(chipState: MediaTttChipState, currentChipView: ViewGroup) {
}

View File

@@ -37,6 +37,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -61,6 +62,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Mock
private lateinit var windowManager: WindowManager
@Mock
private lateinit var viewUtil: ViewUtil
@Mock
private lateinit var commandQueue: CommandQueue
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
@@ -81,6 +84,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
commandQueue,
context,
windowManager,
viewUtil,
FakeExecutor(FakeSystemClock()),
TapGestureDetector(context),
Handler.getMain(),

View File

@@ -38,6 +38,8 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -62,6 +64,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
@Mock
private lateinit var windowManager: WindowManager
@Mock
private lateinit var viewUtil: ViewUtil
@Mock
private lateinit var commandQueue: CommandQueue
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
@@ -82,6 +86,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
commandQueue,
context,
windowManager,
viewUtil,
FakeExecutor(FakeSystemClock()),
TapGestureDetector(context)
)

View File

@@ -111,7 +111,7 @@ class GenericGestureDetectorTest : SysuiTestCase() {
override fun onInputEvent(ev: InputEvent) {
if (ev is MotionEvent && ev.x == CORRECT_X) {
onGestureDetected()
onGestureDetected(ev)
}
}

View File

@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.unfold.SysUIUnfoldComponent
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -63,6 +64,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
private lateinit var configurationController: ConfigurationController
@Mock
private lateinit var userSwitcherController: StatusBarUserSwitcherController
@Mock
private lateinit var viewUtil: ViewUtil
private lateinit var view: PhoneStatusBarView
private lateinit var controller: PhoneStatusBarViewController
@@ -80,7 +83,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
val parent = FrameLayout(mContext) // add parent to keep layout params
view = LayoutInflater.from(mContext)
.inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView
view.setLeftTopRightBottom(VIEW_LEFT, VIEW_TOP, VIEW_RIGHT, VIEW_BOTTOM)
}
controller = createAndInitController(view)
@@ -111,64 +113,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
verify(moveFromCenterAnimation).onViewsReady(any())
}
@Test
fun touchIsWithinView_inBounds_returnsTrue() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP + 1f)).isTrue()
}
@Test
fun touchIsWithinView_onTopLeftCorner_returnsTrue() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_LEFT.toFloat(), VIEW_TOP.toFloat())).isTrue()
}
@Test
fun touchIsWithinView_onBottomRightCorner_returnsTrue() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(
VIEW_RIGHT.toFloat(), VIEW_BOTTOM.toFloat())
).isTrue()
}
@Test
fun touchIsWithinView_xTooSmall_returnsFalse() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_LEFT - 1f, VIEW_TOP + 1f)).isFalse()
}
@Test
fun touchIsWithinView_xTooLarge_returnsFalse() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_RIGHT + 1f, VIEW_TOP + 1f)).isFalse()
}
@Test
fun touchIsWithinView_yTooSmall_returnsFalse() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP - 1f)).isFalse()
}
@Test
fun touchIsWithinView_yTooLarge_returnsFalse() {
val view = createViewMockWithScreenLocation()
controller = createAndInitController(view)
assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse()
}
private fun createViewMock(): PhoneStatusBarView {
val view = spy(view)
val viewTreeObserver = mock(ViewTreeObserver::class.java)
@@ -177,20 +121,12 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
return view
}
private fun createViewMockWithScreenLocation(): PhoneStatusBarView {
val view = spy(view)
val location = IntArray(2)
location[0] = VIEW_LEFT
location[1] = VIEW_TOP
`when`(view.locationOnScreen).thenReturn(location)
return view
}
private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController {
return PhoneStatusBarViewController.Factory(
Optional.of(sysuiUnfoldComponent),
Optional.of(progressProvider),
userSwitcherController,
viewUtil,
configurationController
).create(view, touchEventHandler).also {
it.init()
@@ -215,8 +151,3 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
}
}
}
private const val VIEW_LEFT = 30
private const val VIEW_RIGHT = 100
private const val VIEW_TOP = 40
private const val VIEW_BOTTOM = 100

View File

@@ -0,0 +1,72 @@
package com.android.systemui.util.view
import android.view.View
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.spy
import org.mockito.Mockito.`when`
@SmallTest
class ViewUtilTest : SysuiTestCase() {
private val viewUtil = ViewUtil()
private lateinit var view: View
@Before
fun setUp() {
view = TextView(context)
view.setLeftTopRightBottom(VIEW_LEFT, VIEW_TOP, VIEW_RIGHT, VIEW_BOTTOM)
view = spy(view)
val location = IntArray(2)
location[0] = VIEW_LEFT
location[1] = VIEW_TOP
`when`(view.locationOnScreen).thenReturn(location)
}
@Test
fun touchIsWithinView_inBounds_returnsTrue() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT + 1f, VIEW_TOP + 1f)).isTrue()
}
@Test
fun touchIsWithinView_onTopLeftCorner_returnsTrue() {
assertThat(viewUtil.touchIsWithinView(
view, VIEW_LEFT.toFloat(), VIEW_TOP.toFloat())
).isTrue()
}
@Test
fun touchIsWithinView_onBottomRightCorner_returnsTrue() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_RIGHT.toFloat(), VIEW_BOTTOM.toFloat()))
.isTrue()
}
@Test
fun touchIsWithinView_xTooSmall_returnsFalse() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT - 1f, VIEW_TOP + 1f)).isFalse()
}
@Test
fun touchIsWithinView_xTooLarge_returnsFalse() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_RIGHT + 1f, VIEW_TOP + 1f)).isFalse()
}
@Test
fun touchIsWithinView_yTooSmall_returnsFalse() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT + 1f, VIEW_TOP - 1f)).isFalse()
}
@Test
fun touchIsWithinView_yTooLarge_returnsFalse() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse()
}
}
private const val VIEW_LEFT = 30
private const val VIEW_RIGHT = 100
private const val VIEW_TOP = 40
private const val VIEW_BOTTOM = 100