diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 8fc5519cc73e7..511c4bf6f949e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -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( 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 { @@ -140,8 +142,6 @@ class MediaTttChipControllerReceiver @Inject constructor( } override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) { - super.updateView(newInfo, currentView) - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( context, newInfo.routeInfo.clientPackageName, logger ) @@ -154,14 +154,14 @@ class MediaTttChipControllerReceiver @Inject constructor( context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding) } - val iconView = currentView.requireViewById(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(R.id.app_icon) + val appIconView = view.getAppIconView() appIconView.animate() .translationYBy(-1 * getTranslationAmount().toFloat()) .setDuration(30.frames) @@ -175,6 +175,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) @@ -202,16 +208,19 @@ class MediaTttChipControllerReceiver @Inject constructor( private fun layoutRipple(rippleView: ReceiverChipRippleView) { val windowBounds = windowManager.currentWindowMetrics.bounds - val height = windowBounds.height() - val width = windowBounds.width() + val height = windowBounds.height().toFloat() + val width = windowBounds.width().toFloat() - val maxDiameter = height / 2.5f - rippleView.setMaxSize(maxDiameter, maxDiameter) + rippleView.setMaxSize(width / 2f, height / 2f) // Center the ripple on the bottom of the screen in the middle. - rippleView.setCenter(width * 0.5f, height.toFloat()) + rippleView.setCenter(width * 0.5f, height) 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( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index 6a505f06a4959..e354a03f17250 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer.receiver import android.content.Context import android.util.AttributeSet +import com.android.systemui.ripple.RippleShader import com.android.systemui.ripple.RippleView /** @@ -25,9 +26,9 @@ import com.android.systemui.ripple.RippleView */ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleView(context, attrs) { init { - // TODO: use RippleShape#ELLIPSE when calling setupShader. - setupShader() + setupShader(RippleShader.RippleShape.ELLIPSE) setRippleFill(true) + setSparkleStrength(0f) duration = 3000L } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 11c55285c00b3..a98158eebfbf7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -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, private val falsingCollector: Lazy, + private val viewUtil: ViewUtil, ) : TemporaryViewDisplayController( context, logger, @@ -129,8 +132,6 @@ open class MediaTttChipControllerSender @Inject constructor( newInfo: ChipSenderInfo, currentView: ViewGroup ) { - super.updateView(newInfo, currentView) - val chipState = newInfo.state // Detect falsing touches on the chip. @@ -207,10 +208,10 @@ open class MediaTttChipControllerSender @Inject constructor( // animateChipOut matches the animateChipIn. } - override fun shouldIgnoreViewRemoval(removalReason: String): Boolean { + override fun shouldIgnoreViewRemoval(info: ChipSenderInfo, removalReason: String): Boolean { // Don't remove the chip if we're in progress or succeeded, since the user should still be // able to see the status of the transfer. (But do remove it if it's finally timed out.) - val transferStatus = info?.state?.transferStatus + val transferStatus = info.state.transferStatus if ( (transferStatus == TransferStatus.IN_PROGRESS || transferStatus == TransferStatus.SUCCEEDED) && @@ -224,6 +225,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 diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 91e20ee309762..93650b0d41029 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -20,17 +20,18 @@ 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 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT -import androidx.annotation.CallSuper import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -70,7 +71,8 @@ abstract class TemporaryViewDisplayController Unit, +) : ViewController(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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt b/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt index 613a797f020c5..6160b00379ef2 100644 --- a/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt +++ b/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt @@ -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, + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 775dc11f6edd8..f7b30919738c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index eca3bedee108d..213b74a6e8a36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -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 @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, falsingCollector: Lazy, + 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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 7cb28068fe6cf..e1c1b46baf245 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -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 @@ -259,13 +261,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { override val windowLayoutParams = commonWindowLayoutParams override fun updateView(newInfo: ViewInfo, currentView: ViewGroup) { - super.updateView(newInfo, currentView) mostRecentViewInfo = newInfo } - override fun shouldIgnoreViewRemoval(removalReason: String): Boolean { + override fun shouldIgnoreViewRemoval(info: ViewInfo, removalReason: String): Boolean { return shouldIgnoreViewRemoval } + + override fun getTouchableRegion(view: View, outRect: Rect) { + outRect.setEmpty() + } } inner class ViewInfo(val name: String) : TemporaryViewInfo { diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt new file mode 100644 index 0000000000000..7586fe48b3082 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt @@ -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)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt index dead1592992d7..e3cd9b2d6eafd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt @@ -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