From a5b595956d3049624aa9aa8f9b4e62b5b0829d7a Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Wed, 19 May 2021 16:20:41 -0400 Subject: [PATCH 1/2] Tests for StatusBarContentInsetsProvider Reworked some functions for the status bar content insets calculations and added tests for the content rectangle. Test: atest StatusBarContentInsetsProviderTest Bug: 187973222 Change-Id: I3eb473929944c19888f8c256b4c45ddd99948b88 --- .../phone/StatusBarContentInsetsProvider.kt | 82 ++-- .../StatusBarContentInsetsProviderTest.kt | 368 ++++++++++++++++++ 2 files changed, 411 insertions(+), 39 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index 8befe800b4e1d..edcf261115d26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -23,6 +23,8 @@ import android.util.Pair import android.view.DisplayCutout import android.view.View.LAYOUT_DIRECTION_RTL import android.view.WindowManager +import android.view.WindowMetrics +import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton @@ -118,17 +120,8 @@ class StatusBarContentInsetsProvider @Inject constructor( val chipWidth = rotatedResources.getDimensionPixelSize( R.dimen.ongoing_appops_chip_max_width) - return if (context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL) { - Rect(insets.left - dotWidth, - insets.top, - insets.left + chipWidth, - insets.bottom) - } else { - Rect(insets.right - chipWidth, - insets.top, - insets.right + dotWidth, - insets.bottom) - } + val isRtl = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL + return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl) } /** @@ -139,8 +132,7 @@ class StatusBarContentInsetsProvider @Inject constructor( var insets = insetsByCorner[rotation] if (insets == null) { val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context) - insets = getCalculatedInsetsForRotation(rotation, rotatedResources) - insetsByCorner[rotation] = insets + insets = getAndSetInsetsForRotation(rotation, rotatedResources) } return insets @@ -157,13 +149,19 @@ class StatusBarContentInsetsProvider @Inject constructor( } private fun getCalculatedInsetsForRotation( - @Rotation rotation: Int, + @Rotation targetRotation: Int, rotatedResources: Resources ): Rect { val dc = context.display.cutout + val currentRotation = RotationUtils.getExactRotation(context) return calculateInsetsForRotationWithRotatedResources( - rotation, rotatedResources, dc, windowManager, context) + currentRotation, + targetRotation, + dc, + windowManager.maximumWindowMetrics, + rotatedResources.getDimensionPixelSize(R.dimen.status_bar_height), + rotatedResources.getDimensionPixelSize(R.dimen.rounded_corner_content_padding)) } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { @@ -179,8 +177,8 @@ interface StatusBarContentInsetsChangedListener { private const val TAG = "StatusBarInsetsProvider" -private fun getRotationZeroDisplayBounds(wm: WindowManager, @Rotation exactRotation: Int): Rect { - val bounds = wm.maximumWindowMetrics.bounds +private fun getRotationZeroDisplayBounds(wm: WindowMetrics, @Rotation exactRotation: Int): Rect { + val bounds = wm.bounds if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) { return bounds @@ -190,9 +188,24 @@ private fun getRotationZeroDisplayBounds(wm: WindowManager, @Rotation exactRotat return Rect(0, 0, bounds.bottom, bounds.right) } -private fun getCurrentDisplayBounds(wm: WindowManager): Rect { - val bounds = wm.maximumWindowMetrics.bounds - return bounds +@VisibleForTesting +fun getPrivacyChipBoundingRectForInsets( + contentRect: Rect, + dotWidth: Int, + chipWidth: Int, + isRtl: Boolean +): Rect { + return if (isRtl) { + Rect(contentRect.left - dotWidth, + contentRect.top, + contentRect.left + chipWidth, + contentRect.bottom) + } else { + Rect(contentRect.right - chipWidth, + contentRect.top, + contentRect.right + dotWidth, + contentRect.bottom) + } } /** @@ -206,41 +219,32 @@ private fun getCurrentDisplayBounds(wm: WindowManager): Rect { * @see [RotationUtils#getResourcesForRotation] */ fun calculateInsetsForRotationWithRotatedResources( + @Rotation currentRotation: Int, @Rotation targetRotation: Int, - rotatedResources: Resources, displayCutout: DisplayCutout?, - windowmanager: WindowManager, - context: Context + windowMetrics: WindowMetrics, + statusBarHeight: Int, + roundedCornerPadding: Int ): Rect { - val rtl = rotatedResources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL - - val exactRotation = RotationUtils.getExactRotation(context) - val height = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_height) - /* TODO: Check if this is ever used for devices with no rounded corners - val paddingStart = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_padding_start) - val paddingEnd = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_padding_end) - val left = if (rtl) paddingEnd else paddingStart - val right = if(rtl) paddingStart else paddingEnd + val left = if (isRtl) paddingEnd else paddingStart + val right = if (isRtl) paddingStart else paddingEnd */ - val roundedCornerPadding = rotatedResources.getDimensionPixelSize( - R.dimen.rounded_corner_content_padding) - - val rotZeroBounds = getRotationZeroDisplayBounds(windowmanager, exactRotation) - val currentBounds = getCurrentDisplayBounds(windowmanager) + val rotZeroBounds = getRotationZeroDisplayBounds(windowMetrics, currentRotation) + val currentBounds = windowMetrics.bounds val sbLeftRight = getStatusBarLeftRight( displayCutout, - height, + statusBarHeight, rotZeroBounds.right, rotZeroBounds.bottom, currentBounds.width(), currentBounds.height(), roundedCornerPadding, targetRotation, - exactRotation) + currentRotation) return sbLeftRight } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt new file mode 100644 index 0000000000000..4796cd7689809 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -0,0 +1,368 @@ +/* + * 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 + +import android.graphics.Rect +import android.test.suitebuilder.annotation.SmallTest +import android.view.DisplayCutout +import android.view.WindowMetrics +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.leak.RotationUtils +import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE +import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE +import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE +import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN +import com.android.systemui.util.leak.RotationUtils.Rotation +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +class StatusBarContentInsetsProviderTest : SysuiTestCase() { + @Mock private lateinit var dc: DisplayCutout + @Mock private lateinit var windowMetrics: WindowMetrics + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testGetBoundingRectForPrivacyChipForRotation_noCutout() { + val screenBounds = Rect(0, 0, 1080, 2160) + val roundedCornerPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + val chipWidth = 30 + val dotWidth = 10 + + `when`(windowMetrics.bounds).thenReturn(screenBounds) + + var isRtl = false + var targetRotation = ROTATION_NONE + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + + var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 1080 - 20 (rounded corner) - 30 (chip), + * 0 (sb top) + * 1080 - 20 (rounded corner) + 10 ( dot), + * 100 (sb height portrait) + */ + var expected = Rect(1030, 0, 1070, 100) + assertRects(expected, chipBounds, currentRotation, targetRotation) + isRtl = true + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 0 + 20 (rounded corner) - 10 (dot), + * 0 (sb top) + * 0 + 20 (rounded corner) + 30 (chip), + * 100 (sb height portrait) + */ + expected = Rect(10, 0, 50, 100) + assertRects(expected, chipBounds, currentRotation, targetRotation) + + isRtl = false + targetRotation = ROTATION_LANDSCAPE + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 2160 - 20 (rounded corner) - 30 (chip), + * 0 (sb top) + * 2160 - 20 (rounded corner) + 10 ( dot), + * 60 (sb height landscape) + */ + expected = Rect(2110, 0, 2150, 60) + assertRects(expected, chipBounds, currentRotation, targetRotation) + isRtl = true + chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) + /* 0 + 20 (rounded corner) - 10 (dot), + * 0 (sb top) + * 0 + 20 (rounded corner) + 30 (chip), + * 60 (sb height landscape) + */ + expected = Rect(10, 0, 50, 60) + assertRects(expected, chipBounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() { + // GIVEN a device in portrait mode with width < height and a display cutout in the top-left + val screenBounds = Rect(0, 0, 1080, 2160) + val dcBounds = Rect(0, 0, 100, 100) + val roundedCornerPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + + `when`(windowMetrics.bounds).thenReturn(screenBounds) + `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) + + // THEN rotations which share a short side should use the greater value between rounded + // corner padding and the display cutout's size + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(dcBounds.right, + 0, + screenBounds.right - roundedCornerPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(dcBounds.height(), + 0, + screenBounds.height() - roundedCornerPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // THEN the side that does NOT share a short side with the display cutout ignores the + // display cutout bounds + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.width() - roundedCornerPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + // Phone in portrait, seascape (rot_270) bounds + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.height() - dcBounds.height(), + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_nonCornerCutout() { + // GIVEN phone in portrait mode, where width < height and the cutout is not in the corner + // the assumption here is that if the cutout does NOT touch the corner then we have room to + // layout the status bar in the given space. + + val screenBounds = Rect(0, 0, 1080, 2160) + // cutout centered at the top + val dcBounds = Rect(490, 0, 590, 100) + val roundedCornerPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + val currentRotation = ROTATION_NONE + + `when`(windowMetrics.bounds).thenReturn(screenBounds) + `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) + + // THEN only the landscape/seascape rotations should avoid the cutout area because of the + // potential letterboxing + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.right - roundedCornerPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(dcBounds.height(), + 0, + screenBounds.height() - roundedCornerPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.right - roundedCornerPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_SEASCAPE + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.height() - dcBounds.height(), + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + dc, + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + @Test + fun testCalculateInsetsForRotationWithRotatedResources_noCutout() { + // GIVEN device in portrait mode, where width < height and no cutout + val currentRotation = ROTATION_NONE + val screenBounds = Rect(0, 0, 1080, 2160) + val roundedCornerPadding = 20 + val sbHeightPortrait = 100 + val sbHeightLandscape = 60 + + `when`(windowMetrics.bounds).thenReturn(screenBounds) + + // THEN content insets should only use rounded corner padding + var targetRotation = ROTATION_NONE + var expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.right - roundedCornerPadding, + sbHeightPortrait) + + var bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.height() - roundedCornerPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_UPSIDE_DOWN + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.width() - roundedCornerPadding, + sbHeightPortrait) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + windowMetrics, + sbHeightPortrait, + roundedCornerPadding) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + + targetRotation = ROTATION_LANDSCAPE + expectedBounds = Rect(roundedCornerPadding, + 0, + screenBounds.height() - roundedCornerPadding, + sbHeightLandscape) + + bounds = calculateInsetsForRotationWithRotatedResources( + currentRotation, + targetRotation, + null, /* no cutout */ + windowMetrics, + sbHeightLandscape, + roundedCornerPadding) + assertRects(expectedBounds, bounds, currentRotation, targetRotation) + } + + private fun assertRects( + expected: Rect, + actual: Rect, + @Rotation currentRotation: Int, + @Rotation targetRotation: Int + ) { + assertTrue( + "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + + " targetRotation=${RotationUtils.toString(targetRotation)}" + + " expected=$expected actual=$actual", + expected.equals(actual)) + } +} \ No newline at end of file From 5610e6c27b33e33b4017127e45517809d29e4bac Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Wed, 19 May 2021 18:22:49 -0400 Subject: [PATCH 2/2] Add some dumpsys information to SystemStatusAnimationScheduler Test: adb shell dumpsys activity service com.android.systemui/.SystemUIService SystemStatusAnimationScheduler Bug: 187973222 Change-Id: Iaaefbeb267415d33263b56ab13c77baff267219e --- .../events/SystemStatusAnimationScheduler.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt index 655ed41327260..33aa7c720b22c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt @@ -26,14 +26,18 @@ import android.os.Process import android.provider.DeviceConfig import android.util.Log import android.view.View +import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.phone.StatusBarWindowController import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.Assert import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock +import java.io.FileDescriptor +import java.io.PrintWriter import javax.inject.Inject @@ -59,9 +63,10 @@ class SystemStatusAnimationScheduler @Inject constructor( private val coordinator: SystemEventCoordinator, private val chipAnimationController: SystemEventChipAnimationController, private val statusBarWindowController: StatusBarWindowController, + private val dumpManager: DumpManager, private val systemClock: SystemClock, @Main private val executor: DelayableExecutor -) : CallbackController { +) : CallbackController, Dumpable { companion object { private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator" @@ -71,10 +76,6 @@ class SystemStatusAnimationScheduler @Inject constructor( PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true) } - /** True from the time a scheduled event starts until it's animation finishes */ - var isActive = false - private set - @SystemAnimationState var animationState: Int = IDLE private set @@ -88,6 +89,7 @@ class SystemStatusAnimationScheduler @Inject constructor( init { coordinator.attachScheduler(this) + dumpManager.registerDumpable(TAG, this) } fun onStatusEvent(event: StatusEvent) { @@ -293,6 +295,20 @@ class SystemStatusAnimationScheduler @Inject constructor( anim -> chipAnimationController.onChipAnimationUpdate(anim, animationState) } + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { + pw.println("Scheduled event: $scheduledEvent") + pw.println("Has persistent privacy dot: $hasPersistentDot") + pw.println("Animation state: $animationState") + pw.println("Listeners:") + if (listeners.isEmpty()) { + pw.println("(none)") + } else { + listeners.forEach { + pw.println(" $it") + } + } + } + inner class ChipAnimatorAdapter( @SystemAnimationState val endState: Int, val viewCreator: (context: Context) -> View