Loading packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1042,6 +1042,16 @@ flag { bug: "325099249" } flag { name: "qs_edit_mode_tooltip" namespace: "systemui" description: "Displays an educational tooltip on the edit mode icon from the expanded shade. This flag depends on qs_ui_refactor_compose_fragment flag." bug: "403596182" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "remove_dream_overlay_hide_on_touch" namespace: "systemui" Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt +47 −12 Original line number Diff line number Diff line Loading @@ -25,11 +25,12 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.data.repository.qsPreferencesRepository import com.android.systemui.qs.panels.ui.viewmodel.toolbar.EditModeButtonViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory import com.android.systemui.testKosmos Loading @@ -46,7 +47,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest class EditModeButtonViewModelTest : SysuiTestCase() { val kosmos = testKosmos() val kosmos = testKosmos().useUnconfinedTestDispatcher() // NOTE: cannot instantiate EditModeButtonViewModel here because it would hydrate // showEditButton before the value on fakeHeadlessSystemUserMode is set Loading @@ -69,7 +70,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { fakeFalsingManager.setFalseTap(true) underTest.onButtonClick() runCurrent() assertThat(isEditing).isFalse() } Loading @@ -83,7 +83,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { fakeFalsingManager.setFalseTap(false) underTest.onButtonClick() runCurrent() assertThat(isEditing).isTrue() } Loading @@ -94,8 +93,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(false) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -105,8 +102,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(true) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -116,8 +111,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(false) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -127,11 +120,53 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(true) runCurrent() assertThat(underTest.isEditButtonVisible).isFalse() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasAlreadyShown_shouldNotBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(true) assertThat(underTest.showTooltip).isFalse() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasNotShown_shouldBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isTrue() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasDismissed_shouldBeMarkedAsShown() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isTrue() underTest.onTooltipDisposed() assertThat(underTest.showTooltip).isFalse() } @Test @DisableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_flagDisabled_shouldNotBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isFalse() } private fun Kosmos.createEditModeButtonViewModel( isHeadlessSystemUser: Boolean = false ): EditModeButtonViewModel { Loading packages/SystemUI/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2616,6 +2616,9 @@ <!-- Title for QS Edit mode screen [CHAR LIMIT=30] --> <string name="qs_edit_tiles">Edit tiles</string> <!-- Tooltip for the edit mode button indicating that tiles can be resized [CHAR LIMIT=60] --> <string name="qs_edit_mode_tooltip">Resize Quick Settings tiles</string> <!-- SysUI Tuner: Options for how clock is displayed [CHAR LIMIT=NONE] --> <string name="tuner_time">Time</string> Loading packages/SystemUI/src/com/android/systemui/qs/flags/QSEditModeTooltip.kt 0 → 100644 +48 −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.flags import com.android.systemui.Flags import com.android.systemui.flags.RefactorFlagUtils /** Object to help check if the QS edit mode tooltip is enabled. */ @Suppress("NOTHING_TO_INLINE") object QSEditModeTooltip { /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_QS_EDIT_MODE_TOOLTIP /** Is the tooltip enabled */ @JvmStatic inline val isEnabled get() = Flags.qsEditModeTooltip() /** * Called to ensure code is only run when the flag is enabled. This protects users from the * unintended behaviors caused by accidentally running new logic, while also crashing on an eng * build to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun isUnexpectedlyInLegacyMode() = RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) } packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +25 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.data.repository import android.content.Context import android.content.IntentFilter import android.content.SharedPreferences import androidx.core.content.edit import com.android.systemui.backup.BackupHelper import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED import com.android.systemui.broadcast.BroadcastDispatcher Loading @@ -26,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.qs.flags.QSEditModeTooltip import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.TilesUpgradePath Loading @@ -35,6 +37,7 @@ import com.android.systemui.util.kotlin.SharedPreferencesExt.observe import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest Loading @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach /** Repository for QS user preferences. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class QSPreferencesRepository @Inject Loading Loading @@ -75,6 +79,17 @@ constructor( } .flowOn(backgroundDispatcher) /** Whether or not the edit icon tooltip was shown for the current user. */ val editTooltipShown: Flow<Boolean> = combine(backupRestorationEvents, userRepository.selectedUserInfo, ::Pair) .flatMapLatest { (_, userInfo) -> val prefs = getSharedPrefs(userInfo.id) prefs.observe().emitOnStart().map { prefs.getBoolean(EDIT_TOOLTIP_SHOWN_KEY, false) } } .flowOn(backgroundDispatcher) /** Sets for the current user the set of [TileSpec] to display as large tiles. */ fun writeLargeTileSpecs(specs: Set<TileSpec>) { with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { Loading @@ -92,6 +107,15 @@ constructor( } } /** Sets the value for whether or not the edit icon tooltip was shown for the current user. */ fun writeEditTooltipShown(value: Boolean) { if (QSEditModeTooltip.isEnabled) { getSharedPrefs(userRepository.getSelectedUserInfo().id).edit { putBoolean(EDIT_TOOLTIP_SHOWN_KEY, value) } } } suspend fun deleteLargeTileDataJob() { userRepository.selectedUserInfo.collect { userInfo -> getSharedPrefs(userInfo.id) Loading Loading @@ -174,6 +198,7 @@ constructor( private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default" private const val EDIT_TOOLTIP_SHOWN_KEY = "edit_tooltip_shown" const val FILE_NAME = "quick_settings_prefs" } } Loading
packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1042,6 +1042,16 @@ flag { bug: "325099249" } flag { name: "qs_edit_mode_tooltip" namespace: "systemui" description: "Displays an educational tooltip on the edit mode icon from the expanded shade. This flag depends on qs_ui_refactor_compose_fragment flag." bug: "403596182" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "remove_dream_overlay_hide_on_touch" namespace: "systemui" Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt +47 −12 Original line number Diff line number Diff line Loading @@ -25,11 +25,12 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.data.repository.qsPreferencesRepository import com.android.systemui.qs.panels.ui.viewmodel.toolbar.EditModeButtonViewModel import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory import com.android.systemui.testKosmos Loading @@ -46,7 +47,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest class EditModeButtonViewModelTest : SysuiTestCase() { val kosmos = testKosmos() val kosmos = testKosmos().useUnconfinedTestDispatcher() // NOTE: cannot instantiate EditModeButtonViewModel here because it would hydrate // showEditButton before the value on fakeHeadlessSystemUserMode is set Loading @@ -69,7 +70,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { fakeFalsingManager.setFalseTap(true) underTest.onButtonClick() runCurrent() assertThat(isEditing).isFalse() } Loading @@ -83,7 +83,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { fakeFalsingManager.setFalseTap(false) underTest.onButtonClick() runCurrent() assertThat(isEditing).isTrue() } Loading @@ -94,8 +93,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(false) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -105,8 +102,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(true) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -116,8 +111,6 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(false) runCurrent() assertThat(underTest.isEditButtonVisible).isTrue() } Loading @@ -127,11 +120,53 @@ class EditModeButtonViewModelTest : SysuiTestCase() { kosmos.runTest { val underTest = createEditModeButtonViewModel(true) runCurrent() assertThat(underTest.isEditButtonVisible).isFalse() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasAlreadyShown_shouldNotBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(true) assertThat(underTest.showTooltip).isFalse() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasNotShown_shouldBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isTrue() } @Test @EnableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_tooltipWasDismissed_shouldBeMarkedAsShown() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isTrue() underTest.onTooltipDisposed() assertThat(underTest.showTooltip).isFalse() } @Test @DisableFlags(Flags.FLAG_QS_EDIT_MODE_TOOLTIP) fun showTooltip_flagDisabled_shouldNotBeVisible() = kosmos.runTest { val underTest = createEditModeButtonViewModel() qsPreferencesRepository.writeEditTooltipShown(false) assertThat(underTest.showTooltip).isFalse() } private fun Kosmos.createEditModeButtonViewModel( isHeadlessSystemUser: Boolean = false ): EditModeButtonViewModel { Loading
packages/SystemUI/res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2616,6 +2616,9 @@ <!-- Title for QS Edit mode screen [CHAR LIMIT=30] --> <string name="qs_edit_tiles">Edit tiles</string> <!-- Tooltip for the edit mode button indicating that tiles can be resized [CHAR LIMIT=60] --> <string name="qs_edit_mode_tooltip">Resize Quick Settings tiles</string> <!-- SysUI Tuner: Options for how clock is displayed [CHAR LIMIT=NONE] --> <string name="tuner_time">Time</string> Loading
packages/SystemUI/src/com/android/systemui/qs/flags/QSEditModeTooltip.kt 0 → 100644 +48 −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.flags import com.android.systemui.Flags import com.android.systemui.flags.RefactorFlagUtils /** Object to help check if the QS edit mode tooltip is enabled. */ @Suppress("NOTHING_TO_INLINE") object QSEditModeTooltip { /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_QS_EDIT_MODE_TOOLTIP /** Is the tooltip enabled */ @JvmStatic inline val isEnabled get() = Flags.qsEditModeTooltip() /** * Called to ensure code is only run when the flag is enabled. This protects users from the * unintended behaviors caused by accidentally running new logic, while also crashing on an eng * build to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun isUnexpectedlyInLegacyMode() = RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) }
packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +25 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.data.repository import android.content.Context import android.content.IntentFilter import android.content.SharedPreferences import androidx.core.content.edit import com.android.systemui.backup.BackupHelper import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED import com.android.systemui.broadcast.BroadcastDispatcher Loading @@ -26,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.qs.flags.QSEditModeTooltip import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.TilesUpgradePath Loading @@ -35,6 +37,7 @@ import com.android.systemui.util.kotlin.SharedPreferencesExt.observe import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest Loading @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach /** Repository for QS user preferences. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class QSPreferencesRepository @Inject Loading Loading @@ -75,6 +79,17 @@ constructor( } .flowOn(backgroundDispatcher) /** Whether or not the edit icon tooltip was shown for the current user. */ val editTooltipShown: Flow<Boolean> = combine(backupRestorationEvents, userRepository.selectedUserInfo, ::Pair) .flatMapLatest { (_, userInfo) -> val prefs = getSharedPrefs(userInfo.id) prefs.observe().emitOnStart().map { prefs.getBoolean(EDIT_TOOLTIP_SHOWN_KEY, false) } } .flowOn(backgroundDispatcher) /** Sets for the current user the set of [TileSpec] to display as large tiles. */ fun writeLargeTileSpecs(specs: Set<TileSpec>) { with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { Loading @@ -92,6 +107,15 @@ constructor( } } /** Sets the value for whether or not the edit icon tooltip was shown for the current user. */ fun writeEditTooltipShown(value: Boolean) { if (QSEditModeTooltip.isEnabled) { getSharedPrefs(userRepository.getSelectedUserInfo().id).edit { putBoolean(EDIT_TOOLTIP_SHOWN_KEY, value) } } } suspend fun deleteLargeTileDataJob() { userRepository.selectedUserInfo.collect { userInfo -> getSharedPrefs(userInfo.id) Loading Loading @@ -174,6 +198,7 @@ constructor( private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default" private const val EDIT_TOOLTIP_SHOWN_KEY = "edit_tooltip_shown" const val FILE_NAME = "quick_settings_prefs" } }