summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/combined_qs_header.xml60
-rw-r--r--packages/SystemUI/res/values/dimens.xml4
-rw-r--r--packages/SystemUI/res/xml/combined_qs_header_scene.xml93
-rw-r--r--packages/SystemUI/res/xml/large_screen_shade_header.xml40
-rw-r--r--packages/SystemUI/res/xml/qqs_header.xml47
-rw-r--r--packages/SystemUI/res/xml/qs_header_new.xml124
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt79
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt109
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt510
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt311
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/NoRemeasureMotionLayout.kt57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt328
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangeTest.kt85
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/ConstraintChangesTest.kt75
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt659
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderControllerTest.kt)80
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java5
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