Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7e2a48c authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Automerger Merge Worker
Browse files

Merge changes I2ea2baa4,Icd8ff155,I63484a92,If95af61c into tm-qpr-dev am: dc44d309 am: ead904ba

parents 3073637d ead904ba
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -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>
+26 −0
Original line number Diff line number Diff line
@@ -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"
        }
    }
}
+35 −6
Original line number Diff line number Diff line
@@ -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<out String>?,
@@ -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
    }
}
+214 −0
Original line number Diff line number Diff line
/*
 * 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<Binding> = 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<String, List<String>>
            .values // Collection<List<String>>
            .flatten() // List<String>
            .toSet() // Set<String>
            .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,
    )
}
+107 −15
Original line number Diff line number Diff line
@@ -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,
) {

    private val sharedPrefs: SharedPreferences
        get() =
            userFileManager.getSharedPreferences(
                FILE_NAME,
                Context.MODE_PRIVATE,
                userTracker.userId,
            )

    private val userId: Flow<Int> = conflatedCallbackFlow {
        val callback =
            object : UserTracker.Callback {
                override fun onUserChanged(newUser: Int, userContext: Context) {
                    trySendWithFailureLogging(newUser, TAG)
                }
            }

    // TODO(b/254858695): implement a persistence layer (database).
    private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap())
        userTracker.addCallback(callback) { it.run() }
        trySendWithFailureLogging(userTracker.userId, TAG)

        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>>> = _selections.asStateFlow()
    val selections: Flow<Map<String, List<String>>> =
        userId.flatMapLatest {
            conflatedCallbackFlow {
                val listener =
                    SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
                        trySend(getSelections())
                    }

                sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
                send(getSelections())

                awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
            }
        }

    /**
     * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
     * descending priority order.
     */
    suspend fun getSelections(): Map<String, List<String>> {
        return _selections.value
    fun getSelections(): Map<String, List<String>> {
        val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
        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
                }
                .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
    }

    /**
     * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
     * IDs should be descending priority order.
     */
    suspend fun setSelections(
    fun setSelections(
        slotId: String,
        affordanceIds: List<String>,
    ) {
        // Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit
        // when we set its value to the same instance of the original map, even if we change the
        // map by updating the value of one of its keys.
        val copy = _selections.value.toMutableMap()
        copy[slotId] = affordanceIds
        _selections.value = copy
        val key = "$KEY_PREFIX_SLOT$slotId"
        val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
        sharedPrefs.edit().putString(key, value).apply()
    }

    companion object {
        private const val TAG = "KeyguardQuickAffordanceSelectionManager"
        @VisibleForTesting const val FILE_NAME = "quick_affordance_selections"
        private const val KEY_PREFIX_SLOT = "slot_"
        private const val SLOT_AFFORDANCES_DELIMITER = ":"
        private const val AFFORDANCE_DELIMITER = ","
    }
}
Loading