diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 88af1793d7716..7a362040427a8 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -817,4 +817,13 @@ bottom_end:1 + + + + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index d89ac37322b42..b29cf45cc7094 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -20,9 +20,11 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.content.SharedPreferences import androidx.annotation.VisibleForTesting +import com.android.systemui.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import javax.inject.Inject @@ -38,6 +40,7 @@ import kotlinx.coroutines.flow.flatMapLatest class KeyguardQuickAffordanceSelectionManager @Inject constructor( + @Application context: Context, private val userFileManager: UserFileManager, private val userTracker: UserTracker, ) { @@ -63,6 +66,17 @@ constructor( awaitClose { userTracker.removeCallback(callback) } } + private val defaults: Map> by lazy { + context.resources + .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) + .associate { item -> + val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) + check(splitUp.size == 2) + val slotId = splitUp[0] + val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) + slotId to affordanceIds + } + } /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ val selections: Flow>> = @@ -86,17 +100,35 @@ constructor( */ fun getSelections(): Map> { val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } - return slotKeys.associate { key -> - val slotId = key.substring(KEY_PREFIX_SLOT.length) - val value = sharedPrefs.getString(key, null) - val affordanceIds = - if (!value.isNullOrEmpty()) { - value.split(DELIMITER) - } else { - emptyList() + val result = + slotKeys + .associate { key -> + val slotId = key.substring(KEY_PREFIX_SLOT.length) + val value = sharedPrefs.getString(key, null) + val affordanceIds = + if (!value.isNullOrEmpty()) { + value.split(AFFORDANCE_DELIMITER) + } else { + emptyList() + } + slotId to affordanceIds } - slotId to affordanceIds + .toMutableMap() + + // If the result map is missing keys, it means that the system has never set anything for + // those slots. This is where we need examine our defaults and see if there should be a + // default value for the affordances in the slot IDs that are missing from the result. + // + // Once the user makes any selection for a slot, even when they select "None", this class + // will persist a key for that slot ID. In the case of "None", it will have a value of the + // empty string. This is why this system works. + defaults.forEach { (slotId, affordanceIds) -> + if (!result.containsKey(slotId)) { + result[slotId] = affordanceIds + } } + + return result } /** @@ -108,7 +140,7 @@ constructor( affordanceIds: List, ) { val key = "$KEY_PREFIX_SLOT$slotId" - val value = affordanceIds.joinToString(DELIMITER) + val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) sharedPrefs.edit().putString(key, value).apply() } @@ -116,6 +148,7 @@ constructor( private const val TAG = "KeyguardQuickAffordanceSelectionManager" @VisibleForTesting const val FILE_NAME = "quick_affordance_selections" private const val KEY_PREFIX_SLOT = "slot_" - private const val DELIMITER = "," + private const val SLOT_AFFORDANCES_DELIMITER = ":" + private const val AFFORDANCE_DELIMITER = "," } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index 2d0f9e264f692..5228e1774edf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -77,6 +77,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index b8bef1a9d6f36..d8ee9f113d33c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -19,8 +19,8 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.SharedPreferences import android.content.pm.UserInfo -import androidx.test.core.app.ActivityScenario.launch import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager @@ -63,6 +63,7 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = userFileManager, userTracker = userTracker, ) @@ -70,6 +71,7 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { @Test fun setSelections() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf()) val affordanceIdsBySlotId = mutableListOf>>() val job = launch(UnconfinedTestDispatcher()) { @@ -221,6 +223,101 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { job.cancel() } + @Test + fun `selections respects defaults`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + + @Test + fun `selections ignores defaults after selecting an affordance`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + underTest.setSelections(slotId1, listOf(affordanceId2)) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId2), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + + @Test + fun `selections ignores defaults after clearing a slot`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + underTest.setSelections(slotId1, listOf()) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + private fun assertSelections( observed: Map>?, expected: Map>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index a95f788b1ee91..e56e1b93dcfb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -64,6 +64,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 3e37459d8eeb9..3b1cbb1bd6203 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -240,6 +240,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index f887bf93de8d1..1374c43ebe1ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -100,6 +100,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 03e6ccb6fabe2..abe8a304a4ef7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -124,6 +124,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever(