Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelTest.kt 0 → 100644 +177 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.panels.ui.viewmodel.toolbar import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.fake import com.android.systemui.qs.footer.domain.interactor.FakeFooterActionInteractor import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.qs.footerActionsInteractor import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class ToolbarViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().apply { footerActionsInteractor = FakeFooterActionInteractor() } private val Kosmos.underTest by Kosmos.Fixture { toolbarViewModelFactory.create() } @Before fun setUp() { kosmos.underTest.activateIn(kosmos.testScope) } @Test fun start_noSecurityInfo_collapsed() = with(kosmos) { runTest { assertThat(underTest.securityInfoViewModel).isNull() assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun nullConfig_noSecurityInfo_collapsed() = with(kosmos) { runTest { setSecurityConfig(null) assertThat(underTest.securityInfoViewModel).isNull() assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun config_notCollapsed() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) with(underTest.securityInfoViewModel!!) { assertThat(icon).isEqualTo(MANAGED_CONFIG.icon) assertThat(text).isEqualTo(MANAGED_CONFIG.text) assertThat(onClick).isNotNull() } assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun config_notCollapsed_beforeDelay() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun config_collapsed_afterDelay() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY + 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun changeConfig_timerRestartedForCollapsed() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 2.seconds) setSecurityConfig(INFO_CONFIG) with(underTest.securityInfoViewModel!!) { assertThat(icon).isEqualTo(INFO_CONFIG.icon) assertThat(text).isEqualTo(INFO_CONFIG.text) assertThat(onClick).isNull() } assertThat(underTest.securityInfoShowCollapsed).isFalse() testScope.advanceTimeBy(COLLAPSED_DELAY - 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun changeConfigToNull_collapsedAgainImmediately() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 2.seconds) setSecurityConfig(null) assertThat(underTest.securityInfoShowCollapsed).isTrue() } } private fun Kosmos.setSecurityConfig(config: SecurityButtonConfig?) { footerActionsInteractor.fake.setSecurityConfig(config) runCurrent() } private companion object { val MANAGED_CONFIG = SecurityButtonConfig( icon = Icon.Resource(R.drawable.vd_work, null), text = "Managed device", isClickable = true, ) val INFO_CONFIG = SecurityButtonConfig( icon = Icon.Resource(R.drawable.ic_info, null), text = "General information", isClickable = false, ) private val COLLAPSED_DELAY = 5.seconds } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/SecurityInfo.kt 0 → 100644 +101 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.panels.ui.compose.toolbar import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.compose.animation.rememberExpandableController import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.ui.compose.borderOnFocus @Composable fun SecurityInfo( viewModel: FooterActionsSecurityButtonViewModel?, showCollapsed: Boolean, modifier: Modifier = Modifier, ) { if (viewModel == null) { return } val onClick: ((Expandable) -> Unit)? = viewModel.onClick?.let { onClick -> val context = LocalContext.current { expandable -> onClick(context, expandable) } } CompositionLocalProvider( value = LocalContentColor provides MaterialTheme.colorScheme.onSurface ) { Expandable( controller = rememberExpandableController(color = Color.Transparent, shape = CircleShape), modifier = modifier .padding(horizontal = 4.dp) .borderOnFocus(color = MaterialTheme.colorScheme.secondary, CornerSize(0)) .semantics { if (onClick != null) { role = Role.Button } }, onClick = onClick, useModifierBasedImplementation = true, ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( icon = viewModel.icon, modifier = Modifier.minimumInteractiveComponentSize().size(24.dp).semantics { if (showCollapsed) { contentDescription = viewModel.text } }, ) if (!showCollapsed) { Text( text = viewModel.text, maxLines = 1, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.basicMarquee(iterations = 1, initialDelayMillis = 1000), ) } } } } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +103 −32 Original line number Diff line number Diff line Loading @@ -16,13 +16,19 @@ package com.android.systemui.qs.panels.ui.compose.toolbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext Loading @@ -31,21 +37,60 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.footer.ui.compose.IconButton import com.android.systemui.qs.panels.ui.compose.toolbar.Toolbar.TransitionKeys.SecurityInfoKey import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel import com.android.systemui.qs.ui.compose.borderOnFocus @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { viewModel.userSwitcherViewModel?.let { val securityInfoCollapsed = viewModel.securityInfoShowCollapsed SharedTransitionLayout(modifier = Modifier.weight(1f)) { AnimatedContent( targetState = securityInfoCollapsed, contentAlignment = if (securityInfoCollapsed) { Alignment.CenterStart } else { Alignment.Center }, label = "Toolbar.CollapsedSecurityInfo", ) { securityInfoCollapsed -> if (securityInfoCollapsed) { StandardToolbarLayout(animatedContentScope = this@AnimatedContent, viewModel) } else { SecurityInfo( viewModel = viewModel.securityInfoViewModel, showCollapsed = false, modifier = Modifier.sharedElement( rememberSharedContentState(key = SecurityInfoKey), animatedVisibilityScope = this@AnimatedContent, ), ) } } } IconButton( it, { viewModel.powerButtonViewModel }, useModifierBasedExpandable = true, Modifier.sysuiResTag("multi_user_switch"), Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), ) } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable private fun SharedTransitionScope.StandardToolbarLayout( animatedContentScope: AnimatedContentScope, viewModel: ToolbarViewModel, modifier: Modifier = Modifier, ) { Row(modifier) { // TODO(b/410843063): Support the tooltip in DualShade val editModeButtonViewModel = rememberViewModel("Toolbar") { viewModel.editModeButtonViewModelFactory.create() } Loading @@ -54,17 +99,42 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { IconButton( viewModel.settingsButtonViewModel, useModifierBasedExpandable = true, Modifier.sysuiResTag("settings_button_container"), Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), ) viewModel.userSwitcherViewModel?.let { IconButton( it, useModifierBasedExpandable = true, Modifier.sysuiResTag("multi_user_switch").minimumInteractiveComponentSize(), ) } SecurityInfo( viewModel = viewModel.securityInfoViewModel, showCollapsed = true, modifier = Modifier.sharedElement( rememberSharedContentState(key = SecurityInfoKey), animatedVisibilityScope = animatedContentScope, ), ) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { val context = LocalContext.current val textFeedbackViewModel = rememberViewModel("", context) { rememberViewModel("Toolbar.TextFeedbackViewModel", context) { viewModel.textFeedbackContentViewModelFactory.create(context) } if (textFeedbackViewModel.textFeedback != TextFeedbackViewModel.NoFeedback) { Box(modifier = Modifier.weight(1f)) { val hasTextFeedback = textFeedbackViewModel.textFeedback !is TextFeedbackViewModel.NoFeedback Crossfade( targetState = hasTextFeedback, modifier = Modifier.align(Alignment.Center), label = "Toolbar.ShowTextFeedback", ) { showTextFeedback -> if (showTextFeedback) { TextFeedback(textFeedbackViewModel.textFeedback, Modifier.wrapContentSize()) } else { BuildNumber( Loading @@ -79,11 +149,12 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { ) } } } } } IconButton( { viewModel.powerButtonViewModel }, useModifierBasedExpandable = true, Modifier.sysuiResTag("pm_lite"), ) private object Toolbar { object TransitionKeys { const val SecurityInfoKey = "SecurityInfo" } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TextFeedbackContentViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -42,7 +42,7 @@ constructor( ) : ExclusiveActivatable() { private val hydrator = Hydrator("TextFeedbackViewModel.hydrator") val textFeedback by val textFeedback: TextFeedbackViewModel by hydrator.hydratedStateOf( traceName = "textFeedback", initialValue = TextFeedbackViewModel.NoFeedback, Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt +40 −0 Original line number Diff line number Diff line Loading @@ -30,7 +30,9 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.powerButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.securityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.settingsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.userSwitcherViewModel import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackContentViewModel Loading @@ -40,8 +42,13 @@ import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import javax.inject.Provider import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class ToolbarViewModel Loading Loading @@ -87,6 +94,18 @@ constructor( ), ) var securityInfoViewModel: FooterActionsSecurityButtonViewModel? by mutableStateOf(null) private set /** * Whether the security info text should be shown. When this is `true`, only the icon should be * shown. * * If there's no security info to show, this will also be `true`. */ var securityInfoShowCollapsed: Boolean by mutableStateOf(true) private set override suspend fun onActivated(): Nothing { coroutineScope { launch { Loading @@ -98,6 +117,17 @@ constructor( } } launch { hydrator.activate() } launch { footerActionsInteractor.securityButtonConfig .map { it?.let { securityButtonViewModel(it, ::onSecurityButtonClicked) } } .distinctUntilChanged() .collectLatest { securityInfoShowCollapsed = it == null securityInfoViewModel = it delay(COLLAPSED_SECURITY_INFO_DELAY) securityInfoShowCollapsed = true } } awaitCancellation() } } Loading @@ -120,8 +150,18 @@ constructor( falsingInteractor.runIfNotFalseTap { footerActionsInteractor.showSettings(expandable) } } fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { falsingInteractor.runIfNotFalseTap { footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } } @AssistedFactory interface Factory { fun create(): ToolbarViewModel } private companion object { val COLLAPSED_SECURITY_INFO_DELAY = 5.seconds } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelTest.kt 0 → 100644 +177 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.panels.ui.viewmodel.toolbar import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.fake import com.android.systemui.qs.footer.domain.interactor.FakeFooterActionInteractor import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.qs.footerActionsInteractor import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class ToolbarViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().apply { footerActionsInteractor = FakeFooterActionInteractor() } private val Kosmos.underTest by Kosmos.Fixture { toolbarViewModelFactory.create() } @Before fun setUp() { kosmos.underTest.activateIn(kosmos.testScope) } @Test fun start_noSecurityInfo_collapsed() = with(kosmos) { runTest { assertThat(underTest.securityInfoViewModel).isNull() assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun nullConfig_noSecurityInfo_collapsed() = with(kosmos) { runTest { setSecurityConfig(null) assertThat(underTest.securityInfoViewModel).isNull() assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun config_notCollapsed() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) with(underTest.securityInfoViewModel!!) { assertThat(icon).isEqualTo(MANAGED_CONFIG.icon) assertThat(text).isEqualTo(MANAGED_CONFIG.text) assertThat(onClick).isNotNull() } assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun config_notCollapsed_beforeDelay() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun config_collapsed_afterDelay() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY + 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isTrue() } } @Test fun changeConfig_timerRestartedForCollapsed() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 2.seconds) setSecurityConfig(INFO_CONFIG) with(underTest.securityInfoViewModel!!) { assertThat(icon).isEqualTo(INFO_CONFIG.icon) assertThat(text).isEqualTo(INFO_CONFIG.text) assertThat(onClick).isNull() } assertThat(underTest.securityInfoShowCollapsed).isFalse() testScope.advanceTimeBy(COLLAPSED_DELAY - 100.milliseconds) assertThat(underTest.securityInfoShowCollapsed).isFalse() } } @Test fun changeConfigToNull_collapsedAgainImmediately() = with(kosmos) { runTest { setSecurityConfig(MANAGED_CONFIG) testScope.advanceTimeBy(COLLAPSED_DELAY - 2.seconds) setSecurityConfig(null) assertThat(underTest.securityInfoShowCollapsed).isTrue() } } private fun Kosmos.setSecurityConfig(config: SecurityButtonConfig?) { footerActionsInteractor.fake.setSecurityConfig(config) runCurrent() } private companion object { val MANAGED_CONFIG = SecurityButtonConfig( icon = Icon.Resource(R.drawable.vd_work, null), text = "Managed device", isClickable = true, ) val INFO_CONFIG = SecurityButtonConfig( icon = Icon.Resource(R.drawable.ic_info, null), text = "General information", isClickable = false, ) private val COLLAPSED_DELAY = 5.seconds } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/SecurityInfo.kt 0 → 100644 +101 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.panels.ui.compose.toolbar import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.compose.animation.rememberExpandableController import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.ui.compose.borderOnFocus @Composable fun SecurityInfo( viewModel: FooterActionsSecurityButtonViewModel?, showCollapsed: Boolean, modifier: Modifier = Modifier, ) { if (viewModel == null) { return } val onClick: ((Expandable) -> Unit)? = viewModel.onClick?.let { onClick -> val context = LocalContext.current { expandable -> onClick(context, expandable) } } CompositionLocalProvider( value = LocalContentColor provides MaterialTheme.colorScheme.onSurface ) { Expandable( controller = rememberExpandableController(color = Color.Transparent, shape = CircleShape), modifier = modifier .padding(horizontal = 4.dp) .borderOnFocus(color = MaterialTheme.colorScheme.secondary, CornerSize(0)) .semantics { if (onClick != null) { role = Role.Button } }, onClick = onClick, useModifierBasedImplementation = true, ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( icon = viewModel.icon, modifier = Modifier.minimumInteractiveComponentSize().size(24.dp).semantics { if (showCollapsed) { contentDescription = viewModel.text } }, ) if (!showCollapsed) { Text( text = viewModel.text, maxLines = 1, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, modifier = Modifier.basicMarquee(iterations = 1, initialDelayMillis = 1000), ) } } } } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +103 −32 Original line number Diff line number Diff line Loading @@ -16,13 +16,19 @@ package com.android.systemui.qs.panels.ui.compose.toolbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext Loading @@ -31,21 +37,60 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.footer.ui.compose.IconButton import com.android.systemui.qs.panels.ui.compose.toolbar.Toolbar.TransitionKeys.SecurityInfoKey import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel import com.android.systemui.qs.ui.compose.borderOnFocus @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { viewModel.userSwitcherViewModel?.let { val securityInfoCollapsed = viewModel.securityInfoShowCollapsed SharedTransitionLayout(modifier = Modifier.weight(1f)) { AnimatedContent( targetState = securityInfoCollapsed, contentAlignment = if (securityInfoCollapsed) { Alignment.CenterStart } else { Alignment.Center }, label = "Toolbar.CollapsedSecurityInfo", ) { securityInfoCollapsed -> if (securityInfoCollapsed) { StandardToolbarLayout(animatedContentScope = this@AnimatedContent, viewModel) } else { SecurityInfo( viewModel = viewModel.securityInfoViewModel, showCollapsed = false, modifier = Modifier.sharedElement( rememberSharedContentState(key = SecurityInfoKey), animatedVisibilityScope = this@AnimatedContent, ), ) } } } IconButton( it, { viewModel.powerButtonViewModel }, useModifierBasedExpandable = true, Modifier.sysuiResTag("multi_user_switch"), Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), ) } } @OptIn(ExperimentalSharedTransitionApi::class) @Composable private fun SharedTransitionScope.StandardToolbarLayout( animatedContentScope: AnimatedContentScope, viewModel: ToolbarViewModel, modifier: Modifier = Modifier, ) { Row(modifier) { // TODO(b/410843063): Support the tooltip in DualShade val editModeButtonViewModel = rememberViewModel("Toolbar") { viewModel.editModeButtonViewModelFactory.create() } Loading @@ -54,17 +99,42 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { IconButton( viewModel.settingsButtonViewModel, useModifierBasedExpandable = true, Modifier.sysuiResTag("settings_button_container"), Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), ) viewModel.userSwitcherViewModel?.let { IconButton( it, useModifierBasedExpandable = true, Modifier.sysuiResTag("multi_user_switch").minimumInteractiveComponentSize(), ) } SecurityInfo( viewModel = viewModel.securityInfoViewModel, showCollapsed = true, modifier = Modifier.sharedElement( rememberSharedContentState(key = SecurityInfoKey), animatedVisibilityScope = animatedContentScope, ), ) Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { val context = LocalContext.current val textFeedbackViewModel = rememberViewModel("", context) { rememberViewModel("Toolbar.TextFeedbackViewModel", context) { viewModel.textFeedbackContentViewModelFactory.create(context) } if (textFeedbackViewModel.textFeedback != TextFeedbackViewModel.NoFeedback) { Box(modifier = Modifier.weight(1f)) { val hasTextFeedback = textFeedbackViewModel.textFeedback !is TextFeedbackViewModel.NoFeedback Crossfade( targetState = hasTextFeedback, modifier = Modifier.align(Alignment.Center), label = "Toolbar.ShowTextFeedback", ) { showTextFeedback -> if (showTextFeedback) { TextFeedback(textFeedbackViewModel.textFeedback, Modifier.wrapContentSize()) } else { BuildNumber( Loading @@ -79,11 +149,12 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { ) } } } } } IconButton( { viewModel.powerButtonViewModel }, useModifierBasedExpandable = true, Modifier.sysuiResTag("pm_lite"), ) private object Toolbar { object TransitionKeys { const val SecurityInfoKey = "SecurityInfo" } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TextFeedbackContentViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -42,7 +42,7 @@ constructor( ) : ExclusiveActivatable() { private val hydrator = Hydrator("TextFeedbackViewModel.hydrator") val textFeedback by val textFeedback: TextFeedbackViewModel by hydrator.hydratedStateOf( traceName = "textFeedback", initialValue = TextFeedbackViewModel.NoFeedback, Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt +40 −0 Original line number Diff line number Diff line Loading @@ -30,7 +30,9 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.powerButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.securityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.settingsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.userSwitcherViewModel import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackContentViewModel Loading @@ -40,8 +42,13 @@ import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import javax.inject.Provider import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class ToolbarViewModel Loading Loading @@ -87,6 +94,18 @@ constructor( ), ) var securityInfoViewModel: FooterActionsSecurityButtonViewModel? by mutableStateOf(null) private set /** * Whether the security info text should be shown. When this is `true`, only the icon should be * shown. * * If there's no security info to show, this will also be `true`. */ var securityInfoShowCollapsed: Boolean by mutableStateOf(true) private set override suspend fun onActivated(): Nothing { coroutineScope { launch { Loading @@ -98,6 +117,17 @@ constructor( } } launch { hydrator.activate() } launch { footerActionsInteractor.securityButtonConfig .map { it?.let { securityButtonViewModel(it, ::onSecurityButtonClicked) } } .distinctUntilChanged() .collectLatest { securityInfoShowCollapsed = it == null securityInfoViewModel = it delay(COLLAPSED_SECURITY_INFO_DELAY) securityInfoShowCollapsed = true } } awaitCancellation() } } Loading @@ -120,8 +150,18 @@ constructor( falsingInteractor.runIfNotFalseTap { footerActionsInteractor.showSettings(expandable) } } fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { falsingInteractor.runIfNotFalseTap { footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } } @AssistedFactory interface Factory { fun create(): ToolbarViewModel } private companion object { val COLLAPSED_SECURITY_INFO_DELAY = 5.seconds } }