diff options
| author | 2024-08-23 13:23:45 +0000 | |
|---|---|---|
| committer | 2024-08-23 13:23:45 +0000 | |
| commit | 6d8b1bb72a1e67f6ca6d7bd7d6c997711dbfc4b5 (patch) | |
| tree | 8fab735106f93b91edb7e611ea576c29220c30b7 | |
| parent | f45de596226927de819b180a005432413c896379 (diff) | |
| parent | 6758405d8d172ae400eabd3bc4ae68f7fea2d176 (diff) | |
Merge changes I5d18421d,I41bf6731 into main
* changes:
Track StatusBarState
Add NotificationScrimClip to QSFragmentCompose
4 files changed, 225 insertions, 3 deletions
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 59992650cfc7..768fbca90b56 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.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 @@ -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 { 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 9f9c8e910f20..3613c11012dd 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.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 @@ -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 @@ -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 @@ -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) @@ -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) { @@ -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 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 000000000000..93c6445b78ef --- /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 7d52216a4d2e..4b1312c71ff5 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 @@ 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 @@ -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) |