diff options
| author | 2023-01-30 19:21:55 +0000 | |
|---|---|---|
| committer | 2023-01-30 19:21:55 +0000 | |
| commit | d886dac36c32d04479f47f7a0631d2f0998564d7 (patch) | |
| tree | 4b9291b291674263cfbe91a7fa74ab3f6365eeca | |
| parent | 3944b8312388c06901e0c5434e3462150bd85f65 (diff) | |
| parent | 0b185b890780305c74f0cbe4300c13de846971de (diff) | |
Merge "Add Mute custom quick affordance" into tm-qpr-dev
10 files changed, 612 insertions, 10 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index ef07e996667c..2dfcf70177fe 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.dreams.DreamMonitor import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable import com.android.systemui.log.SessionTracker import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI import com.android.systemui.media.RingtonePlayer @@ -288,6 +289,14 @@ abstract class SystemUICoreStartableModule { @ClassKey(StylusUsiPowerStartable::class) abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable + /** Inject into MuteQuickAffordanceCoreStartable*/ + @Binds + @IntoMap + @ClassKey(MuteQuickAffordanceCoreStartable::class) + abstract fun bindMuteQuickAffordanceCoreStartable( + sysui: MuteQuickAffordanceCoreStartable + ): CoreStartable + /**Inject into DreamMonitor */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index 76c2430e37ad..80675d373b8e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -29,6 +29,7 @@ object BuiltInKeyguardQuickAffordanceKeys { const val DO_NOT_DISTURB = "do_not_disturb" const val FLASHLIGHT = "flashlight" const val HOME_CONTROLS = "home" + const val MUTE = "mute" const val QR_CODE_SCANNER = "qr_code_scanner" const val QUICK_ACCESS_WALLET = "wallet" const val VIDEO_CAMERA = "video_camera" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index a1cce5c670ba..45561959a7df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -33,12 +33,13 @@ interface KeyguardDataQuickAffordanceModule { @Provides @ElementsIntoSet fun quickAffordanceConfigs( + camera: CameraQuickAffordanceConfig, doNotDisturb: DoNotDisturbQuickAffordanceConfig, flashlight: FlashlightQuickAffordanceConfig, home: HomeControlsKeyguardQuickAffordanceConfig, + mute: MuteQuickAffordanceConfig, quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, - camera: CameraQuickAffordanceConfig, videoCamera: VideoCameraQuickAffordanceConfig, ): Set<KeyguardQuickAffordanceConfig> { return setOf( @@ -46,6 +47,7 @@ interface KeyguardDataQuickAffordanceModule { doNotDisturb, flashlight, home, + mute, quickAccessWallet, qrCodeScanner, videoCamera, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt new file mode 100644 index 000000000000..d085db9a1eda --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt @@ -0,0 +1,144 @@ +/* + * 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.media.AudioManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.RingerModeTracker +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +@SysUISingleton +class MuteQuickAffordanceConfig @Inject constructor( + context: Context, + private val userTracker: UserTracker, + private val userFileManager: UserFileManager, + private val ringerModeTracker: RingerModeTracker, + private val audioManager: AudioManager, +) : KeyguardQuickAffordanceConfig { + + private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE + + override val key: String = BuiltInKeyguardQuickAffordanceKeys.MUTE + + override val pickerName: String = context.getString(R.string.volume_ringer_status_silent) + + override val pickerIconResourceId: Int = R.drawable.ic_notifications_silence + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = + ringerModeTracker.ringerModeInternal.asFlow() + .onStart { emit(getLastNonSilentRingerMode()) } + .distinctUntilChanged() + .onEach { mode -> + // only remember last non-SILENT ringer mode + if (mode != null && mode != AudioManager.RINGER_MODE_SILENT) { + previousNonSilentMode = mode + } + } + .map { mode -> + val (activationState, contentDescriptionRes) = when { + audioManager.isVolumeFixed -> + ActivationState.NotSupported to + R.string.volume_ringer_hint_mute + mode == AudioManager.RINGER_MODE_SILENT -> + ActivationState.Active to + R.string.volume_ringer_hint_mute + else -> + ActivationState.Inactive to + R.string.volume_ringer_hint_unmute + } + + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.ic_notifications_silence, + ContentDescription.Resource(contentDescriptionRes), + ), + activationState, + ) + } + + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + val newRingerMode: Int + val currentRingerMode = + ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE + if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) { + newRingerMode = previousNonSilentMode + } else { + previousNonSilentMode = currentRingerMode + newRingerMode = AudioManager.RINGER_MODE_SILENT + } + + if (currentRingerMode != newRingerMode) { + audioManager.ringerModeInternal = newRingerMode + } + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + + override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = + if (audioManager.isVolumeFixed) { + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice + } else { + KeyguardQuickAffordanceConfig.PickerScreenState.Default() + } + + /** + * Gets the last non-silent ringer mode from shared-preferences if it exists. This is + * cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected + */ + private fun getLastNonSilentRingerMode(): Int = + userFileManager.getSharedPreferences( + MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId + ).getInt( + LAST_NON_SILENT_RINGER_MODE_KEY, + ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE + ) + + private fun <T> LiveData<T>.asFlow(): Flow<T?> = + conflatedCallbackFlow { + val observer = Observer { value: T -> trySend(value) } + observeForever(observer) + send(value) + awaitClose { removeObserver(observer) } + } + + companion object { + const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode" + const val MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME = "quick_affordance_mute_ringer_mode_cache" + private const val DEFAULT_LAST_NON_SILENT_VALUE = AudioManager.RINGER_MODE_NORMAL + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt new file mode 100644 index 000000000000..12a6310ccc85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 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.media.AudioManager +import androidx.lifecycle.Observer +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.RingerModeTracker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +/** + * Store previous non-silent Ringer Mode into shared prefs to be used for Mute Lockscreen Shortcut + */ +@SysUISingleton +class MuteQuickAffordanceCoreStartable @Inject constructor( + private val featureFlags: FeatureFlags, + private val userTracker: UserTracker, + private val ringerModeTracker: RingerModeTracker, + private val userFileManager: UserFileManager, + private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository, + @Application private val coroutineScope: CoroutineScope, +) : CoreStartable { + + private val observer = Observer(this::updateLastNonSilentRingerMode) + + override fun start() { + if (!featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)) return + + // only listen to ringerModeInternal changes when Mute is one of the selected affordances + keyguardQuickAffordanceRepository + .selections + .map { selections -> + // determines if Mute is selected in any lockscreen shortcut position + val muteSelected: Boolean = selections.values.any { configList -> + configList.any { config -> + config.key == BuiltInKeyguardQuickAffordanceKeys.MUTE + } + } + if (muteSelected) { + ringerModeTracker.ringerModeInternal.observeForever(observer) + } else { + ringerModeTracker.ringerModeInternal.removeObserver(observer) + } + } + .launchIn(coroutineScope) + } + + private fun updateLastNonSilentRingerMode(lastRingerMode: Int) { + if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) { + userFileManager.getSharedPreferences( + MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId + ) + .edit() + .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode) + .apply() + } + } +}
\ No newline at end of file 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 2b2b9d0703fa..8ece3183fd56 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 @@ -146,7 +146,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. */ - fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { + fun getCurrentSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList()) return configs.filter { selections.contains(it.key) } } @@ -155,7 +155,7 @@ constructor( * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs * are sorted in descending priority order. */ - fun getSelections(): Map<String, List<String>> { + fun getCurrentSelections(): Map<String, List<String>> { return selectionManager.value.getSelections() } @@ -217,7 +217,7 @@ constructor( private inner class Dumpster : Dumpable { override fun dump(pw: PrintWriter, args: Array<out String>) { val slotPickerRepresentations = getSlotPickerRepresentations() - val selectionsBySlotId = getSelections() + val selectionsBySlotId = getCurrentSelections() pw.println("Slots & selections:") slotPickerRepresentations.forEach { slotPickerRepresentation -> val slotId = slotPickerRepresentation.id 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 9ddc575307b5..57c3b3143c62 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 @@ -154,7 +154,7 @@ constructor( val slots = repository.get().getSlotPickerRepresentations() val slot = slots.find { it.id == slotId } ?: return false val selections = - repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList() val alreadySelected = selections.remove(affordanceId) if (!alreadySelected) { while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) { @@ -193,7 +193,7 @@ constructor( if (affordanceId.isNullOrEmpty()) { return if ( - repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty() + repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty() ) { false } else { @@ -203,7 +203,7 @@ constructor( } val selections = - repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList() return if (selections.remove(affordanceId)) { repository .get() @@ -220,7 +220,7 @@ constructor( /** Returns affordance IDs indexed by slot ID, for all known slots. */ suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> { val slots = repository.get().getSlotPickerRepresentations() - val selections = repository.get().getSelections() + val selections = repository.get().getCurrentSelections() val affordanceById = getAffordancePickerRepresentations().associateBy { affordance -> affordance.id } return slots.associate { slot -> diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..a3740d88e1a9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt @@ -0,0 +1,140 @@ +/* + * 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.media.AudioManager +import androidx.lifecycle.LiveData +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.RingerModeTracker +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class MuteQuickAffordanceConfigTest : SysuiTestCase() { + + private lateinit var underTest: MuteQuickAffordanceConfig + @Mock + private lateinit var ringerModeTracker: RingerModeTracker + @Mock + private lateinit var audioManager: AudioManager + @Mock + private lateinit var userTracker: UserTracker + @Mock + private lateinit var userFileManager: UserFileManager + + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + testScope = TestScope() + + whenever(userTracker.userContext).thenReturn(context) + whenever(userFileManager.getSharedPreferences(any(), any(), any())) + .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE)) + + underTest = MuteQuickAffordanceConfig( + context, + userTracker, + userFileManager, + ringerModeTracker, + audioManager + ) + } + + @Test + fun `picker state - volume fixed - not available`() = testScope.runTest { + //given + whenever(audioManager.isVolumeFixed).thenReturn(true) + + //when + val result = underTest.getPickerScreenState() + + //then + assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result) + } + + @Test + fun `picker state - volume not fixed - available`() = testScope.runTest { + //given + whenever(audioManager.isVolumeFixed).thenReturn(false) + + //when + val result = underTest.getPickerScreenState() + + //then + assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result) + } + + @Test + fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() { + //given + val ringerModeCapture = argumentCaptor<Int>() + val ringerModeInternal = mock<LiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL) + underTest.onTriggered(null) + whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT) + + //when + val result = underTest.onTriggered(null) + verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture() + + //then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value) + } + + @Test + fun `triggered - state is not SILENT - move to SILENT ringer`() { + //given + val ringerModeCapture = argumentCaptor<Int>() + val ringerModeInternal = mock<LiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL) + + //when + val result = underTest.onTriggered(null) + verify(audioManager).ringerModeInternal = ringerModeCapture.capture() + + //then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt new file mode 100644 index 000000000000..26601b63e02d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2023 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.media.AudioManager +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.RingerModeTracker +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() { + + @Mock + private lateinit var featureFlags: FeatureFlags + @Mock + private lateinit var userTracker: UserTracker + @Mock + private lateinit var ringerModeTracker: RingerModeTracker + @Mock + private lateinit var userFileManager: UserFileManager + @Mock + private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository + + private lateinit var testScope: TestScope + + private lateinit var underTest: MuteQuickAffordanceCoreStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(true) + + val config: KeyguardQuickAffordanceConfig = mock() + whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE) + + val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config))) + whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission) + + testScope = TestScope() + + underTest = MuteQuickAffordanceCoreStartable( + featureFlags, + userTracker, + ringerModeTracker, + userFileManager, + keyguardQuickAffordanceRepository, + testScope, + ) + } + + @Test + fun `feature flag is OFF - do nothing with keyguardQuickAffordanceRepository`() = testScope.runTest { + //given + whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(false) + + //when + underTest.start() + + //then + verifyZeroInteractions(keyguardQuickAffordanceRepository) + coroutineContext.cancelChildren() + } + + @Test + fun `feature flag is ON - call to keyguardQuickAffordanceRepository`() = testScope.runTest { + //given + val ringerModeInternal = mock<MutableLiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + + //when + underTest.start() + runCurrent() + + //then + verify(keyguardQuickAffordanceRepository).selections + coroutineContext.cancelChildren() + } + + @Test + fun `ringer mode is changed to SILENT - do not save to shared preferences`() = testScope.runTest { + //given + val ringerModeInternal = mock<MutableLiveData<Int>>() + val observerCaptor = argumentCaptor<Observer<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + + //when + underTest.start() + runCurrent() + verify(ringerModeInternal).observeForever(observerCaptor.capture()) + observerCaptor.value.onChanged(AudioManager.RINGER_MODE_SILENT) + + //then + verifyZeroInteractions(userFileManager) + coroutineContext.cancelChildren() + } + + @Test + fun `ringerModeInternal changes to something not SILENT - is set in sharedpreferences`() = testScope.runTest { + //given + val newRingerMode = 99 + val observerCaptor = argumentCaptor<Observer<Int>>() + val ringerModeInternal = mock<MutableLiveData<Int>>() + val sharedPrefs = context.getSharedPreferences("quick_affordance_mute_ringer_mode_cache_test", Context.MODE_PRIVATE) + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + whenever( + userFileManager.getSharedPreferences(eq("quick_affordance_mute_ringer_mode_cache"), any(), any()) + ).thenReturn(sharedPrefs) + + //when + underTest.start() + runCurrent() + verify(ringerModeInternal).observeForever(observerCaptor.capture()) + observerCaptor.value.onChanged(newRingerMode) + val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1) + + //then + assertEquals(newRingerMode, result) + coroutineContext.cancelChildren() + } + + @Test + fun `MUTE is in selections - observe ringerModeInternal`() = testScope.runTest { + //given + val ringerModeInternal = mock<MutableLiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + + //when + underTest.start() + runCurrent() + + //then + verify(ringerModeInternal).observeForever(any()) + coroutineContext.cancelChildren() + } + + @Test + fun `MUTE is in selections 2x - observe ringerModeInternal`() = testScope.runTest { + //given + val config: KeyguardQuickAffordanceConfig = mock() + whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE) + val emission = MutableStateFlow(mapOf("testKey" to listOf(config), "testkey2" to listOf(config))) + whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission) + val ringerModeInternal = mock<MutableLiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + + //when + underTest.start() + runCurrent() + + //then + verify(ringerModeInternal).observeForever(any()) + coroutineContext.cancelChildren() + } + + @Test + fun `MUTE is not in selections - stop observing ringerModeInternal`() = testScope.runTest { + //given + val config: KeyguardQuickAffordanceConfig = mock() + whenever(config.key).thenReturn("notmutequickaffordance") + val emission = MutableStateFlow(mapOf("testKey" to listOf(config))) + whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission) + val ringerModeInternal = mock<MutableLiveData<Int>>() + whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal) + + //when + underTest.start() + runCurrent() + + //then + verify(ringerModeInternal).removeObserver(any()) + coroutineContext.cancelChildren() + } +}
\ No newline at end of file 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 b071a028865d..6099f011a90d 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 @@ -275,10 +275,10 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { expected: Map<String, List<KeyguardQuickAffordanceConfig>>, ) { assertThat(observed).isEqualTo(expected) - assertThat(underTest.getSelections()) + assertThat(underTest.getCurrentSelections()) .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } }) expected.forEach { (slotId, configs) -> - assertThat(underTest.getSelections(slotId)).isEqualTo(configs) + assertThat(underTest.getCurrentSelections(slotId)).isEqualTo(configs) } } |