From fa5d3284594d6b3ec97b90154b201fdfe95b0764 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 13 Jul 2022 14:17:31 -0400 Subject: [PATCH] Add unit tests for combined QS headers Also, some minor fixes in the constraints. Bug: 215584502 Test: atest SystemUITests Change-Id: I23e5e4927010d60084e4b89c05f990fa268c2f2c --- .../res/layout/combined_qs_header.xml | 2 + .../res/xml/large_screen_shade_header.xml | 1 + packages/SystemUI/res/xml/qqs_header.xml | 2 +- packages/SystemUI/res/xml/qs_header_new.xml | 1 + .../CombinedShadeHeadersConstraintManager.kt | 79 +++ ...mbinedShadeHeadersConstraintManagerImpl.kt | 109 +++ .../LargeScreenShadeHeaderController.kt | 144 ++-- .../NotificationPanelViewController.java | 1 - .../dagger/CentralSurfacesComponent.java | 2 +- .../phone/dagger/StatusBarViewModule.java | 10 + .../CombinedShadeHeaderConstraintsTest.kt | 328 +++++++++ .../systemui/shade/ConstraintChangeTest.kt | 85 +++ .../systemui/shade/ConstraintChangesTest.kt | 75 ++ ...ScreenShadeHeaderControllerCombinedTest.kt | 659 ++++++++++++++++++ .../LargeScreenShadeHeaderControllerTest.kt | 17 +- .../NotificationPanelViewControllerTest.java | 1 - 16 files changed, 1433 insertions(+), 83 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt create mode 100644 packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt rename packages/SystemUI/src/com/android/systemui/{statusbar/phone => shade}/LargeScreenShadeHeaderController.kt (80%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangeTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangesTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt rename packages/SystemUI/tests/src/com/android/systemui/{statusbar/phone => shade}/LargeScreenShadeHeaderControllerTest.kt (91%) diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index a031fd4dfc6eb..5dc34b9db594f 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -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" diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index b3814d154c222..cdbf8ab0be412 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -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" /> diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index 7b6e3e0fc8cdb..ee0c4fb6bab83 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -89,7 +89,7 @@ app:layout_constraintTop_toTopOf="parent" /> diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml index e169f7e2a8d75..f39e6bd65b867 100644 --- a/packages/SystemUI/res/xml/qs_header_new.xml +++ b/packages/SystemUI/res/xml/qs_header_new.xml @@ -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" /> diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt new file mode 100644 index 0000000000000..e360ec20bd9bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt @@ -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 +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt new file mode 100644 index 0000000000000..4063af3cbc365 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt @@ -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 + ) + } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt similarity index 80% rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt rename to packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index 775375784717c..5793105e481ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -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(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) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 8b6a62e498fe6..61d19634af381 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java index 84b279760f362..64b04e93e69ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index 41df8e3cbb319..def23e8eaf3c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt new file mode 100644 index 0000000000000..0ce9056dc1d11 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt @@ -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) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangeTest.kt new file mode 100644 index 0000000000000..9b2e085560a14 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangeTest.kt @@ -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) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangesTest.kt new file mode 100644 index 0000000000000..0abb084274787 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangesTest.kt @@ -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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt new file mode 100644 index 0000000000000..ed1a13b36d6cd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -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 + private val configurationController = FakeConfigurationController() + + @Before + fun setUp() { + whenever(view.findViewById(R.id.clock)).thenReturn(clock) + whenever(clock.context).thenReturn(mockedContext) + + whenever(view.findViewById(R.id.date)).thenReturn(date) + whenever(date.context).thenReturn(mockedContext) + whenever(variableDateViewControllerFactory.create(any())) + .thenReturn(variableDateViewController) + + whenever(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup) + whenever(view.findViewById(R.id.batteryRemainingIcon)) + .thenReturn(batteryMeterView) + + whenever(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 = 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 Pair.toAndroidPair(): android.util.Pair { + return android.util.Pair(first, second) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt similarity index 91% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderControllerTest.kt rename to packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt index 3cafa5c00f598..02b26dbbc32d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt @@ -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() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index b621f2ef16649..30c1b2594ee86 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -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;