diff options
21 files changed, 2302 insertions, 384 deletions
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index ec82ccf2022e..5dc34b9db594 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<androidx.constraintlayout.motion.widget.MotionLayout +<com.android.systemui.util.NoRemeasureMotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/split_shade_status_bar" @@ -32,10 +32,37 @@ <androidx.constraintlayout.widget.Guideline android:layout_width="wrap_content" android:layout_height="wrap_content" - android:id="@+id/center" - app:layout_constraintGuide_percent="0.5" + android:id="@+id/begin_guide" + android:orientation="vertical" + app:layout_constraintGuide_begin="0dp"/> + + <androidx.constraintlayout.widget.Guideline + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/end_guide" + android:orientation="vertical" + app:layout_constraintGuide_end="0dp" + /> + + <androidx.constraintlayout.widget.Guideline + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/center_left" android:orientation="vertical" /> + <androidx.constraintlayout.widget.Guideline + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/center_right" + android:orientation="vertical" /> + + <androidx.constraintlayout.widget.Barrier + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/barrier" + app:barrierDirection="start" + app:constraint_referenced_ids="statusIcons,privacy_container" /> + <com.android.systemui.statusbar.policy.Clock android:id="@+id/clock" android:layout_width="wrap_content" @@ -44,18 +71,25 @@ 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" + android:scaleX="1" + android:scaleY="1" /> - <com.android.systemui.statusbar.policy.DateView + <com.android.systemui.statusbar.policy.VariableDateView android:id="@+id/date" android:layout_width="wrap_content" android:layout_height="0dp" android:layout_gravity="start|center_vertical" android:gravity="center_vertical" android:singleLine="true" + android:textDirection="locale" android:textAppearance="@style/TextAppearance.QS.Status" - app:datePattern="@string/abbrev_wday_month_day_no_year_alarm" + app:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm" + app:shortDatePattern="@string/abbrev_month_day_no_year" /> <include @@ -81,7 +115,7 @@ app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" android:paddingEnd="@dimen/signal_cluster_battery_padding" android:layout_width="wrap_content" - android:layout_height="48dp" + android:layout_height="@dimen/large_screen_shade_header_min_height" app:layout_constraintStart_toEndOf="@id/carrier_group" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="@id/clock" @@ -92,8 +126,9 @@ <com.android.systemui.battery.BatteryMeterView android:id="@+id/batteryRemainingIcon" android:layout_width="wrap_content" - android:layout_height="48dp" + android:layout_height="@dimen/large_screen_shade_header_min_height" app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" + app:layout_constrainedWidth="true" app:textAppearance="@style/TextAppearance.QS.Status" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" @@ -104,13 +139,18 @@ <FrameLayout android:id="@+id/privacy_container" android:layout_width="wrap_content" - android:layout_height="48dp" + android:layout_height="@dimen/large_screen_shade_header_min_height" android:gravity="center" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@id/end_guide" app:layout_constraintTop_toTopOf="@id/date" app:layout_constraintBottom_toBottomOf="@id/date" > <include layout="@layout/ongoing_privacy_chip"/> </FrameLayout> -</androidx.constraintlayout.motion.widget.MotionLayout>
\ No newline at end of file + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:id="@+id/space" + /> +</com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 022a6b2f4da2..0cafa355d847 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -553,7 +553,7 @@ <dimen name="qs_dual_tile_padding_horizontal">6dp</dimen> <dimen name="qs_panel_elevation">4dp</dimen> <dimen name="qs_panel_padding_bottom">@dimen/footer_actions_height</dimen> - <dimen name="qs_panel_padding_top">48dp</dimen> + <dimen name="qs_panel_padding_top">80dp</dimen> <dimen name="qs_data_usage_text_size">14sp</dimen> <dimen name="qs_data_usage_usage_text_size">36sp</dimen> @@ -564,6 +564,8 @@ <dimen name="qs_footer_icon_size">20dp</dimen> <dimen name="qs_header_row_min_height">48dp</dimen> + <dimen name="qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> <dimen name="qs_security_footer_single_line_height">48dp</dimen> diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index 0e833265c15f..0fac76d11fbc 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -25,20 +25,109 @@ <KeyFrameSet> <!-- These positions are to prevent visual movement of @id/date --> <KeyPosition - app:keyPositionType="pathRelative" + app:keyPositionType="deltaRelative" app:percentX="0" + app:percentY="0" app:framePosition="49" + app:percentWidth="1" + app:percentHeight="1" + app:curveFit="linear" app:motionTarget="@id/date" /> <KeyPosition - app:keyPositionType="pathRelative" + app:keyPositionType="deltaRelative" app:percentX="1" + app:percentY="0.51" app:framePosition="51" + app:percentWidth="1" + app:percentHeight="1" + app:curveFit="linear" app:motionTarget="@id/date" /> <KeyAttribute app:motionTarget="@id/date" + app:framePosition="30" + android:alpha="0" + /> + <KeyAttribute + app:motionTarget="@id/date" + app:framePosition="70" + android:alpha="0" + /> + <KeyPosition + app:keyPositionType="pathRelative" + app:percentX="0" + app:percentY="0" + app:framePosition="0" + app:curveFit="linear" + app:motionTarget="@id/statusIcons" /> + <KeyPosition + app:keyPositionType="pathRelative" + app:percentX="0" + app:percentY="0" + app:framePosition="50" + app:curveFit="linear" + app:motionTarget="@id/statusIcons" /> + <KeyPosition + app:keyPositionType="deltaRelative" + app:percentX="1" + app:percentY="0.51" + app:framePosition="51" + app:curveFit="linear" + app:motionTarget="@id/statusIcons" /> + <KeyAttribute + app:motionTarget="@id/statusIcons" + app:framePosition="30" + android:alpha="0" + /> + <KeyAttribute + app:motionTarget="@id/statusIcons" + app:framePosition="70" + android:alpha="0" + /> + <KeyPosition + app:keyPositionType="deltaRelative" + app:percentX="0" + app:percentY="0" app:framePosition="50" + app:percentWidth="1" + app:percentHeight="1" + app:curveFit="linear" + app:motionTarget="@id/batteryRemainingIcon" /> + <KeyPosition + app:keyPositionType="deltaRelative" + app:percentX="1" + app:percentY="0.51" + app:framePosition="51" + app:percentWidth="1" + app:percentHeight="1" + app:curveFit="linear" + app:motionTarget="@id/batteryRemainingIcon" /> + <KeyAttribute + app:motionTarget="@id/batteryRemainingIcon" + app:framePosition="30" android:alpha="0" /> + <KeyAttribute + app:motionTarget="@id/batteryRemainingIcon" + app:framePosition="70" + android:alpha="0" + /> + <KeyPosition + app:motionTarget="@id/carrier_group" + app:percentX="1" + app:percentY="0.51" + app:framePosition="51" + app:percentWidth="1" + app:percentHeight="1" + app:curveFit="linear" + app:keyPositionType="deltaRelative" /> + <KeyAttribute + app:motionTarget="@id/carrier_group" + app:framePosition="0" + android:alpha="0" /> + <KeyAttribute + app:motionTarget="@id/carrier_group" + app:framePosition="70" + android:alpha="0" /> </KeyFrameSet> </Transition> diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml index 89090513ea37..cdbf8ab0be41 100644 --- a/packages/SystemUI/res/xml/large_screen_shade_header.xml +++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml @@ -29,7 +29,12 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/date" + app:layout_constraintHorizontal_bias="0" /> + <Transform + android:scaleX="1" + android:scaleY="1" + /> </Constraint> <Constraint @@ -47,9 +52,38 @@ <Constraint android:id="@+id/carrier_group"> + <Layout + app:layout_constraintWidth_min="48dp" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constrainedWidth="true" + android:layout_gravity="end|center_vertical" + android:layout_marginStart="8dp" + app:layout_constraintStart_toEndOf="@id/date" + app:layout_constraintEnd_toStartOf="@id/statusIcons" + app:layout_constraintTop_toTopOf="@id/clock" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> + <PropertySet + android:alpha="1" + /> + </Constraint> + + <Constraint + android:id="@+id/statusIcons"> + <Layout + app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" + android:layout_width="wrap_content" + android:layout_height="@dimen/large_screen_shade_header_min_height" + app:layout_constraintStart_toEndOf="@id/carrier_group" + app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" + app:layout_constraintTop_toTopOf="@id/clock" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> <PropertySet android:alpha="1" - app:customFloatValue="1" /> </Constraint> @@ -64,6 +98,9 @@ app:layout_constraintTop_toTopOf="@id/clock" app:layout_constraintBottom_toBottomOf="parent" /> + <PropertySet + android:alpha="1" + /> </Constraint> <Constraint @@ -75,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> diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index c5b4c5d776b9..ee0c4fb6bab8 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -26,22 +26,27 @@ <Layout android:layout_width="wrap_content" android:layout_height="0dp" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toStartOf="@id/begin_guide" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/date" app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_chainStyle="packed" /> + <Transform + android:scaleX="1" + android:scaleY="1" + /> </Constraint> <Constraint android:id="@+id/date"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="0dp" + app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/clock" - app:layout_constraintEnd_toStartOf="@id/carrier_group" + app:layout_constraintEnd_toStartOf="@id/barrier" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0" @@ -50,17 +55,41 @@ <Constraint android:id="@+id/statusIcons"> + <Layout + android:layout_width="0dp" + android:layout_height="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintStart_toEndOf="@id/date" + app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" + app:layout_constraintTop_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> </Constraint> <Constraint - android:id="@+id/batteryRemainingIcon" > + android:id="@+id/batteryRemainingIcon"> + <Layout + android:layout_width="wrap_content" + android:layout_height="@dimen/qs_header_non_clickable_element_height" + app:layout_constrainedWidth="true" + app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintStart_toEndOf="@id/statusIcons" + app:layout_constraintEnd_toEndOf="@id/end_guide" + app:layout_constraintTop_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> </Constraint> <Constraint android:id="@+id/carrier_group"> - <CustomAttribute - app:attributeName="alpha" - app:customFloatValue="0" + <Layout + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + /> + <PropertySet + android:alpha="0" /> </Constraint> @@ -69,9 +98,11 @@ <Layout android:layout_width="wrap_content" android:layout_height="0dp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/date" + app:layout_constraintEnd_toEndOf="@id/end_guide" app:layout_constraintTop_toTopOf="@id/date" app:layout_constraintBottom_toBottomOf="@id/date" + app:layout_constraintHorizontal_bias="1" /> </Constraint> diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml new file mode 100644 index 000000000000..f39e6bd65b86 --- /dev/null +++ b/packages/SystemUI/res/xml/qs_header_new.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<ConstraintSet + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/qs_header_constraint" +> + + <Constraint + android:id="@+id/privacy_container"> + <Layout + android:layout_width="wrap_content" + android:layout_height="@dimen/large_screen_shade_header_min_height" + 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> + + <Constraint + android:id="@+id/clock"> + <Layout + android:layout_width="wrap_content" + android:layout_height="@dimen/large_screen_shade_header_min_height" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/privacy_container" + app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintEnd_toStartOf="@id/carrier_group" + app:layout_constraintHorizontal_bias="0" + /> + <Transform + android:scaleX="2.4" + android:scaleY="2.4" + /> + </Constraint> + + <Constraint + android:id="@+id/date"> + <Layout + android:layout_width="0dp" + android:layout_height="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/space" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/clock" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="spread_inside" + /> + </Constraint> + + <Constraint + android:id="@+id/carrier_group"> + <Layout + app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height" + android:minHeight="@dimen/large_screen_shade_header_min_height" + app:layout_constraintWidth_min="48dp" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintStart_toEndOf="@id/clock" + app:layout_constraintTop_toBottomOf="@id/privacy_container" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon" + /> + <PropertySet + android:alpha="1" + /> + </Constraint> + + <Constraint + android:id="@+id/statusIcons"> + <Layout + android:layout_width="0dp" + android:layout_height="@dimen/qs_header_non_clickable_element_height" + app:layout_constrainedWidth="true" + app:layout_constraintStart_toEndOf="@id/space" + app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" + app:layout_constraintTop_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> + </Constraint> + + <Constraint + android:id="@+id/batteryRemainingIcon"> + <Layout + android:layout_width="wrap_content" + android:layout_height="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintStart_toEndOf="@id/statusIcons" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="1" + /> + </Constraint> + + + <Constraint + android:id="@id/space"> + <Layout + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintStart_toEndOf="@id/date" + app:layout_constraintEnd_toStartOf="@id/statusIcons" + /> + </Constraint> +</ConstraintSet>
\ No newline at end of file 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 000000000000..e360ec20bd9b --- /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 000000000000..4063af3cbc36 --- /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/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt new file mode 100644 index 000000000000..5793105e481e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.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.WindowInsets +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.constraintlayout.motion.widget.MotionLayout +import com.android.settingslib.Utils +import com.android.systemui.Dumpable +import com.android.systemui.R +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.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 +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.VariableDateView +import com.android.systemui.statusbar.policy.VariableDateViewController +import com.android.systemui.util.ViewController +import java.io.PrintWriter +import javax.inject.Inject +import javax.inject.Named + +/** + * Controller for QS header on Large Screen width (large screen + landscape). + * + * Additionally, this serves as the staging ground for the combined QS headers. A single + * [MotionLayout] that changes constraints depending on the configuration and can animate the + * expansion of the headers in small screen portrait. + * + * [header] will be a [MotionLayout] if [Flags.COMBINED_QS_HEADERS] is enabled. In this case, the + * [MotionLayout] has 2 transitions: + * * [HEADER_TRANSITION_ID]: [QQS_HEADER_CONSTRAINT] <-> [QS_HEADER_CONSTRAINT] for portrait + * handheld device configuration. + * * [LARGE_SCREEN_HEADER_TRANSITION_ID]: [LARGE_SCREEN_HEADER_CONSTRAINT] (to itself) for all + * other configurations + */ +@CentralSurfacesScope +class LargeScreenShadeHeaderController @Inject constructor( + @Named(LARGE_SCREEN_SHADE_HEADER) private val header: View, + private val statusBarIconController: StatusBarIconController, + private val privacyIconsController: HeaderPrivacyIconsController, + private val insetsProvider: StatusBarContentInsetsProvider, + private val configurationController: ConfigurationController, + private val variableDateViewControllerFactory: VariableDateViewController.Factory, + @Named(LARGE_SCREEN_BATTERY_CONTROLLER) + private val batteryMeterViewController: BatteryMeterViewController, + private val dumpManager: DumpManager, + private val featureFlags: FeatureFlags, + 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. + */ + @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" + QS_HEADER_CONSTRAINT -> "QS Header" + LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header" + else -> "Unknown state" + } + } + + init { + loadConstraints() + } + + private val combinedHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS) + + private lateinit var iconManager: StatusBarIconController.TintedIconManager + private lateinit var carrierIconSlots: List<String> + private lateinit var qsCarrierGroupController: QSCarrierGroupController + + private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon) + private val clock: TextView = header.findViewById(R.id.clock) + private val date: TextView = header.findViewById(R.id.date) + private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons) + private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group) + + private var cutoutLeft = 0 + private var cutoutRight = 0 + private var roundedCorners = 0 + private var lastInsets: WindowInsets? = null + + private var qsDisabled = false + private var visible = false + set(value) { + if (field == value) { + return + } + field = value + updateListeners() + } + + /** + * Whether the QQS/QS part of the shade is visible. This is particularly important in + * Lockscreen, as the shade is visible but QS is not. + */ + var qsVisible = false + set(value) { + if (field == value) { + return + } + field = value + onShadeExpandedChanged() + } + + /** + * Whether we are in a configuration with large screen width. In this case, the header is a + * single line. + */ + var largeScreenActive = false + set(value) { + if (field == value) { + return + } + field = value + onHeaderStateChanged() + } + + /** + * Expansion fraction of the QQS/QS shade. This is not the expansion between QQS <-> QS. + */ + var shadeExpandedFraction = -1f + set(value) { + if (visible && field != value) { + header.alpha = ShadeInterpolation.getContentAlpha(value) + field = value + } + } + + /** + * Expansion fraction of the QQS <-> QS animation. + */ + var qsExpandedFraction = -1f + set(value) { + if (visible && field != value) { + field = value + updatePosition() + } + } + + /** + * Current scroll of QS. + */ + var qsScrollY = 0 + set(value) { + if (field != value) { + field = value + updateScrollY() + } + } + + private val insetListener = View.OnApplyWindowInsetsListener { view, insets -> + updateConstraintsForInsets(view as MotionLayout, insets) + lastInsets = WindowInsets(insets) + + view.onApplyWindowInsets(insets) + } + + private val chipVisibilityListener: ChipVisibilityListener = object : ChipVisibilityListener { + override fun onChipVisibilityRefreshed(visible: Boolean) { + if (header is MotionLayout) { + // If the privacy chip is visible, we hide the status icons and battery remaining + // icon, only in QQS. + val update = combinedShadeHeadersConstraintManager + .privacyChipVisibilityConstraints(visible) + header.updateAllConstraints(update) + } + } + } + + private val configurationControllerListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + if (header !is MotionLayout) { + val left = header.resources.getDimensionPixelSize( + R.dimen.large_screen_shade_header_left_padding + ) + header.setPadding( + left, + header.paddingTop, + header.paddingRight, + header.paddingBottom + ) + } + } + + override fun onDensityOrFontScaleChanged() { + clock.setTextAppearance(R.style.TextAppearance_QS_Status) + date.setTextAppearance(R.style.TextAppearance_QS_Status) + qsCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) + if (header is MotionLayout) { + loadConstraints() + lastInsets?.let { updateConstraintsForInsets(header, it) } + } + updateResources() + } + } + + override fun onInit() { + if (header is MotionLayout) { + variableDateViewControllerFactory.create(date as VariableDateView).init() + } + batteryMeterViewController.init() + + // battery settings same as in QS icons + batteryMeterViewController.ignoreTunerUpdates() + batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) + + iconManager = StatusBarIconController.TintedIconManager(iconContainer, featureFlags) + iconManager.setTint( + Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary) + ) + + carrierIconSlots = if (featureFlags.isEnabled(Flags.COMBINED_STATUS_BAR_SIGNAL_ICONS)) { + listOf( + header.context.getString(com.android.internal.R.string.status_bar_no_calling), + header.context.getString(com.android.internal.R.string.status_bar_call_strength) + ) + } else { + listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile)) + } + qsCarrierGroupController = qsCarrierGroupControllerBuilder + .setQSCarrierGroup(qsCarrierGroup) + .build() + } + + override fun onViewAttached() { + 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) + configurationController.addCallback(configurationControllerListener) + + updateVisibility() + updateTransition() + } + + override fun onViewDetached() { + privacyIconsController.chipVisibilityListener = null + dumpManager.unregisterDumpable(this::class.java.simpleName) + configurationController.removeCallback(configurationControllerListener) + } + + fun disable(state1: Int, state2: Int, animate: Boolean) { + val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0 + if (disabled == qsDisabled) return + qsDisabled = disabled + updateVisibility() + } + + private fun loadConstraints() { + if (header is MotionLayout) { + // Use resources.getXml instead of passing the resource id due to bug b/205018300 + header.getConstraintSet(QQS_HEADER_CONSTRAINT) + .load(context, resources.getXml(R.xml.qqs_header)) + val qsConstraints = if (featureFlags.isEnabled(Flags.NEW_HEADER)) { + R.xml.qs_header_new + } else { + R.xml.qs_header + } + header.getConstraintSet(QS_HEADER_CONSTRAINT) + .load(context, resources.getXml(qsConstraints)) + header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT) + .load(context, resources.getXml(R.xml.large_screen_shade_header)) + } + } + + private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) { + val cutout = insets.displayCutout + + val sbInsets: Pair<Int, Int> = insetsProvider.getStatusBarContentInsetsForCurrentRotation() + cutoutLeft = sbInsets.first + cutoutRight = sbInsets.second + val hasCornerCutout: Boolean = insetsProvider.currentRotationHasCornerCutout() + updateQQSPaddings() + // Set these guides as the left/right limits for content that lives in the top row, using + // cutoutLeft and cutoutRight + 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) { + changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints() + } else { + changes += combinedShadeHeadersConstraintManager.centerCutoutConstraints( + view.isLayoutRtl, + (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2 + ) + } + } else { + changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints() + } + + view.updateAllConstraints(changes) + } + + private fun updateScrollY() { + if (!largeScreenActive && combinedHeaders) { + header.scrollY = qsScrollY + } + } + + private fun onShadeExpandedChanged() { + if (qsVisible) { + privacyIconsController.startListening() + } else { + privacyIconsController.stopListening() + } + updateVisibility() + updatePosition() + } + + private fun onHeaderStateChanged() { + if (largeScreenActive || combinedHeaders) { + privacyIconsController.onParentVisible() + } else { + privacyIconsController.onParentInvisible() + } + updateVisibility() + updateTransition() + } + + /** + * If not using [combinedHeaders] this should only be visible on large screen. Else, it should + * be visible any time the QQS/QS shade is open. + */ + private fun updateVisibility() { + val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) { + View.GONE + } else if (qsVisible) { + View.VISIBLE + } else { + View.INVISIBLE + } + if (header.visibility != visibility) { + header.visibility = visibility + visible = visibility == View.VISIBLE + } + } + + private fun updateTransition() { + if (!combinedHeaders) { + return + } + header as MotionLayout + if (largeScreenActive) { + header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID) + header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT).applyTo(header) + } else { + header.setTransition(HEADER_TRANSITION_ID) + header.transitionToStart() + updatePosition() + updateScrollY() + } + } + + private fun updatePosition() { + if (header is MotionLayout && !largeScreenActive && visible) { + Trace.instantForTrack( + TRACE_TAG_APP, + "LargeScreenHeaderController - updatePosition", + "position: $qsExpandedFraction" + ) + header.progress = qsExpandedFraction + } + } + + private fun updateListeners() { + qsCarrierGroupController.setListening(visible) + if (visible) { + updateSingleCarrier(qsCarrierGroupController.isSingleCarrier) + qsCarrierGroupController.setOnSingleCarrierChangedListener { updateSingleCarrier(it) } + statusBarIconController.addIconGroup(iconManager) + } else { + qsCarrierGroupController.setOnSingleCarrierChangedListener(null) + statusBarIconController.removeIconGroup(iconManager) + } + } + + private fun updateSingleCarrier(singleCarrier: Boolean) { + if (singleCarrier) { + iconContainer.removeIgnoredSlots(carrierIconSlots) + } else { + iconContainer.addIgnoredSlots(carrierIconSlots) + } + } + + private fun updateResources() { + roundedCorners = resources.getDimensionPixelSize(R.dimen.rounded_corner_content_padding) + val padding = resources.getDimensionPixelSize(R.dimen.qs_panel_padding) + header.setPadding(padding, header.paddingTop, padding, header.paddingBottom) + updateQQSPaddings() + } + + private fun updateQQSPaddings() { + if (header is MotionLayout) { + val clockPaddingStart = resources + .getDimensionPixelSize(R.dimen.status_bar_left_clock_starting_padding) + val clockPaddingEnd = resources + .getDimensionPixelSize(R.dimen.status_bar_left_clock_end_padding) + clock.setPaddingRelative( + clockPaddingStart, + clock.paddingTop, + clockPaddingEnd, + clock.paddingBottom + ) + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("visible: $visible") + pw.println("shadeExpanded: $qsVisible") + pw.println("shadeExpandedFraction: $shadeExpandedFraction") + pw.println("active: $largeScreenActive") + pw.println("qsExpandedFraction: $qsExpandedFraction") + pw.println("qsScrollY: $qsScrollY") + if (combinedHeaders) { + header as MotionLayout + 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 bab92ba492c9..61d19634af38 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; @@ -1038,6 +1037,7 @@ public final class NotificationPanelViewController extends PanelViewController { } mTapAgainViewController.init(); + mLargeScreenShadeHeaderController.init(); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); mNotificationPanelUnfoldAnimationController.ifPresent(controller -> controller.setup(mNotificationContainerParent)); @@ -1143,7 +1143,7 @@ public final class NotificationPanelViewController extends PanelViewController { SystemBarUtils.getQuickQsOffsetHeight(mView.getContext()); int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top); - mLargeScreenShadeHeaderController.setActive(mUseLargeScreenShadeHeader); + mLargeScreenShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader); mAmbientState.setStackTopMargin(topMargin); mNotificationsQSContainerController.updateResources(); @@ -2417,7 +2417,7 @@ public final class NotificationPanelViewController extends PanelViewController { : getExpandedFraction(); mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction); mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction); - mLargeScreenShadeHeaderController.setShadeExpanded(mQsVisible); + mLargeScreenShadeHeaderController.setQsVisible(mQsVisible); } private void onStackYChanged(boolean shouldAnimate) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt deleted file mode 100644 index 84c8700436cc..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone - -import android.app.StatusBarManager -import android.content.res.Configuration -import android.view.View -import android.widget.TextView -import androidx.constraintlayout.motion.widget.MotionLayout -import com.android.settingslib.Utils -import com.android.systemui.Dumpable -import com.android.systemui.FontSizeUtils -import com.android.systemui.R -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.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 -import com.android.systemui.statusbar.policy.ConfigurationController -import java.io.PrintWriter -import javax.inject.Inject -import javax.inject.Named - -@CentralSurfacesScope -class LargeScreenShadeHeaderController @Inject constructor( - @Named(LARGE_SCREEN_SHADE_HEADER) private val header: View, - private val statusBarIconController: StatusBarIconController, - private val privacyIconsController: HeaderPrivacyIconsController, - private val configurationController: ConfigurationController, - qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder, - featureFlags: FeatureFlags, - @Named(LARGE_SCREEN_BATTERY_CONTROLLER) batteryMeterViewController: BatteryMeterViewController, - dumpManager: DumpManager -) : Dumpable { - - companion object { - 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 - - private fun Int.stateToString() = when (this) { - QQS_HEADER_CONSTRAINT -> "QQS Header" - QS_HEADER_CONSTRAINT -> "QS Header" - LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header" - else -> "Unknown state" - } - } - - private val combinedHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS) - private val iconManager: StatusBarIconController.TintedIconManager - private val iconContainer: StatusIconContainer - private val carrierIconSlots: List<String> - private val qsCarrierGroupController: QSCarrierGroupController - private val clock: TextView = header.findViewById(R.id.clock) - private val date: TextView = header.findViewById(R.id.date) - private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group) - - private var qsDisabled = false - - private var visible = false - set(value) { - if (field == value) { - return - } - field = value - updateListeners() - } - - var shadeExpanded = false - set(value) { - if (field == value) { - return - } - field = value - onShadeExpandedChanged() - } - - var active = false - set(value) { - if (field == value) { - return - } - field = value - onHeaderStateChanged() - } - - var shadeExpandedFraction = -1f - set(value) { - if (visible && field != value) { - header.alpha = ShadeInterpolation.getContentAlpha(value) - field = value - } - } - - var qsExpandedFraction = -1f - set(value) { - if (visible && field != value) { - field = value - updateVisibility() - updatePosition() - } - } - - var qsScrollY = 0 - set(value) { - if (field != value) { - field = value - updateScrollY() - } - } - - private val chipVisibilityListener: ChipVisibilityListener = object : ChipVisibilityListener { - override fun onChipVisibilityRefreshed(visible: Boolean) { - if (header is MotionLayout) { - val state = header.getConstraintSet(QQS_HEADER_CONSTRAINT).apply { - setAlpha(R.id.statusIcons, if (visible) 0f else 1f) - setAlpha(R.id.batteryRemainingIcon, if (visible) 0f else 1f) - } - header.updateState(QQS_HEADER_CONSTRAINT, state) - } - } - } - - init { - if (header is MotionLayout) { - val context = header.context - val resources = header.resources - header.getConstraintSet(QQS_HEADER_CONSTRAINT) - .load(context, resources.getXml(R.xml.qqs_header)) - header.getConstraintSet(QS_HEADER_CONSTRAINT) - .load(context, resources.getXml(R.xml.qs_header)) - header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT) - .load(context, resources.getXml(R.xml.large_screen_shade_header)) - privacyIconsController.chipVisibilityListener = chipVisibilityListener - } - - bindConfigurationListener() - - batteryMeterViewController.init() - val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon) - - // battery settings same as in QS icons - batteryMeterViewController.ignoreTunerUpdates() - batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) - - iconContainer = header.findViewById(R.id.statusIcons) - iconManager = StatusBarIconController.TintedIconManager(iconContainer, featureFlags) - iconManager.setTint(Utils.getColorAttrDefaultColor(header.context, - android.R.attr.textColorPrimary)) - - carrierIconSlots = if (featureFlags.isEnabled(Flags.COMBINED_STATUS_BAR_SIGNAL_ICONS)) { - listOf( - header.context.getString(com.android.internal.R.string.status_bar_no_calling), - header.context.getString(com.android.internal.R.string.status_bar_call_strength) - ) - } else { - listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile)) - } - qsCarrierGroupController = qsCarrierGroupControllerBuilder - .setQSCarrierGroup(header.findViewById(R.id.carrier_group)) - .build() - - dumpManager.registerDumpable(this) - - updateVisibility() - updateConstraints() - } - - fun disable(state1: Int, state2: Int, animate: Boolean) { - val disabled = state2 and StatusBarManager.DISABLE2_QUICK_SETTINGS != 0 - if (disabled == qsDisabled) return - qsDisabled = disabled - updateVisibility() - } - - private fun updateScrollY() { - if (!active && combinedHeaders) { - header.scrollY = qsScrollY - } - } - - private fun bindConfigurationListener() { - val listener = object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - val left = header.resources.getDimensionPixelSize( - R.dimen.large_screen_shade_header_left_padding) - header.setPadding( - left, header.paddingTop, header.paddingRight, header.paddingBottom) - } - override fun onDensityOrFontScaleChanged() { - val qsStatusStyle = R.style.TextAppearance_QS_Status - FontSizeUtils.updateFontSizeFromStyle(clock, qsStatusStyle) - FontSizeUtils.updateFontSizeFromStyle(date, qsStatusStyle) - qsCarrierGroup.updateTextAppearance(qsStatusStyle) - } - } - configurationController.addCallback(listener) - } - - private fun onShadeExpandedChanged() { - if (shadeExpanded) { - privacyIconsController.startListening() - } else { - privacyIconsController.stopListening() - } - updateVisibility() - updatePosition() - } - - private fun onHeaderStateChanged() { - if (active || combinedHeaders) { - privacyIconsController.onParentVisible() - } else { - privacyIconsController.onParentInvisible() - } - updateVisibility() - updateConstraints() - } - - private fun updateVisibility() { - val visibility = if (!active && !combinedHeaders || qsDisabled) { - View.GONE - } else if (shadeExpanded) { - View.VISIBLE - } else { - View.INVISIBLE - } - if (header.visibility != visibility) { - header.visibility = visibility - visible = visibility == View.VISIBLE - } - } - - private fun updateConstraints() { - if (!combinedHeaders) { - return - } - header as MotionLayout - if (active) { - header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID) - } else { - header.setTransition(HEADER_TRANSITION_ID) - header.transitionToStart() - updatePosition() - updateScrollY() - } - } - - private fun updatePosition() { - if (header is MotionLayout && !active && visible) { - header.setProgress(qsExpandedFraction) - } - } - - private fun updateListeners() { - qsCarrierGroupController.setListening(visible) - if (visible) { - updateSingleCarrier(qsCarrierGroupController.isSingleCarrier) - qsCarrierGroupController.setOnSingleCarrierChangedListener { updateSingleCarrier(it) } - statusBarIconController.addIconGroup(iconManager) - } else { - qsCarrierGroupController.setOnSingleCarrierChangedListener(null) - statusBarIconController.removeIconGroup(iconManager) - } - } - - private fun updateSingleCarrier(singleCarrier: Boolean) { - if (singleCarrier) { - iconContainer.removeIgnoredSlots(carrierIconSlots) - } else { - iconContainer.addIgnoredSlots(carrierIconSlots) - } - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println("visible: $visible") - pw.println("shadeExpanded: $shadeExpanded") - pw.println("shadeExpandedFraction: $shadeExpandedFraction") - pw.println("active: $active") - pw.println("qsExpandedFraction: $qsExpandedFraction") - pw.println("qsScrollY: $qsScrollY") - if (combinedHeaders) { - header as MotionLayout - pw.println("currentState: ${header.currentState.stateToString()}") - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index f5462bc0fba5..c850d4f9c56b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -123,7 +123,7 @@ class StatusBarContentInsetsProvider @Inject constructor( val point = Point() context.display.getRealSize(point) - return topBounds.left <= 0 || topBounds.right >= point.y + return topBounds.left <= 0 || topBounds.right >= point.x } /** 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 84b279760f36..64b04e93e69c 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 88e985f9eeaf..b60739164a19 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 @@ -35,6 +35,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; @@ -179,6 +181,14 @@ public abstract class StatusBarViewModule { /** */ @Provides @CentralSurfacesComponent.CentralSurfacesScope + public static CombinedShadeHeadersConstraintManager + provideCombinedShadeHeadersConstraintManager() { + return CombinedShadeHeadersConstraintManagerImpl.INSTANCE; + } + + /** */ + @Provides + @CentralSurfacesComponent.CentralSurfacesScope public static OngoingPrivacyChip getSplitShadeOngoingPrivacyChip( @Named(LARGE_SCREEN_SHADE_HEADER) View header) { return header.findViewById(R.id.privacy_chip); diff --git a/packages/SystemUI/src/com/android/systemui/util/NoRemeasureMotionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/NoRemeasureMotionLayout.kt new file mode 100644 index 000000000000..3095d80d1630 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/NoRemeasureMotionLayout.kt @@ -0,0 +1,57 @@ +/* + * 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.util + +import android.content.Context +import android.util.AttributeSet +import android.view.Choreographer +import androidx.constraintlayout.motion.widget.MotionLayout + +/** + * [MotionLayout] that avoids remeasuring with the same inputs in the same frame. + * + * This is important when this view is the child of a view that performs more than one measure pass + * (e.g. [LinearLayout] or [ConstraintLayout]). In those cases, if this view is measured with the + * same inputs in the same frame, we use the last result. + */ +class NoRemeasureMotionLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet?, + defStyle: Int = 0 +) : MotionLayout(context, attrs, defStyle) { + + private var lastWidthSpec: Int? = null + private var lastHeightSpec: Int? = null + private var lastFrame: Long? = null + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if ( + lastWidthSpec == widthMeasureSpec && + lastHeightSpec == heightMeasureSpec && + Choreographer.getMainThreadInstance()?.frameTime == lastFrame + ) { + setMeasuredDimension(measuredWidth, measuredHeight) + return + } + traceSection("NoRemeasureMotionLayout - measure") { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + lastWidthSpec = widthMeasureSpec + lastHeightSpec = heightMeasureSpec + lastFrame = Choreographer.getMainThreadInstance()?.frameTime + } + } +} 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 000000000000..0ce9056dc1d1 --- /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 000000000000..9b2e085560a1 --- /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 000000000000..0abb08427478 --- /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 000000000000..ed1a13b36d6c --- /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<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) + } +} 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 index 80664013f95d..02b26dbbc32d 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,10 +1,8 @@ -package com.android.systemui.statusbar.phone +package com.android.systemui.shade import android.app.StatusBarManager import android.content.Context -import android.content.res.TypedArray import android.testing.AndroidTestingRunner -import android.util.TypedValue.COMPLEX_UNIT_PX import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest @@ -19,19 +17,24 @@ 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 import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(AndroidTestingRunner::class) @@ -49,10 +52,14 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { @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 private lateinit var typedArray: TypedArray @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE @@ -65,7 +72,6 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { fun setup() { whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock) whenever(clock.context).thenReturn(mockedContext) - whenever(mockedContext.obtainStyledAttributes(anyInt(), any())).thenReturn(typedArray) whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date) whenever(date.context).thenReturn(mockedContext) whenever<QSCarrierGroup>(view.findViewById(R.id.carrier_group)).thenReturn(carrierGroup) @@ -73,6 +79,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { .thenReturn(batteryMeterView) whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons) whenever(view.context).thenReturn(context) + whenever(view.resources).thenReturn(context.resources) whenever(statusIcons.context).thenReturn(context) whenever(qsCarrierGroupControllerBuilder.setQSCarrierGroup(any())) .thenReturn(qsCarrierGroupControllerBuilder) @@ -82,27 +89,39 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + whenever(variableDateViewControllerFactory.create(any())) + .thenReturn(variableDateViewController) whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false) mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController( view, statusBarIconController, privacyIconsController, + insetsProvider, configurationController, - qsCarrierGroupControllerBuilder, - featureFlags, + variableDateViewControllerFactory, batteryMeterViewController, - dumpManager + dumpManager, + featureFlags, + qsCarrierGroupControllerBuilder, + combinedShadeHeadersConstraintManager ) + whenever(view.isAttachedToWindow).thenReturn(true) + mLargeScreenShadeHeaderController.init() carrierIconSlots = listOf( context.getString(com.android.internal.R.string.status_bar_mobile)) } + @After + fun verifyEveryTest() { + verifyZeroInteractions(combinedShadeHeadersConstraintManager) + } + @Test fun setVisible_onlyWhenActive() { makeShadeVisible() assertThat(viewVisibility).isEqualTo(View.VISIBLE) - mLargeScreenShadeHeaderController.active = false + mLargeScreenShadeHeaderController.largeScreenActive = false assertThat(viewVisibility).isEqualTo(View.GONE) } @@ -156,41 +175,16 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } private fun makeShadeVisible() { - mLargeScreenShadeHeaderController.active = true - mLargeScreenShadeHeaderController.shadeExpanded = true + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true } @Test - fun updateConfig_changesFontSize() { - val updatedTextPixelSize = 32 - setReturnTextSize(updatedTextPixelSize) - + fun updateConfig_changesFontStyle() { configurationController.notifyDensityOrFontScaleChanged() - verify(clock).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize.toFloat()) - verify(date).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize.toFloat()) - verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status) - } - - @Test - fun updateConfig_changesFontSizeMultipleTimes() { - val updatedTextPixelSize1 = 32 - setReturnTextSize(updatedTextPixelSize1) - configurationController.notifyDensityOrFontScaleChanged() - verify(clock).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize1.toFloat()) - verify(date).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize1.toFloat()) - verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status) - clearInvocations(carrierGroup) - - val updatedTextPixelSize2 = 42 - setReturnTextSize(updatedTextPixelSize2) - configurationController.notifyDensityOrFontScaleChanged() - verify(clock).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize2.toFloat()) - verify(date).setTextSize(COMPLEX_UNIT_PX, updatedTextPixelSize2.toFloat()) - verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status) - } - - private fun setReturnTextSize(resultTextSize: Int) { - whenever(typedArray.getDimensionPixelSize(anyInt(), anyInt())).thenReturn(resultTextSize) + verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status) + verify(date).setTextAppearance(R.style.TextAppearance_QS_Status) + verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) } } 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 5abcff3c56f6..30c1b2594ee8 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; @@ -1182,11 +1181,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mStatusBarStateController.setState(SHADE); when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(true); mNotificationPanelViewController.updateResources(); - verify(mLargeScreenShadeHeaderController).setActive(true); + verify(mLargeScreenShadeHeaderController).setLargeScreenActive(true); when(mResources.getBoolean(R.bool.config_use_large_screen_shade_header)).thenReturn(false); mNotificationPanelViewController.updateResources(); - verify(mLargeScreenShadeHeaderController).setActive(false); + verify(mLargeScreenShadeHeaderController).setLargeScreenActive(false); } @Test |