[Media TTT] Set specific touchable regions for the media chips so that

other touches will get correctly passed through to the window below.

Fixes: 249639836
Fixes: 249632655
Test: manual: On lockscreen, display media sender chip then verify you
can swipe down to open QS
Test: manual: On homescreen, display media receiver chip then verify you
can swipe up to see recent apps
Test: manual: verify you can still click the Undo button on the
"Transfer succeeded" chip
Test: atest SystemUITests

Change-Id: Idce510fc5b4e0c5ccd74d2b56e1d23db183ebe69
This commit is contained in:
Caitlin Shkuratov
2022-10-04 21:01:29 +00:00
parent 7d9b0e622e
commit cd5a7d1a32
10 changed files with 272 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ package com.android.systemui.media.taptotransfer.receiver
import android.annotation.SuppressLint
import android.app.StatusBarManager
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
@@ -44,6 +45,7 @@ import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.view.ViewUtil
import javax.inject.Inject
/**
@@ -63,6 +65,7 @@ class MediaTttChipControllerReceiver @Inject constructor(
powerManager: PowerManager,
@Main private val mainHandler: Handler,
private val uiEventLogger: MediaTttReceiverUiEventLogger,
private val viewUtil: ViewUtil,
) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>(
context,
logger,
@@ -83,7 +86,6 @@ class MediaTttChipControllerReceiver @Inject constructor(
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
fitInsetsTypes = 0 // Ignore insets from all system bars
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
private val commandQueueCallbacks = object : CommandQueue.Callbacks {
@@ -154,14 +156,14 @@ class MediaTttChipControllerReceiver @Inject constructor(
context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
}
val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
val iconView = currentView.getAppIconView()
iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
iconView.setImageDrawable(iconDrawable)
iconView.contentDescription = iconContentDescription
}
override fun animateViewIn(view: ViewGroup) {
val appIconView = view.requireViewById<View>(R.id.app_icon)
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(-1 * getTranslationAmount().toFloat())
.setDuration(30.frames)
@@ -175,6 +177,12 @@ class MediaTttChipControllerReceiver @Inject constructor(
startRipple(view.requireViewById(R.id.ripple))
}
override fun getTouchableRegion(view: View, outRect: Rect) {
// Even though the app icon view isn't touchable, users might think it is. So, use it as the
// touchable region to ensure that touches don't get passed to the window below.
viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect)
}
/** Returns the amount that the chip will be translated by in its intro animation. */
private fun getTranslationAmount(): Int {
return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
@@ -212,6 +220,10 @@ class MediaTttChipControllerReceiver @Inject constructor(
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
rippleView.setColor(color, 70)
}
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
}
data class ChipReceiverInfo(

View File

@@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer.sender
import android.app.StatusBarManager
import android.content.Context
import android.graphics.Rect
import android.media.MediaRoute2Info
import android.os.PowerManager
import android.util.Log
@@ -46,6 +47,7 @@ import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.view.ViewUtil
import dagger.Lazy
import javax.inject.Inject
@@ -68,6 +70,7 @@ open class MediaTttChipControllerSender @Inject constructor(
// And overcome performance issue, check [b/247817628] for details.
private val falsingManager: Lazy<FalsingManager>,
private val falsingCollector: Lazy<FalsingCollector>,
private val viewUtil: ViewUtil,
) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
context,
logger,
@@ -224,6 +227,10 @@ open class MediaTttChipControllerSender @Inject constructor(
return false
}
override fun getTouchableRegion(view: View, outRect: Rect) {
viewUtil.setRectToViewWindowLocation(view, outRect)
}
private fun Boolean.visibleIfTrue(): Int {
return if (this) {
View.VISIBLE

View File

@@ -20,10 +20,12 @@ import android.annotation.LayoutRes
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.PowerManager
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
@@ -70,7 +72,8 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
title = windowTitle
format = PixelFormat.TRANSLUCENT
setTrustedOverlay()
@@ -87,9 +90,15 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
/** The view currently being displayed. Null if the view is not being displayed. */
private var view: ViewGroup? = null
/** The view controller for [view]. Null if the view is not being displayed. */
private var viewController: TouchableRegionViewController? = null
/** The info currently being displayed. Null if the view is not being displayed. */
internal var info: T? = null
// TODO(b/245610654): We should probably group [view], [viewController], and [info] together
// into one object since they're either all null or all non-null.
/** A [Runnable] that, when run, will cancel the pending timeout of the view. */
private var cancelViewTimeout: Runnable? = null
@@ -141,6 +150,11 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
.from(context)
.inflate(viewLayoutRes, null) as ViewGroup
view = newView
val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion)
newViewController.init()
viewController = newViewController
updateView(newInfo, newView)
windowManager.addView(newView, windowLayoutParams)
animateViewIn(newView)
@@ -181,6 +195,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
// that if a new view event comes in while this view is animating out, we still display the
// new view appropriately.
view = null
viewController = null
info = null
// No need to time the view out since it's already gone
cancelViewTimeout?.run()
@@ -201,6 +216,12 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
info = newInfo
}
/**
* Fills [outRect] with the touchable region of this view. This will be used by WindowManager
* to decide which touch events go to the view.
*/
abstract fun getTouchableRegion(view: View, outRect: Rect)
/**
* A method that can be implemented by subclasses to do custom animations for when the view
* appears.

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2022 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.temporarydisplay
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver
import com.android.systemui.util.ViewController
/**
* A view controller that will notify the [ViewTreeObserver] about the touchable region for this
* view. This will be used by WindowManager to decide which touch events go to the view and which
* pass through to the window below.
*
* @param touchableRegionSetter a function that, given the view and an out rect, fills the rect with
* the touchable region of this view.
*/
class TouchableRegionViewController(
view: View,
touchableRegionSetter: (View, Rect) -> Unit,
) : ViewController<View>(view) {
private val tempRect = Rect()
private val internalInsetsListener =
ViewTreeObserver.OnComputeInternalInsetsListener { inoutInfo ->
inoutInfo.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION
)
tempRect.setEmpty()
touchableRegionSetter.invoke(mView, tempRect)
inoutInfo.touchableRegion.set(tempRect)
}
public override fun onViewAttached() {
mView.viewTreeObserver.addOnComputeInternalInsetsListener(internalInsetsListener)
}
public override fun onViewDetached() {
mView.viewTreeObserver.removeOnComputeInternalInsetsListener(internalInsetsListener)
}
}

View File

@@ -1,5 +1,22 @@
/*
* Copyright (C) 2022 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.util.view
import android.graphics.Rect
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
@@ -23,4 +40,22 @@ class ViewUtil @Inject constructor() {
top <= y &&
y <= top + view.height
}
/**
* Sets [outRect] to be the view's location within its window.
*/
fun setRectToViewWindowLocation(view: View, outRect: Rect) {
val locInWindow = IntArray(2)
view.getLocationInWindow(locInWindow)
val x = locInWindow[0]
val y = locInWindow[1]
outRect.set(
x,
y,
x + view.width,
y + view.height,
)
}
}

View File

@@ -41,6 +41,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
@@ -71,6 +72,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Mock
private lateinit var powerManager: PowerManager
@Mock
private lateinit var viewUtil: ViewUtil
@Mock
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
@@ -104,7 +107,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
configurationController,
powerManager,
Handler.getMain(),
receiverUiEventLogger
receiverUiEventLogger,
viewUtil,
)
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)

View File

@@ -47,6 +47,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 dagger.Lazy
import org.junit.Before
@@ -90,6 +91,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
private lateinit var lazyFalsingCollector: Lazy<FalsingCollector>
@Mock
private lateinit var falsingCollector: FalsingCollector
@Mock
private lateinit var viewUtil: ViewUtil
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var fakeClock: FakeSystemClock
@@ -130,7 +133,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
powerManager,
senderUiEventLogger,
lazyFalsingManager,
lazyFalsingCollector
lazyFalsingCollector,
viewUtil,
)
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
@@ -837,6 +841,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
uiEventLogger: MediaTttSenderUiEventLogger,
falsingManager: Lazy<FalsingManager>,
falsingCollector: Lazy<FalsingCollector>,
viewUtil: ViewUtil,
) : MediaTttChipControllerSender(
commandQueue,
context,
@@ -849,6 +854,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
uiEventLogger,
falsingManager,
falsingCollector,
viewUtil,
) {
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
// Just bypass the animation in tests

View File

@@ -17,7 +17,9 @@
package com.android.systemui.temporarydisplay
import android.content.Context
import android.graphics.Rect
import android.os.PowerManager
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
@@ -266,6 +268,10 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
override fun shouldIgnoreViewRemoval(removalReason: String): Boolean {
return shouldIgnoreViewRemoval
}
override fun getTouchableRegion(view: View, outRect: Rect) {
outRect.setEmpty()
}
}
inner class ViewInfo(val name: String) : TemporaryViewInfo {

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2022 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.temporarydisplay
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
class TouchableRegionViewControllerTest : SysuiTestCase() {
@Mock private lateinit var view: View
@Mock private lateinit var viewTreeObserver: ViewTreeObserver
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(view.viewTreeObserver).thenReturn(viewTreeObserver)
}
@Test
fun viewAttached_listenerAdded() {
val controller = TouchableRegionViewController(view) { _, _ -> }
controller.onViewAttached()
verify(viewTreeObserver).addOnComputeInternalInsetsListener(any())
}
@Test
fun viewDetached_listenerRemoved() {
val controller = TouchableRegionViewController(view) { _, _ -> }
controller.onViewDetached()
verify(viewTreeObserver).removeOnComputeInternalInsetsListener(any())
}
@Test
fun listener_usesPassedInFunction() {
val controller =
TouchableRegionViewController(view) { _, outRect -> outRect.set(1, 2, 3, 4) }
controller.onViewAttached()
val captor =
ArgumentCaptor.forClass(ViewTreeObserver.OnComputeInternalInsetsListener::class.java)
verify(viewTreeObserver).addOnComputeInternalInsetsListener(captor.capture())
val listener = captor.value!!
val inoutInfo = ViewTreeObserver.InternalInsetsInfo()
listener.onComputeInternalInsets(inoutInfo)
assertThat(inoutInfo.touchableRegion.bounds).isEqualTo(Rect(1, 2, 3, 4))
}
}

View File

@@ -1,12 +1,31 @@
/*
* Copyright (C) 2022 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.util.view
import android.graphics.Rect
import android.view.View
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.spy
import org.mockito.Mockito.`when`
@@ -25,6 +44,12 @@ class ViewUtilTest : SysuiTestCase() {
location[0] = VIEW_LEFT
location[1] = VIEW_TOP
`when`(view.locationOnScreen).thenReturn(location)
doAnswer { invocation ->
val pos = invocation.arguments[0] as IntArray
pos[0] = VIEW_LEFT
pos[1] = VIEW_TOP
null
}.`when`(view).getLocationInWindow(any())
}
@Test
@@ -64,6 +89,18 @@ class ViewUtilTest : SysuiTestCase() {
fun touchIsWithinView_yTooLarge_returnsFalse() {
assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse()
}
@Test
fun setRectToViewWindowLocation_rectHasLocation() {
val outRect = Rect()
viewUtil.setRectToViewWindowLocation(view, outRect)
assertThat(outRect.left).isEqualTo(VIEW_LEFT)
assertThat(outRect.right).isEqualTo(VIEW_RIGHT)
assertThat(outRect.top).isEqualTo(VIEW_TOP)
assertThat(outRect.bottom).isEqualTo(VIEW_BOTTOM)
}
}
private const val VIEW_LEFT = 30