Merge "Add Mute custom quick affordance" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
d886dac36c
@@ -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