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

Commit d29f47f3 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Persistence layer for quick affordances.

This approach uses SharedPreferences to persist the selected affordance
IDs for each slot.

Fix: 254858695
Test: unit tests but also manually verified that selecting some
affordances and seeing them on the lock screen then killing the system
UI process via adb and locking the screen again still showed the
selected affordances as expected.

Change-Id: If95af61c7beb14ce97e08018ce7c3b5eed6a06d6
parent 038616a7
Loading
Loading
Loading
Loading
+3 −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 {
@@ -171,12 +170,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 +194,7 @@ class KeyguardQuickAffordanceProvider :
                )
            )
            .apply {
                val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
                val affordanceIdsBySlotId = interactor.getSelections()
                affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
                    affordanceIds.forEach { affordanceId ->
                        addRow(
@@ -271,12 +269,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")
+74 −15
Original line number Diff line number Diff line
@@ -17,46 +17,105 @@

package com.android.systemui.keyguard.data.quickaffordance

import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
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.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(
    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)
                }
            }

        userTracker.addCallback(callback) { it.run() }
        trySendWithFailureLogging(userTracker.userId, TAG)

    // TODO(b/254858695): implement a persistence layer (database).
    private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap())
        awaitClose { userTracker.removeCallback(callback) }
    }

    /** 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) }
        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()
                }
            slotId to affordanceIds
        }
    }

    /**
     * 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(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 DELIMITER = ","
    }
}
+6 −12
Original line number Diff line number Diff line
@@ -21,19 +21,16 @@ import android.content.Context
import com.android.systemui.R
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.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/** Abstracts access to application state related to keyguard quick affordances. */
@SysUISingleton
@@ -42,7 +39,6 @@ class KeyguardQuickAffordanceRepository
constructor(
    @Application private val appContext: Context,
    @Application private val scope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val selectionManager: KeyguardQuickAffordanceSelectionManager,
    private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
) {
@@ -91,7 +87,7 @@ constructor(
     * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
     * slot with the given ID. The configs are sorted in descending priority order.
     */
    suspend fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
    fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
        val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList())
        return configs.filter { selections.contains(it.key) }
    }
@@ -100,7 +96,7 @@ constructor(
     * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
     * are sorted in descending priority order.
     */
    suspend fun getSelections(): Map<String, List<String>> {
    fun getSelections(): Map<String, List<String>> {
        return selectionManager.getSelections()
    }

@@ -112,13 +108,11 @@ constructor(
        slotId: String,
        affordanceIds: List<String>,
    ) {
        scope.launch(backgroundDispatcher) {
        selectionManager.setSelections(
            slotId = slotId,
            affordanceIds = affordanceIds,
        )
    }
    }

    /**
     * Returns the list of representation objects for all known affordances, regardless of what is
+3 −3
Original line number Diff line number Diff line
@@ -117,7 +117,7 @@ constructor(
     *
     * @return `true` if the affordance was selected successfully; `false` otherwise.
     */
    suspend fun select(slotId: String, affordanceId: String): Boolean {
    fun select(slotId: String, affordanceId: String): Boolean {
        check(isUsingRepository)

        val slots = repository.get().getSlotPickerRepresentations()
@@ -152,7 +152,7 @@ constructor(
     * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
     * the affordance was not on the slot to begin with).
     */
    suspend fun unselect(slotId: String, affordanceId: String?): Boolean {
    fun unselect(slotId: String, affordanceId: String?): Boolean {
        check(isUsingRepository)

        val slots = repository.get().getSlotPickerRepresentations()
@@ -187,7 +187,7 @@ constructor(
    }

    /** Returns affordance IDs indexed by slot ID, for all known slots. */
    suspend fun getSelections(): Map<String, List<String>> {
    fun getSelections(): Map<String, List<String>> {
        check(isUsingRepository)

        val selections = repository.get().getSelections()
+20 −2
Original line number Diff line number Diff line
@@ -33,11 +33,14 @@ import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepo
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -46,6 +49,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -70,8 +75,21 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
            KeyguardQuickAffordanceRepository(
                appContext = context,
                scope = CoroutineScope(IMMEDIATE),
                backgroundDispatcher = IMMEDIATE,
                selectionManager = KeyguardQuickAffordanceSelectionManager(),
                selectionManager =
                    KeyguardQuickAffordanceSelectionManager(
                        userFileManager =
                            mock<UserFileManager>().apply {
                                whenever(
                                        getSharedPreferences(
                                            anyString(),
                                            anyInt(),
                                            anyInt(),
                                        )
                                    )
                                    .thenReturn(FakeSharedPreferences())
                            },
                        userTracker = userTracker,
                    ),
                configs =
                    setOf(
                        FakeKeyguardQuickAffordanceConfig(
Loading