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

Commit 5a574a1d authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Legacy setting syncer.

We need to respect the user's settings when we update quick affordance
selections and vice versa. See the documentation of the class for more
details.

Fix: 256662760
Test: added unit tests. Manually verified that updating settings and
selections updates the other side as expected.

Change-Id: Icd8ff1558fe887ca765ce325b97e7815f124b0db
parent 694e0ab1
Loading
Loading
Loading
Loading
+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,
    )
}
+6 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
@@ -40,6 +41,7 @@ constructor(
    @Application private val appContext: Context,
    @Application private val scope: CoroutineScope,
    private val selectionManager: KeyguardQuickAffordanceSelectionManager,
    legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
    private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
) {
    /**
@@ -83,6 +85,10 @@ constructor(
        }
    }

    init {
        legacySettingSyncer.startSyncing()
    }

    /**
     * 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.
+28 −17
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
@@ -41,6 +42,7 @@ 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.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -71,11 +73,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
        MockitoAnnotations.initMocks(this)

        underTest = KeyguardQuickAffordanceProvider()
        val quickAffordanceRepository =
            KeyguardQuickAffordanceRepository(
                appContext = context,
                scope = CoroutineScope(IMMEDIATE),
                selectionManager =
        val scope = CoroutineScope(IMMEDIATE)
        val selectionManager =
            KeyguardQuickAffordanceSelectionManager(
                context = context,
                userFileManager =
@@ -90,7 +89,12 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
                            .thenReturn(FakeSharedPreferences())
                    },
                userTracker = userTracker,
                    ),
            )
        val quickAffordanceRepository =
            KeyguardQuickAffordanceRepository(
                appContext = context,
                scope = scope,
                selectionManager = selectionManager,
                configs =
                    setOf(
                        FakeKeyguardQuickAffordanceConfig(
@@ -102,6 +106,13 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
                            pickerIconResourceId = 2,
                        ),
                    ),
                legacySettingSyncer =
                    KeyguardQuickAffordanceLegacySettingSyncer(
                        scope = scope,
                        backgroundDispatcher = IMMEDIATE,
                        secureSettings = FakeSettings(),
                        selectionsManager = selectionManager,
                    ),
            )
        underTest.interactor =
            KeyguardQuickAffordanceInteractor(
+191 −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.content.Context
import android.content.res.Resources
import android.provider.Settings
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.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
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.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {

    @Mock private lateinit var sharedPrefs: FakeSharedPreferences

    private lateinit var underTest: KeyguardQuickAffordanceLegacySettingSyncer

    private lateinit var testScope: TestScope
    private lateinit var testDispatcher: TestDispatcher
    private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager
    private lateinit var settings: FakeSettings

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        val context: Context = mock()
        sharedPrefs = FakeSharedPreferences()
        whenever(context.getSharedPreferences(anyString(), any())).thenReturn(sharedPrefs)
        val resources: Resources = mock()
        whenever(resources.getStringArray(R.array.config_keyguardQuickAffordanceDefaults))
            .thenReturn(emptyArray())
        whenever(context.resources).thenReturn(resources)

        testDispatcher = UnconfinedTestDispatcher()
        testScope = TestScope(testDispatcher)
        selectionManager =
            KeyguardQuickAffordanceSelectionManager(
                context = context,
                userFileManager =
                    mock {
                        whenever(
                                getSharedPreferences(
                                    anyString(),
                                    anyInt(),
                                    anyInt(),
                                )
                            )
                            .thenReturn(FakeSharedPreferences())
                    },
                userTracker = FakeUserTracker(),
            )
        settings = FakeSettings()
        settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
        settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET, 0)
        settings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0)

        underTest =
            KeyguardQuickAffordanceLegacySettingSyncer(
                scope = testScope,
                backgroundDispatcher = testDispatcher,
                secureSettings = settings,
                selectionsManager = selectionManager,
            )
    }

    @Test
    fun `Setting a setting selects the affordance`() =
        testScope.runTest {
            val job = underTest.startSyncing()

            settings.putInt(
                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
                1,
            )

            assertThat(
                    selectionManager
                        .getSelections()
                        .getOrDefault(
                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                            emptyList()
                        )
                )
                .contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)

            job.cancel()
        }

    @Test
    fun `Clearing a setting selects the affordance`() =
        testScope.runTest {
            val job = underTest.startSyncing()

            settings.putInt(
                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
                1,
            )
            settings.putInt(
                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
                0,
            )

            assertThat(
                    selectionManager
                        .getSelections()
                        .getOrDefault(
                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                            emptyList()
                        )
                )
                .doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)

            job.cancel()
        }

    @Test
    fun `Selecting an affordance sets its setting`() =
        testScope.runTest {
            val job = underTest.startSyncing()

            selectionManager.setSelections(
                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
            )

            advanceUntilIdle()
            assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(1)

            job.cancel()
        }

    @Test
    fun `Unselecting an affordance clears its setting`() =
        testScope.runTest {
            val job = underTest.startSyncing()

            selectionManager.setSelections(
                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
            )
            selectionManager.setSelections(
                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                emptyList()
            )

            assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0)

            job.cancel()
        }
}
+28 −16
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
@@ -30,6 +31,7 @@ import com.android.systemui.settings.UserFileManager
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -58,11 +60,8 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
    fun setUp() {
        config1 = FakeKeyguardQuickAffordanceConfig("built_in:1")
        config2 = FakeKeyguardQuickAffordanceConfig("built_in:2")
        underTest =
            KeyguardQuickAffordanceRepository(
                appContext = context,
                scope = CoroutineScope(IMMEDIATE),
                selectionManager =
        val scope = CoroutineScope(IMMEDIATE)
        val selectionManager =
            KeyguardQuickAffordanceSelectionManager(
                context = context,
                userFileManager =
@@ -77,6 +76,19 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
                            .thenReturn(FakeSharedPreferences())
                    },
                userTracker = FakeUserTracker(),
            )

        underTest =
            KeyguardQuickAffordanceRepository(
                appContext = context,
                scope = scope,
                selectionManager = selectionManager,
                legacySettingSyncer =
                    KeyguardQuickAffordanceLegacySettingSyncer(
                        scope = scope,
                        backgroundDispatcher = IMMEDIATE,
                        secureSettings = FakeSettings(),
                        selectionsManager = selectionManager,
                    ),
                configs = setOf(config1, config2),
            )
Loading