Merge changes I5d18421d,I41bf6731 into main
* changes:
Track StatusBarState
Add NotificationScrimClip to QSFragmentCompose
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 5999265..768fbca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -31,6 +31,8 @@
import com.android.systemui.qs.fgsManagerController
import com.android.systemui.res.R
import com.android.systemui.shade.largeScreenHeaderHelper
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -140,6 +142,42 @@
}
}
+ @Test
+ fun statusBarState_followsController() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val statusBarState by collectLastValue(underTest.statusBarState)
+ runCurrent()
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
+
+ sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED)
+ }
+ }
+
+ @Test
+ fun statusBarState_changesEarlyIfUpcomingStateIsKeyguard() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val statusBarState by collectLastValue(underTest.statusBarState)
+
+ sysuiStatusBarStateController.setState(StatusBarState.SHADE)
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE_LOCKED)
+ assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
+
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.KEYGUARD)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+
+ sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+ }
+ }
+
private inline fun TestScope.testWithinLifecycle(
crossinline block: suspend TestScope.() -> TestResult
): TestResult {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 9f9c8e9..3613c11 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -37,6 +37,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
@@ -50,6 +52,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
+import com.android.compose.modifiers.thenIf
import com.android.compose.theme.PlatformTheme
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -59,6 +62,7 @@
import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.composefragment.ui.notificationScrimClip
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
import com.android.systemui.qs.flags.QSComposeFragment
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -100,6 +104,17 @@
private val qqsPositionOnRoot = Rect()
private val composeViewPositionOnScreen = Rect()
+ // Inside object for namespacing
+ private val notificationScrimClippingParams =
+ object {
+ var isEnabled by mutableStateOf(false)
+ var leftInset by mutableStateOf(0)
+ var rightInset by mutableStateOf(0)
+ var top by mutableStateOf(0)
+ var bottom by mutableStateOf(0)
+ var radius by mutableStateOf(0)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -126,7 +141,18 @@
AnimatedVisibility(
visible = visible,
- modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ modifier =
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
+ notificationScrimClippingParams.isEnabled
+ ) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ }
) {
AnimatedContent(targetState = qsState) {
when (it) {
@@ -280,7 +306,16 @@
cornerRadius: Int,
visible: Boolean,
fullWidth: Boolean
- ) {}
+ ) {
+ notificationScrimClippingParams.isEnabled = visible
+ notificationScrimClippingParams.top = top
+ notificationScrimClippingParams.bottom = bottom
+ // Full width means that QS will show in the entire width allocated to it (for example
+ // phone) vs. showing in a narrower column (for example, tablet portrait).
+ notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset
+ notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset
+ notificationScrimClippingParams.radius = cornerRadius
+ }
override fun isFullyCollapsed(): Boolean {
return viewModel.qsExpansionValue <= 0f
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
new file mode 100644
index 0000000..93c6445
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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.qs.composefragment.ui
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.asAndroidPath
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
+ * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
+ * from the QS container.
+ */
+fun Modifier.notificationScrimClip(
+ leftInset: Int,
+ top: Int,
+ rightInset: Int,
+ bottom: Int,
+ radius: Int
+): Modifier {
+ return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius)
+}
+
+private class NotificationScrimClipNode(
+ var leftInset: Float,
+ var top: Float,
+ var rightInset: Float,
+ var bottom: Float,
+ var radius: Float,
+) : DrawModifierNode, Modifier.Node() {
+ private val path = Path()
+
+ var invalidated = true
+
+ override fun ContentDrawScope.draw() {
+ if (invalidated) {
+ path.rewind()
+ path
+ .asAndroidPath()
+ .addRoundRect(
+ -leftInset,
+ top,
+ size.width + rightInset,
+ bottom,
+ radius,
+ radius,
+ android.graphics.Path.Direction.CW
+ )
+ invalidated = false
+ }
+ clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
+ }
+}
+
+private data class NotificationScrimClipElement(
+ val leftInset: Int,
+ val top: Int,
+ val rightInset: Int,
+ val bottom: Int,
+ val radius: Int,
+) : ModifierNodeElement<NotificationScrimClipNode>() {
+ override fun create(): NotificationScrimClipNode {
+ return NotificationScrimClipNode(
+ leftInset.toFloat(),
+ top.toFloat(),
+ rightInset.toFloat(),
+ bottom.toFloat(),
+ radius.toFloat(),
+ )
+ }
+
+ override fun update(node: NotificationScrimClipNode) {
+ val changed =
+ node.leftInset != leftInset.toFloat() ||
+ node.top != top.toFloat() ||
+ node.rightInset != rightInset.toFloat() ||
+ node.bottom != bottom.toFloat() ||
+ node.radius != radius.toFloat()
+ if (changed) {
+ node.leftInset = leftInset.toFloat()
+ node.top = top.toFloat()
+ node.rightInset = rightInset.toFloat()
+ node.bottom = bottom.toFloat()
+ node.radius = radius.toFloat()
+ node.invalidated = true
+ }
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "notificationScrimClip"
+ properties["leftInset"] = leftInset
+ properties["top"] = top
+ properties["rightInset"] = rightInset
+ properties["bottom"] = bottom
+ properties["radius"] = radius
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 7d52216..4b1312c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -19,21 +19,26 @@
import android.content.res.Resources
import android.graphics.Rect
import androidx.annotation.FloatRange
+import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
+import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -140,7 +145,34 @@
private val _keyguardAndExpanded = MutableStateFlow(false)
- private val _statusBarState = MutableStateFlow(-1)
+ /**
+ * Tracks the current [StatusBarState]. It will switch early if the upcoming state is
+ * [StatusBarState.KEYGUARD]
+ */
+ @get:VisibleForTesting
+ val statusBarState =
+ conflatedCallbackFlow {
+ val callback =
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
+ trySend(newState)
+ }
+
+ override fun onUpcomingStateChanged(upcomingState: Int) {
+ if (upcomingState == StatusBarState.KEYGUARD) {
+ trySend(upcomingState)
+ }
+ }
+ }
+ sysuiStatusBarStateController.addCallback(callback)
+
+ awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
+ }
+ .stateIn(
+ lifecycleScope,
+ SharingStarted.WhileSubscribed(),
+ sysuiStatusBarStateController.state,
+ )
private val _viewHeight = MutableStateFlow(0)