Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +38 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,8 @@ import com.android.systemui.kosmos.testScope 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 Loading Loading @@ -140,6 +142,42 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } @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 { Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +37 −2 Original line number Diff line number Diff line Loading @@ -37,6 +37,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding 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 Loading @@ -50,6 +52,7 @@ import androidx.lifecycle.lifecycleScope 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 Loading @@ -59,6 +62,7 @@ import com.android.systemui.media.dagger.MediaModule.QS_PANEL 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 Loading Loading @@ -100,6 +104,17 @@ constructor( 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) Loading @@ -126,7 +141,18 @@ constructor( 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) { Loading Loading @@ -280,7 +306,16 @@ constructor( 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 Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt 0 → 100644 +117 −0 Original line number Diff line number Diff line /* * 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 } } packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +33 −1 Original line number Diff line number Diff line Loading @@ -19,21 +19,26 @@ package com.android.systemui.qs.composefragment.viewmodel 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 Loading Loading @@ -140,7 +145,34 @@ constructor( 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) Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +38 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,8 @@ import com.android.systemui.kosmos.testScope 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 Loading Loading @@ -140,6 +142,42 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } @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 { Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +37 −2 Original line number Diff line number Diff line Loading @@ -37,6 +37,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding 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 Loading @@ -50,6 +52,7 @@ import androidx.lifecycle.lifecycleScope 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 Loading @@ -59,6 +62,7 @@ import com.android.systemui.media.dagger.MediaModule.QS_PANEL 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 Loading Loading @@ -100,6 +104,17 @@ constructor( 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) Loading @@ -126,7 +141,18 @@ constructor( 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) { Loading Loading @@ -280,7 +306,16 @@ constructor( 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 Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt 0 → 100644 +117 −0 Original line number Diff line number Diff line /* * 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 } }
packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +33 −1 Original line number Diff line number Diff line Loading @@ -19,21 +19,26 @@ package com.android.systemui.qs.composefragment.viewmodel 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 Loading Loading @@ -140,7 +145,34 @@ constructor( 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) Loading