Defines config for default affordances.
As per a request from the large screen team, adding a way for device configurations to be able to supply System UI with default affordances for slot IDs. The way it works is: 1. An OEM overrides the value of config_keyguardQuickAffordanceDefaults in config.xml with a list of key-value pairs. Each pair is a slot ID followed by one or more affordance IDs. 2. The KeyguardQuickAffordanceSelectionManager contains logic to default to those affordances in cases when the user hasn't yet selected anything for that slot. Once the user selects something for the slot, even the "None" option (which clears out the slot), the default is ignored. Fix: 257051288 Test: manually verified by temporarily adding <item>bottom_end:home,wallet</item> to the config.xml and seeing that, if I cleared data for system UI and restarted it, the home control affordance was visible (after I set it up through quick settings home tile, for course - because clearing data forgets that too). Also added unit tests. Change-Id: I63484a92cd8ed8bbcb6a331f500531c62a021f6c
This commit is contained in:
@@ -817,4 +817,13 @@
|
||||
<item>bottom_end:1</item>
|
||||
</string-array>
|
||||
|
||||
<!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
|
||||
string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
|
||||
separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
|
||||
default is displayed by System UI as long as the user hasn't made a different choice for that
|
||||
slot. If the user did make a choice, even if the choice is the "None" option, the default is
|
||||
ignored. -->
|
||||
<string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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<String, List<String>> 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<Map<String, List<String>>> =
|
||||
@@ -86,17 +100,35 @@ constructor(
|
||||
*/
|
||||
fun getSelections(): Map<String, List<String>> {
|
||||
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<String>,
|
||||
) {
|
||||
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 = ","
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
selectionManager =
|
||||
KeyguardQuickAffordanceSelectionManager(
|
||||
context = context,
|
||||
userFileManager =
|
||||
mock<UserFileManager>().apply {
|
||||
whenever(
|
||||
|
||||
@@ -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<String>())
|
||||
val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
|
||||
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<Map<String, List<String>>>()
|
||||
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<Map<String, List<String>>>()
|
||||
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<Map<String, List<String>>>()
|
||||
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<String, List<String>>?,
|
||||
expected: Map<String, List<String>>,
|
||||
|
||||
@@ -64,6 +64,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
selectionManager =
|
||||
KeyguardQuickAffordanceSelectionManager(
|
||||
context = context,
|
||||
userFileManager =
|
||||
mock<UserFileManager>().apply {
|
||||
whenever(
|
||||
|
||||
@@ -240,6 +240,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
selectionManager =
|
||||
KeyguardQuickAffordanceSelectionManager(
|
||||
context = context,
|
||||
userFileManager =
|
||||
mock<UserFileManager>().apply {
|
||||
whenever(
|
||||
|
||||
@@ -100,6 +100,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
selectionManager =
|
||||
KeyguardQuickAffordanceSelectionManager(
|
||||
context = context,
|
||||
userFileManager =
|
||||
mock<UserFileManager>().apply {
|
||||
whenever(
|
||||
|
||||
@@ -124,6 +124,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
selectionManager =
|
||||
KeyguardQuickAffordanceSelectionManager(
|
||||
context = context,
|
||||
userFileManager =
|
||||
mock<UserFileManager>().apply {
|
||||
whenever(
|
||||
|
||||
Reference in New Issue
Block a user