Merge changes from topic "215584502" into tm-qpr-dev

* changes:
  Add unit tests for combined QS headers
  New headers
This commit is contained in:
Fabian Kozynski
2022-07-21 19:27:43 +00:00
committed by Android (Google) Code Review
21 changed files with 2303 additions and 385 deletions

View File

@@ -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>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/space"
/>
</com.android.systemui.util.NoRemeasureMotionLayout>

View File

@@ -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>

View File

@@ -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="50"
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.shade
import androidx.constraintlayout.widget.ConstraintSet
typealias ConstraintChange = ConstraintSet.() -> Unit
operator fun ConstraintChange?.plus(other: ConstraintChange?): ConstraintChange? {
// Prevent wrapping
if (this == null) return other
if (other == null) return this
else return {
this@plus()
other()
}
}
/**
* Contains all changes that need to be performed to the different [ConstraintSet] in
* [LargeScreenShadeHeaderController].
*/
data class ConstraintsChanges(
val qqsConstraintsChanges: ConstraintChange? = null,
val qsConstraintsChanges: ConstraintChange? = null,
val largeScreenConstraintsChanges: ConstraintChange? = null
) {
operator fun plus(other: ConstraintsChanges) = ConstraintsChanges(
qqsConstraintsChanges + other.qqsConstraintsChanges,
qsConstraintsChanges + other.qsConstraintsChanges,
largeScreenConstraintsChanges + other.largeScreenConstraintsChanges
)
}
/**
* Determines [ConstraintChanges] for [LargeScreenShadeHeaderController] based on configurations.
*
* Given that the number of different scenarios is not that large, having specific methods instead
* of a full map between state and [ConstraintSet] was preferred.
*/
interface CombinedShadeHeadersConstraintManager {
/**
* Changes for when the visibility of the privacy chip changes
*/
fun privacyChipVisibilityConstraints(visible: Boolean): ConstraintsChanges
/**
* Changes for situations with no top center cutout (there may be a corner cutout)
*/
fun emptyCutoutConstraints(): ConstraintsChanges
/**
* Changes to incorporate side insets due to rounded corners/corner cutouts
*/
fun edgesGuidelinesConstraints(
cutoutStart: Int,
paddingStart: Int,
cutoutEnd: Int,
paddingEnd: Int
): ConstraintsChanges
/**
* Changes for situations with top center cutout (in this case, there are no corner cutouts).
*/
fun centerCutoutConstraints(rtl: Boolean, offsetFromEdge: Int): ConstraintsChanges
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.shade
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
/**
* Standard implementation of [CombinedShadeHeadersConstraintManager].
*/
@CentralSurfacesComponent.CentralSurfacesScope
object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstraintManager {
override fun privacyChipVisibilityConstraints(visible: Boolean): ConstraintsChanges {
val constraintAlpha = if (visible) 0f else 1f
return ConstraintsChanges(
qqsConstraintsChanges = {
setAlpha(R.id.statusIcons, constraintAlpha)
setAlpha(R.id.batteryRemainingIcon, constraintAlpha)
}
)
}
override fun emptyCutoutConstraints(): ConstraintsChanges {
return ConstraintsChanges(
qqsConstraintsChanges = {
connect(R.id.date, ConstraintSet.END, R.id.barrier, ConstraintSet.START)
createBarrier(
R.id.barrier,
ConstraintSet.START,
0,
R.id.statusIcons,
R.id.privacy_container
)
connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END)
connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END)
constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT)
}
)
}
override fun edgesGuidelinesConstraints(
cutoutStart: Int,
paddingStart: Int,
cutoutEnd: Int,
paddingEnd: Int
): ConstraintsChanges {
val change: ConstraintChange = {
setGuidelineBegin(R.id.begin_guide, Math.max(cutoutStart - paddingStart, 0))
setGuidelineEnd(R.id.end_guide, Math.max(cutoutEnd - paddingEnd, 0))
}
return ConstraintsChanges(
qqsConstraintsChanges = change,
qsConstraintsChanges = change
)
}
override fun centerCutoutConstraints(rtl: Boolean, offsetFromEdge: Int): ConstraintsChanges {
val centerStart = if (!rtl) R.id.center_left else R.id.center_right
val centerEnd = if (!rtl) R.id.center_right else R.id.center_left
// Use guidelines to block the center cutout area.
return ConstraintsChanges(
qqsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
setGuidelineEnd(centerEnd, offsetFromEdge)
connect(R.id.date, ConstraintSet.END, centerStart, ConstraintSet.START)
connect(
R.id.statusIcons,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
connect(
R.id.privacy_container,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
constrainWidth(R.id.statusIcons, 0)
},
qsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
setGuidelineEnd(centerEnd, offsetFromEdge)
connect(
R.id.privacy_container,
ConstraintSet.START,
centerEnd,
ConstraintSet.END
)
}
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -189,7 +189,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -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) {

View File

@@ -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()}")
}
}
}

View File

@@ -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
}
/**

View File

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

View File

@@ -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;
@@ -176,6 +178,14 @@ public abstract class StatusBarViewModule {
return v;
}
/** */
@Provides
@CentralSurfacesComponent.CentralSurfacesScope
public static CombinedShadeHeadersConstraintManager
provideCombinedShadeHeadersConstraintManager() {
return CombinedShadeHeadersConstraintManagerImpl.INSTANCE;
}
/** */
@Provides
@CentralSurfacesComponent.CentralSurfacesScope

View File

@@ -0,0 +1,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
}
}
}

View File

@@ -0,0 +1,328 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class CombinedShadeHeaderConstraintsTest : SysuiTestCase() {
private lateinit var qqsConstraint: ConstraintSet
private lateinit var qsConstraint: ConstraintSet
private lateinit var largeScreenConstraint: ConstraintSet
@Before
fun setUp() {
qqsConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.qqs_header))
}
qsConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.qs_header_new))
}
largeScreenConstraint = ConstraintSet().apply {
load(context, context.resources.getXml(R.xml.large_screen_shade_header))
}
}
@Test
fun testEdgeElementsAlignedWithGuide_qqs() {
with(qqsConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(R.id.begin_guide)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
.isEqualTo(1f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias)
.isEqualTo(1f)
}
}
@Test
fun testClockScale() {
with(qqsConstraint.getConstraint(R.id.clock)) {
assertThat(transform.scaleX).isEqualTo(1f)
assertThat(transform.scaleY).isEqualTo(1f)
}
with(qsConstraint.getConstraint(R.id.clock)) {
assertThat(transform.scaleX).isGreaterThan(1f)
assertThat(transform.scaleY).isGreaterThan(1f)
}
}
@Test
fun testEdgeElementsAlignedWithEdgeOrGuide_qs() {
with(qsConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
.isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
.isEqualTo(1f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
.isEqualTo(R.id.end_guide)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
}
}
@Test
fun testEdgeElementsAlignedWithEdge_largeScreen() {
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd).isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
}
}
@Test
fun testCarrierAlpha() {
assertThat(qqsConstraint.getConstraint(R.id.carrier_group).propertySet.alpha).isEqualTo(0f)
assertThat(qsConstraint.getConstraint(R.id.carrier_group).propertySet.alpha).isEqualTo(1f)
assertThat(largeScreenConstraint.getConstraint(R.id.carrier_group).propertySet.alpha)
.isEqualTo(1f)
}
@Test
fun testPrivacyChipVisibilityConstraints_notVisible() {
val changes = CombinedShadeHeadersConstraintManagerImpl
.privacyChipVisibilityConstraints(false)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
}
@Test
fun testPrivacyChipVisibilityConstraints_visible() {
val changes = CombinedShadeHeadersConstraintManagerImpl
.privacyChipVisibilityConstraints(true)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(0f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(0f)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
with(largeScreenConstraint) {
assertThat(getConstraint(R.id.statusIcons).propertySet.alpha).isEqualTo(1f)
assertThat(getConstraint(R.id.batteryRemainingIcon).propertySet.alpha).isEqualTo(1f)
}
}
@Test
fun testEmptyCutoutConstraints() {
val changes = CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()
changes()
// QS and Large Screen don't change with cutouts.
assertThat(changes.qsConstraintsChanges).isNull()
assertThat(changes.largeScreenConstraintsChanges).isNull()
with(qqsConstraint) {
// In this case, the date is constrained on the end by a Barrier determined by either
// privacy or statusIcons
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.barrier)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd).isEqualTo(R.id.date)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd).isEqualTo(R.id.date)
assertThat(getConstraint(R.id.barrier).layout.mReferenceIds).asList().containsExactly(
R.id.statusIcons,
R.id.privacy_container
)
assertThat(getConstraint(R.id.barrier).layout.mBarrierDirection).isEqualTo(START)
}
}
@Test
fun testGuidesAreSetInCorrectPosition_largeCutoutSmallerPadding() {
val cutoutStart = 100
val padding = 10
val cutoutEnd = 30
val changes = CombinedShadeHeadersConstraintManagerImpl.edgesGuidelinesConstraints(
cutoutStart,
padding,
cutoutEnd,
padding
)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
.isEqualTo(cutoutStart - padding)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
.isEqualTo(cutoutEnd - padding)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
.isEqualTo(cutoutStart - padding)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
.isEqualTo(cutoutEnd - padding)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testGuidesAreSetInCorrectPosition_smallCutoutLargerPadding() {
val cutoutStart = 5
val padding = 10
val cutoutEnd = 10
val changes = CombinedShadeHeadersConstraintManagerImpl.edgesGuidelinesConstraints(
cutoutStart,
padding,
cutoutEnd,
padding
)
changes()
with(qqsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testCenterCutoutConstraints_ltr() {
val offsetFromEdge = 400
val rtl = false
val changes = CombinedShadeHeadersConstraintManagerImpl
.centerCutoutConstraints(rtl, offsetFromEdge)
changes()
// In LTR, center_left is towards the start and center_right is towards the end
with(qqsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_right)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_right)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
@Test
fun testCenterCutoutConstraints_rtl() {
val offsetFromEdge = 400
val rtl = true
val changes = CombinedShadeHeadersConstraintManagerImpl
.centerCutoutConstraints(rtl, offsetFromEdge)
changes()
// In RTL, center_left is towards the end and center_right is towards the start
with(qqsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_left)
}
with(qsConstraint) {
assertThat(getConstraint(R.id.center_left).layout.guideEnd).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.center_right).layout.guideBegin).isEqualTo(offsetFromEdge)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.date).layout.endToStart).isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_left)
assertThat(getConstraint(R.id.statusIcons).layout.startToEnd)
.isNotEqualTo(R.id.center_right)
assertThat(getConstraint(R.id.privacy_container).layout.startToEnd)
.isEqualTo(R.id.center_left)
}
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
private operator fun ConstraintsChanges.invoke() {
qqsConstraintsChanges?.invoke(qqsConstraint)
qsConstraintsChanges?.invoke(qsConstraint)
largeScreenConstraintsChanges?.invoke(largeScreenConstraint)
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ConstraintChangeTest : SysuiTestCase() {
@Test
fun testSumNonNull() {
val mock1: ConstraintChange = mock()
val mock2: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = mock1 + mock2
sum?.invoke(constraintSet)
val inOrder = inOrder(mock1, mock2)
inOrder.verify(mock1).invoke(constraintSet)
inOrder.verify(mock2).invoke(constraintSet)
}
@Test
fun testSumThisNull() {
val mock: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = (null as? ConstraintChange?) + mock
sum?.invoke(constraintSet)
verify(mock).invoke(constraintSet)
}
@Test
fun testSumThisNull_notWrapped() {
val change: ConstraintChange = {}
val sum = (null as? ConstraintChange?) + change
assertThat(sum).isSameInstanceAs(change)
}
@Test
fun testSumOtherNull() {
val mock: ConstraintChange = mock()
val constraintSet = ConstraintSet()
val sum = mock + (null as? ConstraintChange?)
sum?.invoke(constraintSet)
verify(mock).invoke(constraintSet)
}
@Test
fun testSumOtherNull_notWrapped() {
val change: ConstraintChange = {}
val sum = change + (null as? ConstraintChange?)
assertThat(sum).isSameInstanceAs(change)
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.shade
import android.testing.AndroidTestingRunner
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.inOrder
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ConstraintChangesTest : SysuiTestCase() {
@Test
fun testSumWithoutNulls() {
val mockQQS1: ConstraintChange = mock()
val mockQS1: ConstraintChange = mock()
val mockLS1: ConstraintChange = mock()
val mockQQS2: ConstraintChange = mock()
val mockQS2: ConstraintChange = mock()
val mockLS2: ConstraintChange = mock()
val changes1 = ConstraintsChanges(mockQQS1, mockQS1, mockLS1)
val changes2 = ConstraintsChanges(mockQQS2, mockQS2, mockLS2)
val sum = changes1 + changes2
val constraintSet = ConstraintSet()
sum.qqsConstraintsChanges?.invoke(constraintSet)
sum.qsConstraintsChanges?.invoke(constraintSet)
sum.largeScreenConstraintsChanges?.invoke(constraintSet)
val inOrder = inOrder(mockQQS1, mockQS1, mockLS1, mockQQS2, mockQS2, mockLS2)
inOrder.verify(mockQQS1).invoke(constraintSet)
inOrder.verify(mockQQS2).invoke(constraintSet)
inOrder.verify(mockQS1).invoke(constraintSet)
inOrder.verify(mockQS2).invoke(constraintSet)
inOrder.verify(mockLS1).invoke(constraintSet)
inOrder.verify(mockLS2).invoke(constraintSet)
}
@Test
fun testSumWithSomeNulls() {
val mockQQS: ConstraintChange = mock()
val mockQS: ConstraintChange = mock()
val changes1 = ConstraintsChanges(mockQQS, null, null)
val changes2 = ConstraintsChanges(null, mockQS, null)
val sum = changes1 + changes2
assertThat(sum.qqsConstraintsChanges).isSameInstanceAs(mockQQS)
assertThat(sum.qsConstraintsChanges).isSameInstanceAs(mockQS)
assertThat(sum.largeScreenConstraintsChanges).isNull()
}
}

View File

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

View File

@@ -1,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)
}
}

View File

@@ -143,7 +143,6 @@ import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
import com.android.systemui.statusbar.phone.LargeScreenShadeHeaderController;
import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.PanelViewController;
@@ -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