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/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
index c2658a9e61b12..f60db2ad2687f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
@@ -108,4 +108,30 @@ object KeyguardQuickAffordanceProviderContract {
const val AFFORDANCE_ID = "affordance_id"
}
}
+
+ /**
+ * Table for flags.
+ *
+ * Flags are key-value pairs.
+ *
+ * Supported operations:
+ * - Query - to know the values of flags, query the [FlagsTable.URI] [Uri]. The result set will
+ * contain rows, each of which with the columns from [FlagsTable.Columns].
+ */
+ object FlagsTable {
+ const val TABLE_NAME = "flags"
+ val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+ /**
+ * Flag denoting whether the customizable lock screen quick affordances feature is enabled.
+ */
+ const val FLAG_NAME_FEATURE_ENABLED = "is_feature_enabled"
+
+ object Columns {
+ /** String. Unique ID for the flag. */
+ const val NAME = "name"
+ /** Int. Value of the flag. `1` means `true` and `0` means `false`. */
+ const val VALUE = "value"
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
index 0f4581ce3e616..1f1ed007fca0a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -31,7 +31,6 @@ import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCall
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import javax.inject.Inject
-import kotlinx.coroutines.runBlocking
class KeyguardQuickAffordanceProvider :
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
@@ -57,6 +56,11 @@ class KeyguardQuickAffordanceProvider :
Contract.SelectionTable.TABLE_NAME,
MATCH_CODE_ALL_SELECTIONS,
)
+ addURI(
+ Contract.AUTHORITY,
+ Contract.FlagsTable.TABLE_NAME,
+ MATCH_CODE_ALL_FLAGS,
+ )
}
override fun onCreate(): Boolean {
@@ -77,6 +81,7 @@ class KeyguardQuickAffordanceProvider :
when (uriMatcher.match(uri)) {
MATCH_CODE_ALL_SLOTS,
MATCH_CODE_ALL_AFFORDANCES,
+ MATCH_CODE_ALL_FLAGS,
MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
else -> null
}
@@ -86,6 +91,7 @@ class KeyguardQuickAffordanceProvider :
MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
+ MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME
else -> null
}
@@ -115,6 +121,7 @@ class KeyguardQuickAffordanceProvider :
MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
MATCH_CODE_ALL_SLOTS -> querySlots()
MATCH_CODE_ALL_SELECTIONS -> querySelections()
+ MATCH_CODE_ALL_FLAGS -> queryFlags()
else -> null
}
}
@@ -171,12 +178,11 @@ class KeyguardQuickAffordanceProvider :
throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
}
- val success = runBlocking {
+ val success =
interactor.select(
slotId = slotId,
affordanceId = affordanceId,
)
- }
return if (success) {
Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
@@ -196,7 +202,7 @@ class KeyguardQuickAffordanceProvider :
)
)
.apply {
- val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
+ val affordanceIdsBySlotId = interactor.getSelections()
affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
affordanceIds.forEach { affordanceId ->
addRow(
@@ -250,6 +256,29 @@ class KeyguardQuickAffordanceProvider :
}
}
+ private fun queryFlags(): Cursor {
+ return MatrixCursor(
+ arrayOf(
+ Contract.FlagsTable.Columns.NAME,
+ Contract.FlagsTable.Columns.VALUE,
+ )
+ )
+ .apply {
+ interactor.getPickerFlags().forEach { flag ->
+ addRow(
+ arrayOf(
+ flag.name,
+ if (flag.value) {
+ 1
+ } else {
+ 0
+ },
+ )
+ )
+ }
+ }
+ }
+
private fun deleteSelection(
uri: Uri,
selectionArgs: Array?,
@@ -271,12 +300,11 @@ class KeyguardQuickAffordanceProvider :
)
}
- val deleted = runBlocking {
+ val deleted =
interactor.unselect(
slotId = slotId,
affordanceId = affordanceId,
)
- }
return if (deleted) {
Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
@@ -293,5 +321,6 @@ class KeyguardQuickAffordanceProvider :
private const val MATCH_CODE_ALL_SLOTS = 1
private const val MATCH_CODE_ALL_AFFORDANCES = 2
private const val MATCH_CODE_ALL_SELECTIONS = 3
+ private const val MATCH_CODE_ALL_FLAGS = 4
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
new file mode 100644
index 0000000000000..766096f1fa2bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2022 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.keyguard.data.quickaffordance
+
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer.Companion.BINDINGS
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Keeps quick affordance selections and legacy user settings in sync.
+ *
+ * "Legacy user settings" are user settings like: Settings > Display > Lock screen > "Show device
+ * controls" Settings > Display > Lock screen > "Show wallet"
+ *
+ * Quick affordance selections are the ones available through the new custom lock screen experience
+ * from Settings > Wallpaper & Style.
+ *
+ * This class keeps these in sync, mostly for backwards compatibility purposes and in order to not
+ * "forget" an existing legacy user setting when the device gets updated with a version of System UI
+ * that has the new customizable lock screen feature.
+ *
+ * The way it works is that, when [startSyncing] is called, the syncer starts coroutines to listen
+ * for changes in both legacy user settings and their respective affordance selections. Whenever one
+ * of each pair is changed, the other member of that pair is also updated to match. For example, if
+ * the user turns on "Show device controls", we automatically select the home controls affordance
+ * for the preferred slot. Conversely, when the home controls affordance is unselected by the user,
+ * we set the "Show device controls" setting to "off".
+ *
+ * The class can be configured by updating its list of triplets in the code under [BINDINGS].
+ */
+@SysUISingleton
+class KeyguardQuickAffordanceLegacySettingSyncer
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val secureSettings: SecureSettings,
+ private val selectionsManager: KeyguardQuickAffordanceSelectionManager,
+) {
+ companion object {
+ private val BINDINGS =
+ listOf(
+ Binding(
+ settingsKey = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
+ ),
+ Binding(
+ settingsKey = Settings.Secure.LOCKSCREEN_SHOW_WALLET,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET,
+ ),
+ Binding(
+ settingsKey = Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER,
+ ),
+ )
+ }
+
+ fun startSyncing(
+ bindings: List = BINDINGS,
+ ): Job {
+ return scope.launch { bindings.forEach { binding -> startSyncing(this, binding) } }
+ }
+
+ private fun startSyncing(
+ scope: CoroutineScope,
+ binding: Binding,
+ ) {
+ secureSettings
+ .observerFlow(
+ names = arrayOf(binding.settingsKey),
+ userId = UserHandle.USER_ALL,
+ )
+ .map {
+ isSet(
+ settingsKey = binding.settingsKey,
+ )
+ }
+ .distinctUntilChanged()
+ .onEach { isSet ->
+ if (isSelected(binding.affordanceId) != isSet) {
+ if (isSet) {
+ select(
+ slotId = binding.slotId,
+ affordanceId = binding.affordanceId,
+ )
+ } else {
+ unselect(
+ affordanceId = binding.affordanceId,
+ )
+ }
+ }
+ }
+ .flowOn(backgroundDispatcher)
+ .launchIn(scope)
+
+ selectionsManager.selections
+ .map { it.values.flatten().toSet() }
+ .map { it.contains(binding.affordanceId) }
+ .distinctUntilChanged()
+ .onEach { isSelected ->
+ if (isSet(binding.settingsKey) != isSelected) {
+ set(binding.settingsKey, isSelected)
+ }
+ }
+ .flowOn(backgroundDispatcher)
+ .launchIn(scope)
+ }
+
+ private fun isSelected(
+ affordanceId: String,
+ ): Boolean {
+ return selectionsManager
+ .getSelections() // Map>
+ .values // Collection>
+ .flatten() // List
+ .toSet() // Set
+ .contains(affordanceId)
+ }
+
+ private fun select(
+ slotId: String,
+ affordanceId: String,
+ ) {
+ val affordanceIdsAtSlotId = selectionsManager.getSelections()[slotId] ?: emptyList()
+ selectionsManager.setSelections(
+ slotId = slotId,
+ affordanceIds = affordanceIdsAtSlotId + listOf(affordanceId),
+ )
+ }
+
+ private fun unselect(
+ affordanceId: String,
+ ) {
+ val currentSelections = selectionsManager.getSelections()
+ val slotIdsContainingAffordanceId =
+ currentSelections
+ .filter { (_, affordanceIds) -> affordanceIds.contains(affordanceId) }
+ .map { (slotId, _) -> slotId }
+
+ slotIdsContainingAffordanceId.forEach { slotId ->
+ val currentAffordanceIds = currentSelections[slotId] ?: emptyList()
+ val affordanceIdsAfterUnselecting =
+ currentAffordanceIds.toMutableList().apply { remove(affordanceId) }
+
+ selectionsManager.setSelections(
+ slotId = slotId,
+ affordanceIds = affordanceIdsAfterUnselecting,
+ )
+ }
+ }
+
+ private fun isSet(
+ settingsKey: String,
+ ): Boolean {
+ return secureSettings.getIntForUser(
+ settingsKey,
+ 0,
+ UserHandle.USER_CURRENT,
+ ) != 0
+ }
+
+ private suspend fun set(
+ settingsKey: String,
+ isSet: Boolean,
+ ) {
+ withContext(backgroundDispatcher) {
+ secureSettings.putInt(
+ settingsKey,
+ if (isSet) 1 else 0,
+ )
+ }
+ }
+
+ data class Binding(
+ val settingsKey: String,
+ val slotId: String,
+ val affordanceId: String,
+ )
+}
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 9c9354fec6950..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
@@ -17,46 +17,138 @@
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
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
/**
* Manages and provides access to the current "selections" of keyguard quick affordances, answering
* the question "which affordances should the keyguard show?".
*/
@SysUISingleton
-class KeyguardQuickAffordanceSelectionManager @Inject constructor() {
+class KeyguardQuickAffordanceSelectionManager
+@Inject
+constructor(
+ @Application context: Context,
+ private val userFileManager: UserFileManager,
+ private val userTracker: UserTracker,
+) {
- // TODO(b/254858695): implement a persistence layer (database).
- private val _selections = MutableStateFlow