Add Mute custom quick affordance
Upon silencing or using the Mute shortcut, the user can go back to the previous ringer mode. This previous state is cached in SharedPreferences, so that it survives reboots. This also works with secondary users. Bug: 256687297 Test: MuteQuickAffordanceConfigTest.kt Test: MuteQuickAffordanceCoreStartableTest.kt Change-Id: I20aa5375e1384f0656cc4da9900bf4d8cbad3d1a
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user