Merge "Add Mute custom quick affordance" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e8524eaa14
@@ -30,6 +30,7 @@ import com.android.systemui.dagger.qualifiers.PerUser
|
|||||||
import com.android.systemui.globalactions.GlobalActionsComponent
|
import com.android.systemui.globalactions.GlobalActionsComponent
|
||||||
import com.android.systemui.keyboard.KeyboardUI
|
import com.android.systemui.keyboard.KeyboardUI
|
||||||
import com.android.systemui.keyguard.KeyguardViewMediator
|
import com.android.systemui.keyguard.KeyguardViewMediator
|
||||||
|
import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable
|
||||||
import com.android.systemui.log.SessionTracker
|
import com.android.systemui.log.SessionTracker
|
||||||
import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
|
import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
|
||||||
import com.android.systemui.media.RingtonePlayer
|
import com.android.systemui.media.RingtonePlayer
|
||||||
@@ -286,4 +287,12 @@ abstract class SystemUICoreStartableModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@ClassKey(StylusUsiPowerStartable::class)
|
@ClassKey(StylusUsiPowerStartable::class)
|
||||||
abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable
|
abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable
|
||||||
|
|
||||||
|
/** Inject into MuteQuickAffordanceCoreStartable*/
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ClassKey(MuteQuickAffordanceCoreStartable::class)
|
||||||
|
abstract fun bindMuteQuickAffordanceCoreStartable(
|
||||||
|
sysui: MuteQuickAffordanceCoreStartable
|
||||||
|
): CoreStartable
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ object BuiltInKeyguardQuickAffordanceKeys {
|
|||||||
const val DO_NOT_DISTURB = "do_not_disturb"
|
const val DO_NOT_DISTURB = "do_not_disturb"
|
||||||
const val FLASHLIGHT = "flashlight"
|
const val FLASHLIGHT = "flashlight"
|
||||||
const val HOME_CONTROLS = "home"
|
const val HOME_CONTROLS = "home"
|
||||||
|
const val MUTE = "mute"
|
||||||
const val QR_CODE_SCANNER = "qr_code_scanner"
|
const val QR_CODE_SCANNER = "qr_code_scanner"
|
||||||
const val QUICK_ACCESS_WALLET = "wallet"
|
const val QUICK_ACCESS_WALLET = "wallet"
|
||||||
const val VIDEO_CAMERA = "video_camera"
|
const val VIDEO_CAMERA = "video_camera"
|
||||||
|
|||||||
@@ -33,12 +33,13 @@ interface KeyguardDataQuickAffordanceModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@ElementsIntoSet
|
@ElementsIntoSet
|
||||||
fun quickAffordanceConfigs(
|
fun quickAffordanceConfigs(
|
||||||
|
camera: CameraQuickAffordanceConfig,
|
||||||
doNotDisturb: DoNotDisturbQuickAffordanceConfig,
|
doNotDisturb: DoNotDisturbQuickAffordanceConfig,
|
||||||
flashlight: FlashlightQuickAffordanceConfig,
|
flashlight: FlashlightQuickAffordanceConfig,
|
||||||
home: HomeControlsKeyguardQuickAffordanceConfig,
|
home: HomeControlsKeyguardQuickAffordanceConfig,
|
||||||
|
mute: MuteQuickAffordanceConfig,
|
||||||
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
|
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
|
||||||
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
|
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
|
||||||
camera: CameraQuickAffordanceConfig,
|
|
||||||
videoCamera: VideoCameraQuickAffordanceConfig,
|
videoCamera: VideoCameraQuickAffordanceConfig,
|
||||||
): Set<KeyguardQuickAffordanceConfig> {
|
): Set<KeyguardQuickAffordanceConfig> {
|
||||||
return setOf(
|
return setOf(
|
||||||
@@ -46,6 +47,7 @@ interface KeyguardDataQuickAffordanceModule {
|
|||||||
doNotDisturb,
|
doNotDisturb,
|
||||||
flashlight,
|
flashlight,
|
||||||
home,
|
home,
|
||||||
|
mute,
|
||||||
quickAccessWallet,
|
quickAccessWallet,
|
||||||
qrCodeScanner,
|
qrCodeScanner,
|
||||||
videoCamera,
|
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
|
* 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.
|
* 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())
|
val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
|
||||||
return configs.filter { selections.contains(it.key) }
|
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
|
* Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
|
||||||
* are sorted in descending priority order.
|
* are sorted in descending priority order.
|
||||||
*/
|
*/
|
||||||
fun getSelections(): Map<String, List<String>> {
|
fun getCurrentSelections(): Map<String, List<String>> {
|
||||||
return selectionManager.value.getSelections()
|
return selectionManager.value.getSelections()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ constructor(
|
|||||||
private inner class Dumpster : Dumpable {
|
private inner class Dumpster : Dumpable {
|
||||||
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
||||||
val slotPickerRepresentations = getSlotPickerRepresentations()
|
val slotPickerRepresentations = getSlotPickerRepresentations()
|
||||||
val selectionsBySlotId = getSelections()
|
val selectionsBySlotId = getCurrentSelections()
|
||||||
pw.println("Slots & selections:")
|
pw.println("Slots & selections:")
|
||||||
slotPickerRepresentations.forEach { slotPickerRepresentation ->
|
slotPickerRepresentations.forEach { slotPickerRepresentation ->
|
||||||
val slotId = slotPickerRepresentation.id
|
val slotId = slotPickerRepresentation.id
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ constructor(
|
|||||||
val slots = repository.get().getSlotPickerRepresentations()
|
val slots = repository.get().getSlotPickerRepresentations()
|
||||||
val slot = slots.find { it.id == slotId } ?: return false
|
val slot = slots.find { it.id == slotId } ?: return false
|
||||||
val selections =
|
val selections =
|
||||||
repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
|
repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
|
||||||
val alreadySelected = selections.remove(affordanceId)
|
val alreadySelected = selections.remove(affordanceId)
|
||||||
if (!alreadySelected) {
|
if (!alreadySelected) {
|
||||||
while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
|
while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
|
||||||
@@ -193,7 +193,7 @@ constructor(
|
|||||||
|
|
||||||
if (affordanceId.isNullOrEmpty()) {
|
if (affordanceId.isNullOrEmpty()) {
|
||||||
return if (
|
return if (
|
||||||
repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
|
repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty()
|
||||||
) {
|
) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
@@ -203,7 +203,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selections =
|
val selections =
|
||||||
repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
|
repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
|
||||||
return if (selections.remove(affordanceId)) {
|
return if (selections.remove(affordanceId)) {
|
||||||
repository
|
repository
|
||||||
.get()
|
.get()
|
||||||
@@ -220,7 +220,7 @@ constructor(
|
|||||||
/** Returns affordance IDs indexed by slot ID, for all known slots. */
|
/** Returns affordance IDs indexed by slot ID, for all known slots. */
|
||||||
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
|
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
|
||||||
val slots = repository.get().getSlotPickerRepresentations()
|
val slots = repository.get().getSlotPickerRepresentations()
|
||||||
val selections = repository.get().getSelections()
|
val selections = repository.get().getCurrentSelections()
|
||||||
val affordanceById =
|
val affordanceById =
|
||||||
getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
|
getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
|
||||||
return slots.associate { slot ->
|
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>>,
|
expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
|
||||||
) {
|
) {
|
||||||
assertThat(observed).isEqualTo(expected)
|
assertThat(observed).isEqualTo(expected)
|
||||||
assertThat(underTest.getSelections())
|
assertThat(underTest.getCurrentSelections())
|
||||||
.isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
|
.isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
|
||||||
expected.forEach { (slotId, configs) ->
|
expected.forEach { (slotId, configs) ->
|
||||||
assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
|
assertThat(underTest.getCurrentSelections(slotId)).isEqualTo(configs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user