From d29f47f3fe072db18ec440c3045045101bbcb67e Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Fri, 4 Nov 2022 11:39:53 -0700 Subject: [PATCH 1/4] 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 --- .../KeyguardQuickAffordanceProvider.kt | 9 +- ...KeyguardQuickAffordanceSelectionManager.kt | 89 +++++-- .../KeyguardQuickAffordanceRepository.kt | 18 +- .../KeyguardQuickAffordanceInteractor.kt | 6 +- .../KeyguardQuickAffordanceProviderTest.kt | 22 +- ...uardQuickAffordanceSelectionManagerTest.kt | 252 +++++++++++++----- .../KeyguardQuickAffordanceRepositoryTest.kt | 24 +- ...ckAffordanceInteractorParameterizedTest.kt | 26 +- .../KeyguardQuickAffordanceInteractorTest.kt | 21 +- .../KeyguardBottomAreaViewModelTest.kt | 20 +- 10 files changed, 365 insertions(+), 122 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 0f4581ce3e616..82209637e9fa7 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 { @@ -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") 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..d89ac37322b42 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,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, +) { - // TODO(b/254858695): implement a persistence layer (database). - private val _selections = MutableStateFlow>>(emptyMap()) + private val sharedPrefs: SharedPreferences + get() = + userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + + private val userId: Flow = 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) + + awaitClose { userTracker.removeCallback(callback) } + } /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ - val selections: Flow>> = _selections.asStateFlow() + val selections: Flow>> = + 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> { - return _selections.value + fun getSelections(): Map> { + 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, ) { - // 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 = "," } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index 9fda98c5f7c2c..a1fdbe4a90126 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -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 { + fun getSelections(slotId: String): List { 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> { + fun getSelections(): Map> { return selectionManager.getSelections() } @@ -112,12 +108,10 @@ constructor( slotId: String, affordanceIds: List, ) { - scope.launch(backgroundDispatcher) { - selectionManager.setSelections( - slotId = slotId, - affordanceIds = affordanceIds, - ) - } + selectionManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIds, + ) } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 92caa89bb0e8b..88fed18574cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -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> { + fun getSelections(): Map> { check(isUsingRepository) val selections = repository.get().getSelections() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index ed08849fe70c0..2d0f9e264f692 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -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().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ), configs = setOf( FakeKeyguardQuickAffordanceConfig( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index d2422ad7b53b3..b8bef1a9d6f36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -17,111 +17,215 @@ 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.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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 @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { + @Mock private lateinit var userFileManager: UserFileManager + private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + private lateinit var userTracker: FakeUserTracker + private lateinit var sharedPrefs: MutableMap + @Before fun setUp() { - underTest = KeyguardQuickAffordanceSelectionManager() + MockitoAnnotations.initMocks(this) + sharedPrefs = mutableMapOf() + whenever(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())).thenAnswer { + val userId = it.arguments[2] as Int + sharedPrefs.getOrPut(userId) { FakeSharedPreferences() } + } + userTracker = FakeUserTracker() + + underTest = + KeyguardQuickAffordanceSelectionManager( + userFileManager = userFileManager, + userTracker = userTracker, + ) } @Test - fun setSelections() = - runBlocking(IMMEDIATE) { - var affordanceIdsBySlotId: Map>? = null - val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this) - val slotId1 = "slot1" - val slotId2 = "slot2" - val affordanceId1 = "affordance1" - val affordanceId2 = "affordance2" - val affordanceId3 = "affordance3" + fun setSelections() = runTest { + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1), + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1), + ), + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(affordanceId2), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1), + slotId2 to listOf(affordanceId2), ) - assertSelections( - affordanceIdsBySlotId, + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1, affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(), + ) + ) + + job.cancel() + } + + @Test + fun `remembers selections by user`() = runTest { + val slot1 = "slot_1" + val slot2 = "slot_2" + val affordance1 = "affordance_1" + val affordance2 = "affordance_2" + val affordance3 = "affordance_3" + + val affordanceIdsBySlotId = mutableListOf>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + val userInfos = + listOf( + UserInfo(/* id= */ 0, "zero", /* flags= */ 0), + UserInfo(/* id= */ 1, "one", /* flags= */ 0), + ) + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + underTest.setSelections( + slotId = slot1, + affordanceIds = listOf(affordance1), + ) + underTest.setSelections( + slotId = slot2, + affordanceIds = listOf(affordance2), + ) + + // Switch to user 1 + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 1, + ) + // We never set selections on user 1, so it should be empty. + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = emptyMap(), + ) + // Now, let's set selections on user 1. + underTest.setSelections( + slotId = slot1, + affordanceIds = listOf(affordance2), + ) + underTest.setSelections( + slotId = slot2, + affordanceIds = listOf(affordance3), + ) + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = mapOf( - slotId1 to listOf(affordanceId1), + slot1 to listOf(affordance2), + slot2 to listOf(affordance3), ), - ) + ) - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(affordanceId2), - ) - assertSelections( - affordanceIdsBySlotId, + // Switch back to user 0. + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + // Assert that we still remember the old selections for user 0. + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = mapOf( - slotId1 to listOf(affordanceId1), - slotId2 to listOf(affordanceId2), - ) - ) + slot1 to listOf(affordance1), + slot2 to listOf(affordance2), + ), + ) - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1, affordanceId3), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId1, affordanceId3), - slotId2 to listOf(affordanceId2), - ) - ) + job.cancel() + } - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId3), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(affordanceId2), - ) - ) - - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(), - ) - ) - - job.cancel() - } - - private suspend fun assertSelections( + private fun assertSelections( observed: Map>?, expected: Map>, ) { assertThat(underTest.getSelections()).isEqualTo(expected) assertThat(observed).isEqualTo(expected) } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index dceb492af2ed0..a95f788b1ee91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -25,6 +25,11 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc 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 com.android.systemui.settings.FakeUserTracker +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.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,6 +41,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 @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -55,8 +62,21 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { KeyguardQuickAffordanceRepository( appContext = context, scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + selectionManager = + KeyguardQuickAffordanceSelectionManager( + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = FakeUserTracker(), + ), configs = setOf(config1, config2), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 737f242247273..3e37459d8eeb9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -36,8 +36,11 @@ import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepo import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController +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 @@ -50,6 +53,8 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.same import org.mockito.Mock @@ -201,7 +206,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller @Mock private lateinit var expandable: Expandable @@ -214,12 +218,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false @JvmField @Parameter(4) var startActivity: Boolean = false private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig + private lateinit var userTracker: UserTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(expandable.activityLaunchController()).thenReturn(animationController) + userTracker = FakeUserTracker() homeControls = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) val quickAccessWallet = @@ -232,8 +238,21 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { KeyguardQuickAffordanceRepository( appContext = context, scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + selectionManager = + KeyguardQuickAffordanceSelectionManager( + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) underTest = @@ -319,7 +338,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { needStrongAuthAfterBoot: Boolean = true, keyguardIsUnlocked: Boolean = false, ) { - whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(any())) .thenReturn( if (needStrongAuthAfterBoot) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index ffcf832b95aec..f887bf93de8d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -35,9 +35,11 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker 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 @@ -53,6 +55,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.MockitoAnnotations @@ -94,8 +98,21 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceRepository( appContext = context, scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + selectionManager = + KeyguardQuickAffordanceSelectionManager( + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) featureFlags = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index fa2ac46f46d20..03e6ccb6fabe2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -38,8 +38,10 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -56,6 +58,7 @@ 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 import org.mockito.Mockito.verifyZeroInteractions @@ -119,8 +122,21 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { KeyguardQuickAffordanceRepository( appContext = context, scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + selectionManager = + KeyguardQuickAffordanceSelectionManager( + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ), configs = setOf( homeControlsQuickAffordanceConfig, From 694e0ab10fe0ea76fd2370094fa9b25211ec2c94 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Fri, 4 Nov 2022 15:35:25 -0700 Subject: [PATCH 2/4] 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 bottom_end:home,wallet 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 --- packages/SystemUI/res/values/config.xml | 9 ++ ...KeyguardQuickAffordanceSelectionManager.kt | 55 ++++++++--- .../KeyguardQuickAffordanceProviderTest.kt | 1 + ...uardQuickAffordanceSelectionManagerTest.kt | 99 ++++++++++++++++++- .../KeyguardQuickAffordanceRepositoryTest.kt | 1 + ...ckAffordanceInteractorParameterizedTest.kt | 1 + .../KeyguardQuickAffordanceInteractorTest.kt | 1 + .../KeyguardBottomAreaViewModelTest.kt | 1 + 8 files changed, 156 insertions(+), 12 deletions(-) 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/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index d89ac37322b42..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 @@ -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> 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>> = @@ -86,17 +100,35 @@ constructor( */ fun getSelections(): Map> { 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, ) { 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 = "," } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index 2d0f9e264f692..5228e1774edf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -77,6 +77,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index b8bef1a9d6f36..d8ee9f113d33c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -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()) val affordanceIdsBySlotId = mutableListOf>>() 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>>() + 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>>() + 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>>() + 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>?, expected: Map>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index a95f788b1ee91..e56e1b93dcfb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -64,6 +64,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 3e37459d8eeb9..3b1cbb1bd6203 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -240,6 +240,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index f887bf93de8d1..1374c43ebe1ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -100,6 +100,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 03e6ccb6fabe2..abe8a304a4ef7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -124,6 +124,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { scope = CoroutineScope(IMMEDIATE), selectionManager = KeyguardQuickAffordanceSelectionManager( + context = context, userFileManager = mock().apply { whenever( From 5a574a1db4bacd9bffcfb8f8be30082fa9627a92 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 7 Nov 2022 12:17:52 -0800 Subject: [PATCH 3/4] 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 --- ...guardQuickAffordanceLegacySettingSyncer.kt | 214 ++++++++++++++++++ .../KeyguardQuickAffordanceRepository.kt | 6 + .../KeyguardQuickAffordanceProviderTest.kt | 45 ++-- ...dQuickAffordanceLegacySettingSyncerTest.kt | 191 ++++++++++++++++ .../KeyguardQuickAffordanceRepositoryTest.kt | 44 ++-- ...ckAffordanceInteractorParameterizedTest.kt | 43 ++-- .../KeyguardQuickAffordanceInteractorTest.kt | 43 ++-- .../KeyguardBottomAreaViewModelTest.kt | 43 ++-- 8 files changed, 548 insertions(+), 81 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt 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/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index a1fdbe4a90126..533b3abf4fb65 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -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. diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index 5228e1774edf6..8395f02cbc41e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -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,26 +73,28 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) underTest = KeyguardQuickAffordanceProvider() + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - selectionManager = - KeyguardQuickAffordanceSelectionManager( - context = context, - userFileManager = - mock().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) - .thenReturn(FakeSharedPreferences()) - }, - userTracker = userTracker, - ), + 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( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt new file mode 100644 index 0000000000000..8ef921eaa50ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -0,0 +1,191 @@ +/* + * 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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index e56e1b93dcfb6..d8a360567a07f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -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,25 +60,35 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { fun setUp() { config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = FakeUserTracker(), + ) + underTest = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - selectionManager = - KeyguardQuickAffordanceSelectionManager( - context = context, - userFileManager = - mock().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) - .thenReturn(FakeSharedPreferences()) - }, - userTracker = FakeUserTracker(), + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, ), configs = setOf(config1, config2), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 3b1cbb1bd6203..1e1d3f19d83cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys 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.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository @@ -44,6 +45,7 @@ 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runBlockingTest @@ -234,25 +236,34 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ) val qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - selectionManager = - KeyguardQuickAffordanceSelectionManager( - context = context, - userFileManager = - mock().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) - .thenReturn(FakeSharedPreferences()) - }, - userTracker = userTracker, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 1374c43ebe1ba..c47e6f52c5962 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys 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.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository @@ -42,6 +43,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 @@ -93,26 +95,35 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - selectionManager = - KeyguardQuickAffordanceSelectionManager( - context = context, - userFileManager = - mock().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) - .thenReturn(FakeSharedPreferences()) - }, - userTracker = userTracker, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index abe8a304a4ef7..ecc63ecc879d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys 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.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController 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.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min @@ -118,25 +120,34 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = CoroutineScope(IMMEDIATE), - selectionManager = - KeyguardQuickAffordanceSelectionManager( - context = context, - userFileManager = - mock().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) - .thenReturn(FakeSharedPreferences()) - }, - userTracker = userTracker, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, ), configs = setOf( From ed7dd1dc580baee25e5bdd53f7dd8571d0d21f7b Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 3 Nov 2022 11:17:16 -0700 Subject: [PATCH 4/4] Exposes feature flag to wallpaper picker. Wallpaper picker, through the shared library, can now query the feature flag to know whether the customizable quick affordance feature is enabled or not. Bug: 254857639 Test: manually verified that turning the flag on and off on the system UI side is reflected to WPPG with a visible or gone view and that selecting affordances from WPPG still works Change-Id: I2ea2baa4dbe935f1173648d673a64267ba8b0de2 --- ...KeyguardQuickAffordanceProviderContract.kt | 26 +++++++++++++++ .../KeyguardQuickAffordanceProvider.kt | 32 +++++++++++++++++++ .../KeyguardQuickAffordanceInteractor.kt | 11 +++++++ .../shared/model/KeyguardPickerFlag.kt | 24 ++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt 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 82209637e9fa7..1f1ed007fca0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -56,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 { @@ -76,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 } @@ -85,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 } @@ -114,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 } } @@ -248,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?, @@ -290,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/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 88fed18574cd6..45eb6f5012874 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -28,11 +28,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry +import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy @@ -314,6 +316,15 @@ constructor( return repository.get().getSlotPickerRepresentations() } + fun getPickerFlags(): List { + return listOf( + KeyguardPickerFlag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES), + ) + ) + } + companion object { private const val TAG = "KeyguardQuickAffordanceInteractor" private const val DELIMITER = "::" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt new file mode 100644 index 0000000000000..a7a5957cd5275 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt @@ -0,0 +1,24 @@ +/* + * 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.shared.model + +/** Represents a flag that's consumed by the settings or wallpaper picker app. */ +data class KeyguardPickerFlag( + val name: String, + val value: Boolean, +)