Add unit tests for combined QS headers

Also, some minor fixes in the constraints.

Bug: 215584502
Test: atest SystemUITests
Change-Id: I23e5e4927010d60084e4b89c05f990fa268c2f2c
This commit is contained in:
Fabian Kozynski
2022-07-13 14:17:31 -04:00
parent 0ab0199c39
commit fa5d328459
16 changed files with 1433 additions and 83 deletions

View File

@@ -71,6 +71,7 @@
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:singleLine="true"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.Status"
android:transformPivotX="0sp"
android:transformPivotY="20sp"
@@ -85,6 +86,7 @@
android:layout_gravity="start|center_vertical"
android:gravity="center_vertical"
android:singleLine="true"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.Status"
app:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
app:shortDatePattern="@string/abbrev_month_day_no_year"

View File

@@ -112,6 +112,7 @@
app:layout_constraintTop_toTopOf="@id/date"
app:layout_constraintBottom_toBottomOf="@id/date"
app:layout_constraintStart_toEndOf="@id/batteryRemainingIcon"
app:layout_constraintHorizontal_bias="1"
/>
</Constraint>

View File

@@ -89,7 +89,7 @@
app:layout_constraintTop_toTopOf="parent"
/>
<PropertySet
app:alpha="0"
android:alpha="0"
/>
</Constraint>

View File

@@ -29,6 +29,7 @@
app:layout_constraintEnd_toEndOf="@id/end_guide"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/carrier_group"
app:layout_constraintHorizontal_bias="1"
/>
</Constraint>

View File

@@ -0,0 +1,79 @@
/*
* 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.shade
import androidx.constraintlayout.widget.ConstraintSet
typealias ConstraintChange = ConstraintSet.() -> Unit
operator fun ConstraintChange?.plus(other: ConstraintChange?): ConstraintChange? {
// Prevent wrapping
if (this == null) return other
if (other == null) return this
else return {
this@plus()
other()
}
}
/**
* Contains all changes that need to be performed to the different [ConstraintSet] in
* [LargeScreenShadeHeaderController].
*/
data class ConstraintsChanges(
val qqsConstraintsChanges: ConstraintChange? = null,
val qsConstraintsChanges: ConstraintChange? = null,
val largeScreenConstraintsChanges: ConstraintChange? = null
) {
operator fun plus(other: ConstraintsChanges) = ConstraintsChanges(
qqsConstraintsChanges + other.qqsConstraintsChanges,
qsConstraintsChanges + other.qsConstraintsChanges,
largeScreenConstraintsChanges + other.largeScreenConstraintsChanges
)
}
/**
* Determines [ConstraintChanges] for [LargeScreenShadeHeaderController] based on configurations.
*
* Given that the number of different scenarios is not that large, having specific methods instead
* of a full map between state and [ConstraintSet] was preferred.
*/
interface CombinedShadeHeadersConstraintManager {
/**
* Changes for when the visibility of the privacy chip changes
*/
fun privacyChipVisibilityConstraints(visible: Boolean): ConstraintsChanges
/**
* Changes for situations with no top center cutout (there may be a corner cutout)
*/
fun emptyCutoutConstraints(): ConstraintsChanges
/**
* Changes to incorporate side insets due to rounded corners/corner cutouts
*/
fun edgesGuidelinesConstraints(
cutoutStart: Int,
paddingStart: Int,
cutoutEnd: Int,
paddingEnd: Int
): ConstraintsChanges
/**
* Changes for situations with top center cutout (in this case, there are no corner cutouts).
*/
fun centerCutoutConstraints(rtl: Boolean, offsetFromEdge: Int): ConstraintsChanges
}

View File

@@ -0,0 +1,109 @@
/*
* 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.shade
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
/**
* Standard implementation of [CombinedShadeHeadersConstraintManager].
*/
@CentralSurfacesComponent.CentralSurfacesScope
object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstraintManager {
override fun privacyChipVisibilityConstraints(visible: Boolean): ConstraintsChanges {
val constraintAlpha = if (visible) 0f else 1f
return ConstraintsChanges(
qqsConstraintsChanges = {
setAlpha(R.id.statusIcons, constraintAlpha)
setAlpha(R.id.batteryRemainingIcon, constraintAlpha)
}
)
}
override fun emptyCutoutConstraints(): ConstraintsChanges {
return ConstraintsChanges(
qqsConstraintsChanges = {
connect(R.id.date, ConstraintSet.END, R.id.barrier, ConstraintSet.START)
createBarrier(
R.id.barrier,
ConstraintSet.START,
0,
R.id.statusIcons,
R.id.privacy_container
)
connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END)
connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END)
constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT)
}
)
}
override fun edgesGuidelinesConstraints(
cutoutStart: Int,
paddingStart: Int,
cutoutEnd: Int,
paddingEnd: Int
): ConstraintsChanges {
val change: ConstraintChange = {
setGuidelineBegin(R.id.begin_guide, Math.max(cutoutStart - paddingStart, 0))
setGuidelineEnd(R.id.end_guide, Math.max(cutoutEnd - paddingEnd, 0))
}
return ConstraintsChanges(
qqsConstraintsChanges = change,
qsConstraintsChanges = change
)
}
override fun centerCutoutConstraints(rtl: Boolean, offsetFromEdge: Int): ConstraintsChanges {
val centerStart = if (!rtl) R.id.center_left else R.id.center_right
val centerEnd = if (!rtl) R.id.center_right else R.id.center_left
// Use guidelines to block the center cutout area.
return ConstraintsChanges(
qqsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
setGuidelineEnd(centerEnd, offsetFromEdge)
connect(R.id.date, ConstraintSet.END, centerStart, ConstraintSet.START)
connect(
R.id.statusIcons,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
connect(
R.id.privacy_container,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
constrainWidth(R.id.statusIcons, 0)
},
qsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
setGuidelineEnd(centerEnd, offsetFromEdge)
connect(
R.id.privacy_container,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
}
)
}
}

View File

@@ -14,19 +14,19 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.phone
package com.android.systemui.shade
import android.annotation.IdRes
import android.app.StatusBarManager
import android.content.res.Configuration
import android.os.Trace
import android.os.Trace.TRACE_TAG_APP
import android.util.Pair
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowInsets
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.android.settingslib.Utils
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -40,11 +40,14 @@ import com.android.systemui.qs.ChipVisibilityListener
import com.android.systemui.qs.HeaderPrivacyIconsController
import com.android.systemui.qs.carrier.QSCarrierGroup
import com.android.systemui.qs.carrier.QSCarrierGroupController
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_SHADE_HEADER
@@ -82,18 +85,24 @@ class LargeScreenShadeHeaderController @Inject constructor(
private val batteryMeterViewController: BatteryMeterViewController,
private val dumpManager: DumpManager,
private val featureFlags: FeatureFlags,
private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager
) : ViewController<View>(header), Dumpable {
companion object {
/** IDs for transitions and constraints for the [MotionLayout]. These are only used when
* [Flags.COMBINED_QS_HEADERS] is enabled.
*/
private val HEADER_TRANSITION_ID = R.id.header_transition
private val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
private val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
private val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
private val LARGE_SCREEN_HEADER_CONSTRAINT = R.id.large_screen_header_constraint
@VisibleForTesting
internal val HEADER_TRANSITION_ID = R.id.header_transition
@VisibleForTesting
internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
@VisibleForTesting
internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
@VisibleForTesting
internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
@VisibleForTesting
internal val LARGE_SCREEN_HEADER_CONSTRAINT = R.id.large_screen_header_constraint
private fun Int.stateToString() = when (this) {
QQS_HEADER_CONSTRAINT -> "QQS Header"
@@ -178,7 +187,6 @@ class LargeScreenShadeHeaderController @Inject constructor(
set(value) {
if (visible && field != value) {
field = value
updateVisibility()
updatePosition()
}
}
@@ -206,12 +214,9 @@ class LargeScreenShadeHeaderController @Inject constructor(
if (header is MotionLayout) {
// If the privacy chip is visible, we hide the status icons and battery remaining
// icon, only in QQS.
val constraintAlpha = if (visible) 0f else 1f
val state = header.getConstraintSet(QQS_HEADER_CONSTRAINT).apply {
setAlpha(R.id.statusIcons, constraintAlpha)
setAlpha(R.id.batteryRemainingIcon, constraintAlpha)
}
header.updateState(QQS_HEADER_CONSTRAINT, state)
val update = combinedShadeHeadersConstraintManager
.privacyChipVisibilityConstraints(visible)
header.updateAllConstraints(update)
}
}
}
@@ -276,6 +281,10 @@ class LargeScreenShadeHeaderController @Inject constructor(
privacyIconsController.chipVisibilityListener = chipVisibilityListener
if (header is MotionLayout) {
header.setOnApplyWindowInsetsListener(insetListener)
clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
v.pivotX = newPivot
}
}
dumpManager.registerDumpable(this)
@@ -322,74 +331,32 @@ class LargeScreenShadeHeaderController @Inject constructor(
cutoutLeft = sbInsets.first
cutoutRight = sbInsets.second
val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout()
val collapsedConstraint = view.getConstraintSet(QQS_HEADER_CONSTRAINT)
updateQQSPaddings()
// Set these guides as the left/right limits for content that lives in the top row, using
// cutoutLeft and cutoutRight
view.getConstraintSet(QQS_HEADER_CONSTRAINT).updateGuides()
view.getConstraintSet(QS_HEADER_CONSTRAINT).updateGuides()
var changes = combinedShadeHeadersConstraintManager
.edgesGuidelinesConstraints(
if (view.isLayoutRtl) cutoutRight else cutoutLeft,
header.paddingStart,
if (view.isLayoutRtl) cutoutLeft else cutoutRight,
header.paddingEnd
)
if (cutout != null) {
val topCutout = cutout.boundingRectTop
if (topCutout.isEmpty || hasCornerCutout) {
updateConstraintsForNoCutout(view)
changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
} else {
val rtl = view.isLayoutRtl
val centerStart = if (!rtl) R.id.center_left else R.id.center_right
val centerEnd = if (!rtl) R.id.center_right else R.id.center_left
val offsetFromEdge =
(view.width - view.paddingLeft - view.paddingStart) / 2 - topCutout.width() / 2
collapsedConstraint.apply {
// Use guidelines to block the center cutout area.
setGuidelineBegin(centerStart, offsetFromEdge)
setGuidelineEnd(centerEnd, offsetFromEdge)
connect(R.id.date, ConstraintSet.END, centerStart, ConstraintSet.START)
connect(
R.id.statusIcons,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
connect(
R.id.privacy_container,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
constrainWidth(R.id.statusIcons, 0)
}
changes += combinedShadeHeadersConstraintManager.centerCutoutConstraints(
view.isLayoutRtl,
(view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2
)
}
} else {
updateConstraintsForNoCutout(view)
changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints()
}
view.updateState(QQS_HEADER_CONSTRAINT, collapsedConstraint)
}
private fun ConstraintSet.updateGuides() {
setGuidelineBegin(R.id.begin_guide, Math.max(cutoutLeft - header.paddingLeft, 0))
setGuidelineEnd(R.id.end_guide, Math.max(cutoutRight - header.paddingRight, 0))
}
/**
* If there's no center cutout, either due to no cutouts at all or just corner cutouts, update
* constraints so elements are not constrained in the center.
*/
private fun updateConstraintsForNoCutout(view: MotionLayout) {
val collapsedConstraint = view.getConstraintSet(QQS_HEADER_CONSTRAINT)
collapsedConstraint.apply {
connect(R.id.date, ConstraintSet.END, R.id.barrier, ConstraintSet.START)
createBarrier(
R.id.barrier,
ConstraintSet.START,
0,
R.id.statusIcons,
R.id.privacy_container
)
connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END)
connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END)
constrainWidth(R.id.statusIcons, WRAP_CONTENT)
}
view.updateAllConstraints(changes)
}
private fun updateScrollY() {
@@ -517,4 +484,27 @@ class LargeScreenShadeHeaderController @Inject constructor(
pw.println("currentState: ${header.currentState.stateToString()}")
}
}
private fun MotionLayout.updateConstraints(@IdRes state: Int, update: ConstraintChange) {
val constraints = getConstraintSet(state)
constraints.update()
updateState(state, constraints)
}
/**
* Updates the [ConstraintSet] for the case of combined headers.
*
* Only non-`null` changes are applied to reduce the number of rebuilding in the [MotionLayout].
*/
private fun MotionLayout.updateAllConstraints(updates: ConstraintsChanges) {
if (updates.qqsConstraintsChanges != null) {
updateConstraints(QQS_HEADER_CONSTRAINT, updates.qqsConstraintsChanges)
}
if (updates.qsConstraintsChanges != null) {
updateConstraints(QS_HEADER_CONSTRAINT, updates.qsConstraintsChanges)
}
if (updates.largeScreenConstraintsChanges != null) {
updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges)
}
}
}

View File

@@ -189,7 +189,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;

View File

@@ -22,6 +22,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.android.keyguard.LockIconViewController;
import com.android.systemui.biometrics.AuthRippleController;
import com.android.systemui.shade.LargeScreenShadeHeaderController;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -35,7 +36,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutListContainerModule;
import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks;
import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController;
import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener;
import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarterModule;
import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;

View File

@@ -34,6 +34,8 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.privacy.OngoingPrivacyChip;
import com.android.systemui.shade.CombinedShadeHeadersConstraintManager;
import com.android.systemui.shade.CombinedShadeHeadersConstraintManagerImpl;
import com.android.systemui.shade.NotificationPanelView;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowView;
@@ -175,6 +177,14 @@ public abstract class StatusBarViewModule {
return v;
}
/** */
@Provides
@CentralSurfacesComponent.CentralSurfacesScope
public static CombinedShadeHeadersConstraintManager
provideCombinedShadeHeadersConstraintManager() {
return CombinedShadeHeadersConstraintManagerImpl.INSTANCE;
}
/** */
@Provides
@CentralSurfacesComponent.CentralSurfacesScope

View File

@@ -0,0 +1,328 @@
/*
* 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.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class CombinedShadeHeaderConstraintsTest : SysuiTestCase() {
private lateinit var qqsConstraint: ConstraintSet
private lateinit var qsConstraint: ConstraintSet
private lateinit var largeScreenConstraint: ConstraintSet
@Before
fun setUp() {
qqsConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.qqs_header))
}
qsConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.qs_header_new))
}
largeScreenConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.large_screen_shade_header))
}
}
@Test
fun testEdgeElementsAlignedWithGuide_qqs() {
with(qqsConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(R.id.begin_guide)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
.isEqualTo(1f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias)
.isEqualTo(1f)
}
}
@Test
fun testClockScale() {
with(qqsConstraint.getConstraint(R.id.clock)) {
assertThat(transform.scaleX).isEqualTo(1f)
assertThat(transform.scaleY).isEqualTo(1f)
}
with(qsConstraint.getConstraint(R.id.clock)) {
assertThat(transform.scaleX).isGreaterThan(1f)
assertThat(transform.scaleY).isGreaterThan(1f)
}
}
@Test
fun testEdgeElementsAlignedWithEdgeOrGuide_qs() {
with(qsConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
.isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
.isEqualTo(1f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
}
}
@Test
fun testEdgeElementsAlignedWithEdge_largeScreen() {
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
}
}
@Test
fun testCarrierAlpha() {
assertThat(qqsConstraint.getConstraint(R.id.carrier_group).propertySet.alpha).isEqualTo(0f)
assertThat(qsConstraint.getConstraint(R.id.carrier_group).propertySet.alpha).isEqualTo(1f)
assertThat(largeScreenConstraint.getConstraint(R.id.carrier_group).propertySet.alpha)
.isEqualTo(1f)
}
@Test
fun testPrivacyChipVisibilityConstraints_notVisible() {
val changes = CombinedShadeHeadersConstraintManagerImpl
.privacyChipVisibilityConstraints(false)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
}
@Test
fun testPrivacyChipVisibilityConstraints_visible() {
val changes = CombinedShadeHeadersConstraintManagerImpl
.privacyChipVisibilityConstraints(true)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(0f)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
}
@Test
fun testEmptyCutoutConstraints() {
val changes = CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()
changes()
// QS and Large Screen don't change with cutouts.
assertThat(changes.qsConstraintsChanges).isNull()
assertThat(changes.largeScreenConstraintsChanges).isNull()
with(qqsConstraint) {
// In this case, the date is constrained on the end by a Barrier determined by either
// privacy or statusIcons
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.barrier)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd).isEqualTo(R.id.date)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd).isEqualTo(R.id.date)
assertThat(getConstraint(R.id.barrier).layout.mReferenceIds).asList().containsExactly(
R.id.statusIcons,
R.id.privacy_container
)
assertThat(getConstraint(R.id.barrier).layout.mBarrierDirection).isEqualTo(START)
}
}
@Test
fun testGuidesAreSetInCorrectPosition_largeCutoutSmallerPadding() {
val cutoutStart = 100
val padding = 10
val cutoutEnd = 30
val changes = CombinedShadeHeadersConstraintManagerImpl.edgesGuidelinesConstraints(
cutoutStart,
padding,
cutoutEnd,
padding
)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
.isEqualTo(cutoutStart - padding)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
.isEqualTo(cutoutEnd - padding)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
.isEqualTo(cutoutStart - padding)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
.isEqualTo(cutoutEnd - padding)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testGuidesAreSetInCorrectPosition_smallCutoutLargerPadding() {
val cutoutStart = 5
val padding = 10
val cutoutEnd = 10
val changes = CombinedShadeHeadersConstraintManagerImpl.edgesGuidelinesConstraints(
cutoutStart,
padding,
cutoutEnd,
padding
)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testCenterCutoutConstraints_ltr() {
val offsetFromEdge = 400
val rtl = false
val changes = CombinedShadeHeadersConstraintManagerImpl
.centerCutoutConstraints(rtl, offsetFromEdge)
changes()
// In LTR, center_left is towards the start and center_right is towards the end
with(qqsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_right)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_right)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testCenterCutoutConstraints_rtl() {
val offsetFromEdge = 400
val rtl = true
val changes = CombinedShadeHeadersConstraintManagerImpl
.centerCutoutConstraints(rtl, offsetFromEdge)
changes()
// In RTL, center_left is towards the end and center_right is towards the start
with(qqsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_left)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_left)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
private operator fun ConstraintsChanges.invoke() {
qqsConstraintsChanges?.invoke(qqsConstraint)
qsConstraintsChanges?.invoke(qsConstraint)
largeScreenConstraintsChanges?.invoke(largeScreenConstraint)
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ConstraintChangeTest : SysuiTestCase() {
@Test
fun testSumNonNull() {
val mock1: ConstraintChange = mock()
val mock2: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = mock1 + mock2
sum?.invoke(constraintSet)
val inOrder = inOrder(mock1, mock2)
inOrder.verify(mock1).invoke(constraintSet)
inOrder.verify(mock2).invoke(constraintSet)
}
@Test
fun testSumThisNull() {
val mock: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = (null as? ConstraintChange?) + mock
sum?.invoke(constraintSet)
verify(mock).invoke(constraintSet)
}
@Test
fun testSumThisNull_notWrapped() {
val change: ConstraintChange = {}
val sum = (null as? ConstraintChange?) + change
assertThat(sum).isSameInstanceAs(change)
}
@Test
fun testSumOtherNull() {
val mock: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = mock + (null as? ConstraintChange?)
sum?.invoke(constraintSet)
verify(mock).invoke(constraintSet)
}
@Test
fun testSumOtherNull_notWrapped() {
val change: ConstraintChange = {}
val sum = change + (null as? ConstraintChange?)
assertThat(sum).isSameInstanceAs(change)
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.inOrder
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ConstraintChangesTest : SysuiTestCase() {
@Test
fun testSumWithoutNulls() {
val mockQQS1: ConstraintChange = mock()
val mockQS1: ConstraintChange = mock()
val mockLS1: ConstraintChange = mock()
val mockQQS2: ConstraintChange = mock()
val mockQS2: ConstraintChange = mock()
val mockLS2: ConstraintChange = mock()
val changes1 = ConstraintsChanges(mockQQS1, mockQS1, mockLS1)
val changes2 = ConstraintsChanges(mockQQS2, mockQS2, mockLS2)
val sum = changes1 + changes2
val constraintSet = ConstraintSet()
sum.qqsConstraintsChanges?.invoke(constraintSet)
sum.qsConstraintsChanges?.invoke(constraintSet)
sum.largeScreenConstraintsChanges?.invoke(constraintSet)
val inOrder = inOrder(mockQQS1, mockQS1, mockLS1, mockQQS2, mockQS2, mockLS2)
inOrder.verify(mockQQS1).invoke(constraintSet)
inOrder.verify(mockQQS2).invoke(constraintSet)
inOrder.verify(mockQS1).invoke(constraintSet)
inOrder.verify(mockQS2).invoke(constraintSet)
inOrder.verify(mockLS1).invoke(constraintSet)
inOrder.verify(mockLS2).invoke(constraintSet)
}
@Test
fun testSumWithSomeNulls() {
val mockQQS: ConstraintChange = mock()
val mockQS: ConstraintChange = mock()
val changes1 = ConstraintsChanges(mockQQS, null, null)
val changes2 = ConstraintsChanges(null, mockQS, null)
val sum = changes1 + changes2
assertThat(sum.qqsConstraintsChanges).isSameInstanceAs(mockQQS)
assertThat(sum.qsConstraintsChanges).isSameInstanceAs(mockQS)
assertThat(sum.largeScreenConstraintsChanges).isNull()
}
}

View File

@@ -0,0 +1,659 @@
/*
* 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.shade
import android.content.Context
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.DisplayCutout
import android.view.View
import android.view.WindowInsets
import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.qs.ChipVisibilityListener
import com.android.systemui.qs.HeaderPrivacyIconsController
import com.android.systemui.qs.carrier.QSCarrierGroup
import com.android.systemui.qs.carrier.QSCarrierGroupController
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.VariableDateView
import com.android.systemui.statusbar.policy.VariableDateViewController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
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.Answers
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
private val EMPTY_CHANGES = ConstraintsChanges()
/**
* Tests for [LargeScreenShadeHeaderController] when [Flags.COMBINED_QS_HEADERS] is `true`.
*
* Once that flag is removed, this class will be combined with
* [LargeScreenShadeHeaderControllerTest].
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
@Mock
private lateinit var statusIcons: StatusIconContainer
@Mock
private lateinit var statusBarIconController: StatusBarIconController
@Mock
private lateinit var qsCarrierGroupController: QSCarrierGroupController
@Mock
private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
@Mock
private lateinit var featureFlags: FeatureFlags
@Mock
private lateinit var clock: TextView
@Mock
private lateinit var date: VariableDateView
@Mock
private lateinit var carrierGroup: QSCarrierGroup
@Mock
private lateinit var batteryMeterView: BatteryMeterView
@Mock
private lateinit var batteryMeterViewController: BatteryMeterViewController
@Mock
private lateinit var privacyIconsController: HeaderPrivacyIconsController
@Mock
private lateinit var insetsProvider: StatusBarContentInsetsProvider
@Mock
private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
@Mock
private lateinit var variableDateViewController: VariableDateViewController
@Mock
private lateinit var dumpManager: DumpManager
@Mock
private lateinit var combinedShadeHeadersConstraintManager:
CombinedShadeHeadersConstraintManager
@Mock
private lateinit var mockedContext: Context
@Mock(answer = Answers.RETURNS_MOCKS)
private lateinit var view: MotionLayout
@Mock
private lateinit var qqsConstraints: ConstraintSet
@Mock
private lateinit var qsConstraints: ConstraintSet
@Mock
private lateinit var largeScreenConstraints: ConstraintSet
@JvmField @Rule
val mockitoRule = MockitoJUnit.rule()
var viewVisibility = View.GONE
private lateinit var controller: LargeScreenShadeHeaderController
private lateinit var carrierIconSlots: List<String>
private val configurationController = FakeConfigurationController()
@Before
fun setUp() {
whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock)
whenever(clock.context).thenReturn(mockedContext)
whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
whenever(date.context).thenReturn(mockedContext)
whenever(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
whenever<QSCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup)
whenever<BatteryMeterView>(view.findViewById(R.id.batteryRemainingIcon))
.thenReturn(batteryMeterView)
whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
whenever(statusIcons.context).thenReturn(context)
whenever(qsCarrierGroupControllerBuilder.setQSCarrierGroup(any()))
.thenReturn(qsCarrierGroupControllerBuilder)
whenever(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
whenever(view.context).thenReturn(context)
whenever(view.resources).thenReturn(context.resources)
whenever(view.setVisibility(ArgumentMatchers.anyInt())).then {
viewVisibility = it.arguments[0] as Int
null
}
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true)
setUpDefaultInsets()
setUpMotionLayout(view)
controller = LargeScreenShadeHeaderController(
view,
statusBarIconController,
privacyIconsController,
insetsProvider,
configurationController,
variableDateViewControllerFactory,
batteryMeterViewController,
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder,
combinedShadeHeadersConstraintManager
)
whenever(view.isAttachedToWindow).thenReturn(true)
controller.init()
carrierIconSlots = listOf(
context.getString(com.android.internal.R.string.status_bar_mobile))
}
@Test
fun testCorrectConstraints() {
val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java)
verify(qqsConstraints).load(eq(context), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header)
verify(qsConstraints).load(eq(context), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header_new)
verify(largeScreenConstraints).load(eq(context), capture(captor))
assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
}
@Test
fun testControllersCreatedAndInitialized() {
verify(variableDateViewController).init()
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 testClockPivotLtr() {
val width = 200
whenever(clock.width).thenReturn(width)
whenever(clock.isLayoutRtl).thenReturn(false)
val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
verify(clock).addOnLayoutChangeListener(capture(captor))
captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
verify(clock).pivotX = 0f
}
@Test
fun testClockPivotRtl() {
val width = 200
whenever(clock.width).thenReturn(width)
whenever(clock.isLayoutRtl).thenReturn(true)
val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
verify(clock).addOnLayoutChangeListener(capture(captor))
captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7)
verify(clock).pivotX = width.toFloat()
}
@Test
fun testShadeExpanded_true() {
// When shade is expanded, view should be visible regardless of largeScreenActive
controller.largeScreenActive = false
controller.qsVisible = true
assertThat(viewVisibility).isEqualTo(View.VISIBLE)
controller.largeScreenActive = true
assertThat(viewVisibility).isEqualTo(View.VISIBLE)
}
@Test
fun testShadeExpanded_false() {
// When shade is not expanded, view should be invisible regardless of largeScreenActive
controller.largeScreenActive = false
controller.qsVisible = false
assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
controller.largeScreenActive = true
assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
}
@Test
fun testLargeScreenActive_true() {
controller.largeScreenActive = false // Make sure there's a change
clearInvocations(view)
controller.largeScreenActive = true
verify(view).setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
}
@Test
fun testLargeScreenActive_false() {
controller.largeScreenActive = true // Make sure there's a change
clearInvocations(view)
controller.largeScreenActive = false
verify(view).setTransition(HEADER_TRANSITION_ID)
}
@Test
fun testShadeExpandedFraction() {
// View needs to be visible for this to actually take effect
controller.qsVisible = true
clearInvocations(view)
controller.shadeExpandedFraction = 0.3f
verify(view).alpha = ShadeInterpolation.getContentAlpha(0.3f)
clearInvocations(view)
controller.shadeExpandedFraction = 1f
verify(view).alpha = ShadeInterpolation.getContentAlpha(1f)
clearInvocations(view)
controller.shadeExpandedFraction = 0f
verify(view).alpha = ShadeInterpolation.getContentAlpha(0f)
}
@Test
fun testQsExpandedFraction_headerTransition() {
controller.qsVisible = true
controller.largeScreenActive = false
clearInvocations(view)
controller.qsExpandedFraction = 0.3f
verify(view).progress = 0.3f
}
@Test
fun testQsExpandedFraction_largeScreen() {
controller.qsVisible = true
controller.largeScreenActive = true
clearInvocations(view)
controller.qsExpandedFraction = 0.3f
verify(view, never()).progress = anyFloat()
}
@Test
fun testScrollY_headerTransition() {
controller.largeScreenActive = false
clearInvocations(view)
controller.qsScrollY = 20
verify(view).scrollY = 20
}
@Test
fun testScrollY_largeScreen() {
controller.largeScreenActive = true
clearInvocations(view)
controller.qsScrollY = 20
verify(view, never()).scrollY = anyInt()
}
@Test
fun testPrivacyChipVisibilityChanged_visible_changesCorrectConstraints() {
val chipVisibleChanges = createMockConstraintChanges()
val chipNotVisibleChanges = createMockConstraintChanges()
whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
.thenReturn(chipVisibleChanges)
whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
.thenReturn(chipNotVisibleChanges)
val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
verify(privacyIconsController).chipVisibilityListener = capture(captor)
captor.value.onChipVisibilityRefreshed(true)
verify(chipVisibleChanges.qqsConstraintsChanges)!!.invoke(qqsConstraints)
verify(chipVisibleChanges.qsConstraintsChanges)!!.invoke(qsConstraints)
verify(chipVisibleChanges.largeScreenConstraintsChanges)!!.invoke(largeScreenConstraints)
verify(chipNotVisibleChanges.qqsConstraintsChanges, never())!!.invoke(any())
verify(chipNotVisibleChanges.qsConstraintsChanges, never())!!.invoke(any())
verify(chipNotVisibleChanges.largeScreenConstraintsChanges, never())!!.invoke(any())
}
@Test
fun testPrivacyChipVisibilityChanged_notVisible_changesCorrectConstraints() {
val chipVisibleChanges = createMockConstraintChanges()
val chipNotVisibleChanges = createMockConstraintChanges()
whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true))
.thenReturn(chipVisibleChanges)
whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false))
.thenReturn(chipNotVisibleChanges)
val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java)
verify(privacyIconsController).chipVisibilityListener = capture(captor)
captor.value.onChipVisibilityRefreshed(false)
verify(chipVisibleChanges.qqsConstraintsChanges, never())!!.invoke(qqsConstraints)
verify(chipVisibleChanges.qsConstraintsChanges, never())!!.invoke(qsConstraints)
verify(chipVisibleChanges.largeScreenConstraintsChanges, never())!!
.invoke(largeScreenConstraints)
verify(chipNotVisibleChanges.qqsConstraintsChanges)!!.invoke(any())
verify(chipNotVisibleChanges.qsConstraintsChanges)!!.invoke(any())
verify(chipNotVisibleChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testInsetsGuides_ltr() {
whenever(view.isLayoutRtl).thenReturn(false)
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
val (insetLeft, insetRight) = 30 to 40
val (paddingStart, paddingEnd) = 10 to 20
whenever(view.paddingStart).thenReturn(paddingStart)
whenever(view.paddingEnd).thenReturn(paddingEnd)
mockInsetsProvider(insetLeft to insetRight, false)
whenever(combinedShadeHeadersConstraintManager
.edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
).thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets())
verify(combinedShadeHeadersConstraintManager)
.edgesGuidelinesConstraints(insetLeft, paddingStart, insetRight, paddingEnd)
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testInsetsGuides_rtl() {
whenever(view.isLayoutRtl).thenReturn(true)
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
val (insetLeft, insetRight) = 30 to 40
val (paddingStart, paddingEnd) = 10 to 20
whenever(view.paddingStart).thenReturn(paddingStart)
whenever(view.paddingEnd).thenReturn(paddingEnd)
mockInsetsProvider(insetLeft to insetRight, false)
whenever(combinedShadeHeadersConstraintManager
.edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
).thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets())
verify(combinedShadeHeadersConstraintManager)
.edgesGuidelinesConstraints(insetRight, paddingStart, insetLeft, paddingEnd)
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testNullCutout() {
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
.thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets(null))
verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
verify(combinedShadeHeadersConstraintManager, never())
.centerCutoutConstraints(anyBoolean(), anyInt())
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testEmptyCutout() {
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
.thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets())
verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
verify(combinedShadeHeadersConstraintManager, never())
.centerCutoutConstraints(anyBoolean(), anyInt())
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testCornerCutout_emptyRect() {
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
mockInsetsProvider(0 to 0, true)
whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
.thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets())
verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
verify(combinedShadeHeadersConstraintManager, never())
.centerCutoutConstraints(anyBoolean(), anyInt())
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testCornerCutout_nonEmptyRect() {
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
mockInsetsProvider(0 to 0, true)
whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
.thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(1, 2, 3, 4)))
verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints()
verify(combinedShadeHeadersConstraintManager, never())
.centerCutoutConstraints(anyBoolean(), anyInt())
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testTopCutout_ltr() {
val width = 100
val paddingLeft = 10
val paddingRight = 20
val cutoutWidth = 30
whenever(view.isLayoutRtl).thenReturn(false)
whenever(view.width).thenReturn(width)
whenever(view.paddingLeft).thenReturn(paddingLeft)
whenever(view.paddingRight).thenReturn(paddingRight)
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
mockInsetsProvider(0 to 0, false)
whenever(combinedShadeHeadersConstraintManager
.centerCutoutConstraints(anyBoolean(), anyInt())
).thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
verify(combinedShadeHeadersConstraintManager, never()).emptyCutoutConstraints()
val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(false, offset)
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
@Test
fun testTopCutout_rtl() {
val width = 100
val paddingLeft = 10
val paddingRight = 20
val cutoutWidth = 30
whenever(view.isLayoutRtl).thenReturn(true)
whenever(view.width).thenReturn(width)
whenever(view.paddingLeft).thenReturn(paddingLeft)
whenever(view.paddingRight).thenReturn(paddingRight)
val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
verify(view).setOnApplyWindowInsetsListener(capture(captor))
val mockConstraintsChanges = createMockConstraintChanges()
mockInsetsProvider(0 to 0, false)
whenever(combinedShadeHeadersConstraintManager
.centerCutoutConstraints(anyBoolean(), anyInt())
).thenReturn(mockConstraintsChanges)
captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1)))
verify(combinedShadeHeadersConstraintManager, never()).emptyCutoutConstraints()
val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2
verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(true, offset)
verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any())
verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any())
}
private fun createWindowInsets(
topCutout: Rect? = Rect()
): WindowInsets {
val windowInsets: WindowInsets = mock()
val displayCutout: DisplayCutout = mock()
whenever(windowInsets.displayCutout)
.thenReturn(if (topCutout != null) displayCutout else null)
whenever(displayCutout.boundingRectTop).thenReturn(topCutout)
return windowInsets
}
private fun mockInsetsProvider(
insets: Pair<Int, Int> = 0 to 0,
cornerCutout: Boolean = false,
) {
whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
.thenReturn(insets.toAndroidPair())
whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout)
}
private fun createMockConstraintChanges(): ConstraintsChanges {
return ConstraintsChanges(mock(), mock(), mock())
}
private fun XmlResourceParser.getResId(): Int {
return Resources.getAttributeSetSourceResId(this)
}
private fun setUpMotionLayout(motionLayout: MotionLayout) {
whenever(motionLayout.getConstraintSet(QQS_HEADER_CONSTRAINT)).thenReturn(qqsConstraints)
whenever(motionLayout.getConstraintSet(QS_HEADER_CONSTRAINT)).thenReturn(qsConstraints)
whenever(motionLayout.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT))
.thenReturn(largeScreenConstraints)
}
private fun setUpDefaultInsets() {
whenever(combinedShadeHeadersConstraintManager
.edgesGuidelinesConstraints(anyInt(), anyInt(), anyInt(), anyInt())
).thenReturn(EMPTY_CHANGES)
whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints())
.thenReturn(EMPTY_CHANGES)
whenever(combinedShadeHeadersConstraintManager
.centerCutoutConstraints(anyBoolean(), anyInt())
).thenReturn(EMPTY_CHANGES)
whenever(combinedShadeHeadersConstraintManager
.privacyChipVisibilityConstraints(anyBoolean())
).thenReturn(EMPTY_CHANGES)
whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
.thenReturn(Pair(0, 0).toAndroidPair())
whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false)
}
private fun<T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> {
return android.util.Pair(first, second)
}
}

View File

@@ -1,4 +1,4 @@
package com.android.systemui.statusbar.phone
package com.android.systemui.shade
import android.app.StatusBarManager
import android.content.Context
@@ -17,10 +17,14 @@ import com.android.systemui.flags.Flags
import com.android.systemui.qs.HeaderPrivacyIconsController
import com.android.systemui.qs.carrier.QSCarrierGroup
import com.android.systemui.qs.carrier.QSCarrierGroupController
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.VariableDateViewController
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -28,6 +32,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@@ -51,6 +56,8 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
@Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
@Mock private lateinit var variableDateViewController: VariableDateViewController
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var combinedShadeHeadersConstraintManager:
CombinedShadeHeadersConstraintManager
@Mock private lateinit var mockedContext: Context
@@ -95,7 +102,8 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
batteryMeterViewController,
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder
qsCarrierGroupControllerBuilder,
combinedShadeHeadersConstraintManager
)
whenever(view.isAttachedToWindow).thenReturn(true)
mLargeScreenShadeHeaderController.init()
@@ -103,6 +111,11 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
context.getString(com.android.internal.R.string.status_bar_mobile))
}
@After
fun verifyEveryTest() {
verifyZeroInteractions(combinedShadeHeadersConstraintManager)
}
@Test
fun setVisible_onlyWhenActive() {
makeShadeVisible()

View File

@@ -143,7 +143,6 @@ import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.PanelViewController;