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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<PropertySet
|
||||
app:alpha="0"
|
||||
android:alpha="0"
|
||||
/>
|
||||
</Constraint>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user