Merge "Retain old logic for prohibiting battery estimate on the devices with center cutout." into tm-qpr-dev

This commit is contained in:
Anton Potapov
2023-02-16 10:55:38 +00:00
committed by Android (Google) Code Review
6 changed files with 260 additions and 48 deletions

View File

@@ -152,9 +152,7 @@ public class QuickStatusBarHeader extends FrameLayout {
Configuration config = mContext.getResources().getConfiguration();
setDatePrivacyContainersWidth(config.orientation == Configuration.ORIENTATION_LANDSCAPE);
// QS will always show the estimate, and BatteryMeterView handles the case where
// it's unavailable or charging
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
updateBatteryMode();
mIconsAlphaAnimatorFixed = new TouchAnimator.Builder()
.addFloat(mIconContainer, "alpha", 0, 1)
@@ -460,24 +458,24 @@ public class QuickStatusBarHeader extends FrameLayout {
(LinearLayout.LayoutParams) mDatePrivacySeparator.getLayoutParams();
LinearLayout.LayoutParams mClockIconsSeparatorLayoutParams =
(LinearLayout.LayoutParams) mClockIconsSeparator.getLayoutParams();
if (cutout != null) {
Rect topCutout = cutout.getBoundingRectTop();
if (topCutout.isEmpty() || hasCornerCutout) {
datePrivacySeparatorLayoutParams.width = 0;
mDatePrivacySeparator.setVisibility(View.GONE);
mClockIconsSeparatorLayoutParams.width = 0;
setSeparatorVisibility(false);
mShowClockIconsSeparator = false;
mHasCenterCutout = false;
} else {
datePrivacySeparatorLayoutParams.width = topCutout.width();
mDatePrivacySeparator.setVisibility(View.VISIBLE);
mClockIconsSeparatorLayoutParams.width = topCutout.width();
mShowClockIconsSeparator = true;
setSeparatorVisibility(mKeyguardExpansionFraction == 0f);
mHasCenterCutout = true;
}
Rect topCutout = cutout == null ? null : cutout.getBoundingRectTop();
if (topCutout == null || topCutout.isEmpty() || hasCornerCutout) {
datePrivacySeparatorLayoutParams.width = 0;
mDatePrivacySeparator.setVisibility(View.GONE);
mClockIconsSeparatorLayoutParams.width = 0;
setSeparatorVisibility(false);
mShowClockIconsSeparator = false;
mHasCenterCutout = false;
} else {
datePrivacySeparatorLayoutParams.width = topCutout.width();
mDatePrivacySeparator.setVisibility(View.VISIBLE);
mClockIconsSeparatorLayoutParams.width = topCutout.width();
mShowClockIconsSeparator = true;
setSeparatorVisibility(mKeyguardExpansionFraction == 0f);
mHasCenterCutout = true;
}
mDatePrivacySeparator.setLayoutParams(datePrivacySeparatorLayoutParams);
mClockIconsSeparator.setLayoutParams(mClockIconsSeparatorLayoutParams);
mCutOutPaddingLeft = sbInsets.first;

View File

@@ -24,6 +24,7 @@ import android.os.Bundle
import android.os.Trace
import android.os.Trace.TRACE_TAG_APP
import android.util.Pair
import android.view.DisplayCutout
import android.view.View
import android.view.WindowInsets
import android.widget.TextView
@@ -91,7 +92,8 @@ class LargeScreenShadeHeaderController @Inject constructor(
private val featureFlags: FeatureFlags,
private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
private val demoModeController: DemoModeController
private val demoModeController: DemoModeController,
private val qsBatteryModeController: QsBatteryModeController,
) : ViewController<View>(header), Dumpable {
companion object {
@@ -129,9 +131,8 @@ class LargeScreenShadeHeaderController @Inject constructor(
private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons)
private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group)
private var cutoutLeft = 0
private var cutoutRight = 0
private var roundedCorners = 0
private var cutout: DisplayCutout? = null
private var lastInsets: WindowInsets? = null
private var qsDisabled = false
@@ -273,7 +274,6 @@ class LargeScreenShadeHeaderController @Inject constructor(
// battery settings same as in QS icons
batteryMeterViewController.ignoreTunerUpdates()
batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
iconManager.setTint(
@@ -305,6 +305,7 @@ class LargeScreenShadeHeaderController @Inject constructor(
if (header is MotionLayout) {
header.setOnApplyWindowInsetsListener(insetListener)
clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
v.pivotX = newPivot
@@ -376,11 +377,13 @@ class LargeScreenShadeHeaderController @Inject constructor(
}
private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) {
val cutout = insets.displayCutout
val cutout = insets.displayCutout.also {
this.cutout = it
}
val sbInsets: Pair<Int, Int> = insetsProvider.getStatusBarContentInsetsForCurrentRotation()
cutoutLeft = sbInsets.first
cutoutRight = sbInsets.second
val cutoutLeft = sbInsets.first
val cutoutRight = sbInsets.second
val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout()
updateQQSPaddings()
// Set these guides as the left/right limits for content that lives in the top row, using
@@ -408,6 +411,13 @@ class LargeScreenShadeHeaderController @Inject constructor(
}
view.updateAllConstraints(changes)
updateBatteryMode()
}
private fun updateBatteryMode() {
qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let {
batteryIcon.setPercentShowMode(it)
}
}
private fun updateScrollY() {
@@ -475,6 +485,7 @@ class LargeScreenShadeHeaderController @Inject constructor(
if (header is MotionLayout && !largeScreenActive && visible) {
logInstantEvent("updatePosition: $qsExpandedFraction")
header.progress = qsExpandedFraction
updateBatteryMode()
}
}
@@ -511,6 +522,7 @@ class LargeScreenShadeHeaderController @Inject constructor(
val padding = resources.getDimensionPixelSize(R.dimen.qs_panel_padding)
header.setPadding(padding, header.paddingTop, padding, header.paddingBottom)
updateQQSPaddings()
qsBatteryModeController.updateResources()
}
private fun updateQQSPaddings() {

View File

@@ -0,0 +1,70 @@
package com.android.systemui.shade
import android.content.Context
import android.view.DisplayCutout
import com.android.systemui.R
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import javax.inject.Inject
/**
* Controls [BatteryMeterView.BatteryPercentMode]. It takes into account cutout and qs-qqs
* transition fraction when determining the mode.
*/
class QsBatteryModeController
@Inject
constructor(
private val context: Context,
private val insetsProvider: StatusBarContentInsetsProvider,
) {
private companion object {
// MotionLayout frames are in [0, 100]. Where 0 and 100 are reserved for start and end
// frames.
const val MOTION_LAYOUT_MAX_FRAME = 100
// We add a single buffer frame to ensure that battery view faded out completely when we are
// about to change it's state
const val BUFFER_FRAME_COUNT = 1
}
private var fadeInStartFraction: Float = 0f
private var fadeOutCompleteFraction: Float = 0f
init {
updateResources()
}
/**
* Returns an appropriate [BatteryMeterView.BatteryPercentMode] for the [qsExpandedFraction] and
* [cutout]. We don't show battery estimation in qqs header on the devices with center cutout.
* The result might be null when the battery icon is invisible during the qs-qqs transition
* animation.
*/
@BatteryMeterView.BatteryPercentMode
fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? =
when {
qsExpandedFraction > fadeInStartFraction -> BatteryMeterView.MODE_ESTIMATE
qsExpandedFraction < fadeOutCompleteFraction ->
if (hasCenterCutout(cutout)) {
BatteryMeterView.MODE_ON
} else {
BatteryMeterView.MODE_ESTIMATE
}
else -> null
}
fun updateResources() {
fadeInStartFraction =
(context.resources.getInteger(R.integer.fade_in_start_frame) - BUFFER_FRAME_COUNT) /
MOTION_LAYOUT_MAX_FRAME.toFloat()
fadeOutCompleteFraction =
(context.resources.getInteger(R.integer.fade_out_complete_frame) + BUFFER_FRAME_COUNT) /
MOTION_LAYOUT_MAX_FRAME.toFloat()
}
private fun hasCenterCutout(cutout: DisplayCutout?): Boolean =
cutout?.let {
!insetsProvider.currentRotationHasCornerCutout() && !it.boundingRectTop.isEmpty
}
?: false
}

View File

@@ -21,6 +21,7 @@ import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.DisplayCutout
import android.view.View
import android.view.ViewPropertyAnimator
@@ -77,9 +78,11 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.same
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
private val EMPTY_CHANGES = ConstraintsChanges()
@@ -133,6 +136,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
@Mock
private lateinit var mockedContext: Context
private lateinit var viewContext: Context
@Mock(answer = Answers.RETURNS_MOCKS)
private lateinit var view: MotionLayout
@@ -143,6 +147,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
@Mock
private lateinit var largeScreenConstraints: ConstraintSet
@Mock private lateinit var demoModeController: DemoModeController
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController
@JvmField @Rule
val mockitoRule = MockitoJUnit.rule()
@@ -175,7 +180,8 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
.thenReturn(qsCarrierGroupControllerBuilder)
whenever(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
whenever(view.context).thenReturn(context)
viewContext = spy(context)
whenever(view.context).thenReturn(viewContext)
whenever(view.resources).thenReturn(context.resources)
whenever(view.setVisibility(ArgumentMatchers.anyInt())).then {
viewVisibility = it.arguments[0] as Int
@@ -192,19 +198,20 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
setUpMotionLayout(view)
controller = LargeScreenShadeHeaderController(
view,
statusBarIconController,
iconManagerFactory,
privacyIconsController,
insetsProvider,
configurationController,
variableDateViewControllerFactory,
batteryMeterViewController,
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder,
combinedShadeHeadersConstraintManager,
demoModeController
view,
statusBarIconController,
iconManagerFactory,
privacyIconsController,
insetsProvider,
configurationController,
variableDateViewControllerFactory,
batteryMeterViewController,
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder,
combinedShadeHeadersConstraintManager,
demoModeController,
qsBatteryModeController,
)
whenever(view.isAttachedToWindow).thenReturn(true)
controller.init()
@@ -218,13 +225,29 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
verify(batteryMeterViewController).init()
verify(batteryMeterViewController).ignoreTunerUpdates()
verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
val inOrder = inOrder(qsCarrierGroupControllerBuilder)
inOrder.verify(qsCarrierGroupControllerBuilder).setQSCarrierGroup(carrierGroup)
inOrder.verify(qsCarrierGroupControllerBuilder).build()
}
@Test
fun `battery mode controller called when qsExpandedFraction changes`() {
whenever(qsBatteryModeController.getBatteryMode(same(null), eq(0f)))
.thenReturn(BatteryMeterView.MODE_ON)
whenever(qsBatteryModeController.getBatteryMode(same(null), eq(1f)))
.thenReturn(BatteryMeterView.MODE_ESTIMATE)
controller.qsVisible = true
val times = 10
repeat(times) {
controller.qsExpandedFraction = it / (times - 1).toFloat()
}
verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ON)
verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
}
@Test
fun testClockPivotLtr() {
val width = 200
@@ -684,11 +707,11 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
configurationController.notifyDensityOrFontScaleChanged()
val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
verify(qqsConstraints).load(eq(context), capture(captor))
verify(qqsConstraints).load(eq(viewContext), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
verify(qsConstraints).load(eq(context), capture(captor))
verify(qsConstraints).load(eq(viewContext), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header)
verify(largeScreenConstraints).load(eq(context), capture(captor))
verify(largeScreenConstraints).load(eq(viewContext), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
}
@@ -786,6 +809,13 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
.thenReturn(Pair(0, 0).toAndroidPair())
whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false)
setupCurrentInsets(null)
}
private fun setupCurrentInsets(cutout: DisplayCutout?) {
val mockedDisplay =
mock<Display>().also { display -> whenever(display.cutout).thenReturn(cutout) }
whenever(viewContext.display).thenReturn(mockedDisplay)
}
private fun<T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> {

View File

@@ -76,6 +76,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
@Mock private lateinit var mockedContext: Context
@Mock private lateinit var demoModeController: DemoModeController
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
var viewVisibility = View.GONE
@@ -130,8 +131,9 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
featureFlags,
qsCarrierGroupControllerBuilder,
combinedShadeHeadersConstraintManager,
demoModeController
)
demoModeController,
qsBatteryModeController,
)
whenever(view.isAttachedToWindow).thenReturn(true)
mLargeScreenShadeHeaderController.init()
carrierIconSlots = listOf(

View File

@@ -0,0 +1,100 @@
package com.android.systemui.shade
import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.DisplayCutout
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
@SmallTest
@RunWith(AndroidTestingRunner::class)
class QsBatteryModeControllerTest : SysuiTestCase() {
private companion object {
val CENTER_TOP_CUTOUT: DisplayCutout =
mock<DisplayCutout>().also {
whenever(it.boundingRectTop).thenReturn(Rect(10, 0, 20, 10))
}
const val MOTION_LAYOUT_MAX_FRAME = 100
const val QQS_START_FRAME = 14
const val QS_END_FRAME = 58
}
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()!!
@Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
@Mock private lateinit var mockedContext: Context
@Mock private lateinit var mockedResources: Resources
private lateinit var controller: QsBatteryModeController // under test
@Before
fun setup() {
whenever(mockedContext.resources).thenReturn(mockedResources)
whenever(mockedResources.getInteger(R.integer.fade_in_start_frame)).thenReturn(QS_END_FRAME)
whenever(mockedResources.getInteger(R.integer.fade_out_complete_frame))
.thenReturn(QQS_START_FRAME)
controller = QsBatteryModeController(mockedContext, insetsProvider)
}
@Test
fun `returns MODE_ON for qqs with center cutout`() {
assertThat(
controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.prevFrameToFraction())
)
.isEqualTo(BatteryMeterView.MODE_ON)
}
@Test
fun `returns MODE_ESTIMATE for qs with center cutout`() {
assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.nextFrameToFraction()))
.isEqualTo(BatteryMeterView.MODE_ESTIMATE)
}
@Test
fun `returns MODE_ON for qqs with corner cutout`() {
whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(true)
assertThat(
controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.prevFrameToFraction())
)
.isEqualTo(BatteryMeterView.MODE_ESTIMATE)
}
@Test
fun `returns MODE_ESTIMATE for qs with corner cutout`() {
whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(true)
assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.nextFrameToFraction()))
.isEqualTo(BatteryMeterView.MODE_ESTIMATE)
}
@Test
fun `returns null in-between`() {
assertThat(
controller.getBatteryMode(CENTER_TOP_CUTOUT, QQS_START_FRAME.nextFrameToFraction())
)
.isNull()
assertThat(controller.getBatteryMode(CENTER_TOP_CUTOUT, QS_END_FRAME.prevFrameToFraction()))
.isNull()
}
private fun Int.prevFrameToFraction(): Float = (this - 1) / MOTION_LAYOUT_MAX_FRAME.toFloat()
private fun Int.nextFrameToFraction(): Float = (this + 1) / MOTION_LAYOUT_MAX_FRAME.toFloat()
}