Merge "Adjust Mute Quick Affordance to not make binder calls on main thread" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
d56308bc57
@@ -27,16 +27,24 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
|
|||||||
import com.android.systemui.common.shared.model.ContentDescription
|
import com.android.systemui.common.shared.model.ContentDescription
|
||||||
import com.android.systemui.common.shared.model.Icon
|
import com.android.systemui.common.shared.model.Icon
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
|
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
|
||||||
import com.android.systemui.settings.UserFileManager
|
import com.android.systemui.settings.UserFileManager
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
import com.android.systemui.util.RingerModeTracker
|
import com.android.systemui.util.RingerModeTracker
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
@@ -46,6 +54,9 @@ class MuteQuickAffordanceConfig @Inject constructor(
|
|||||||
private val userFileManager: UserFileManager,
|
private val userFileManager: UserFileManager,
|
||||||
private val ringerModeTracker: RingerModeTracker,
|
private val ringerModeTracker: RingerModeTracker,
|
||||||
private val audioManager: AudioManager,
|
private val audioManager: AudioManager,
|
||||||
|
@Application private val coroutineScope: CoroutineScope,
|
||||||
|
@Main private val mainDispatcher: CoroutineDispatcher,
|
||||||
|
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
) : KeyguardQuickAffordanceConfig {
|
) : KeyguardQuickAffordanceConfig {
|
||||||
|
|
||||||
private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
|
private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
|
||||||
@@ -58,7 +69,7 @@ class MuteQuickAffordanceConfig @Inject constructor(
|
|||||||
|
|
||||||
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
|
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
|
||||||
ringerModeTracker.ringerModeInternal.asFlow()
|
ringerModeTracker.ringerModeInternal.asFlow()
|
||||||
.onStart { emit(getLastNonSilentRingerMode()) }
|
.onStart { getLastNonSilentRingerMode() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { mode ->
|
.onEach { mode ->
|
||||||
// only remember last non-SILENT ringer mode
|
// only remember last non-SILENT ringer mode
|
||||||
@@ -87,54 +98,60 @@ class MuteQuickAffordanceConfig @Inject constructor(
|
|||||||
activationState,
|
activationState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.flowOn(backgroundDispatcher)
|
||||||
|
|
||||||
override fun onTriggered(
|
override fun onTriggered(
|
||||||
expandable: Expandable?
|
expandable: Expandable?
|
||||||
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
|
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
|
||||||
val newRingerMode: Int
|
coroutineScope.launch(backgroundDispatcher) {
|
||||||
val currentRingerMode =
|
val newRingerMode: Int
|
||||||
ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
|
val currentRingerMode = audioManager.ringerModeInternal
|
||||||
if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
|
if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
|
||||||
newRingerMode = previousNonSilentMode
|
newRingerMode = previousNonSilentMode
|
||||||
} else {
|
} else {
|
||||||
previousNonSilentMode = currentRingerMode
|
previousNonSilentMode = currentRingerMode
|
||||||
newRingerMode = AudioManager.RINGER_MODE_SILENT
|
newRingerMode = AudioManager.RINGER_MODE_SILENT
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRingerMode != newRingerMode) {
|
if (currentRingerMode != newRingerMode) {
|
||||||
audioManager.ringerModeInternal = newRingerMode
|
audioManager.ringerModeInternal = newRingerMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
|
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
|
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
|
||||||
if (audioManager.isVolumeFixed) {
|
withContext(backgroundDispatcher) {
|
||||||
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
|
if (audioManager.isVolumeFixed) {
|
||||||
} else {
|
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
|
||||||
KeyguardQuickAffordanceConfig.PickerScreenState.Default()
|
} else {
|
||||||
|
KeyguardQuickAffordanceConfig.PickerScreenState.Default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the last non-silent ringer mode from shared-preferences if it exists. This is
|
* Gets the last non-silent ringer mode from shared-preferences if it exists. This is
|
||||||
* cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
|
* cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
|
||||||
*/
|
*/
|
||||||
private fun getLastNonSilentRingerMode(): Int =
|
private suspend fun getLastNonSilentRingerMode(): Int =
|
||||||
userFileManager.getSharedPreferences(
|
withContext(backgroundDispatcher) {
|
||||||
MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
|
userFileManager.getSharedPreferences(
|
||||||
Context.MODE_PRIVATE,
|
MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
|
||||||
userTracker.userId
|
Context.MODE_PRIVATE,
|
||||||
).getInt(
|
userTracker.userId
|
||||||
LAST_NON_SILENT_RINGER_MODE_KEY,
|
).getInt(
|
||||||
ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
|
LAST_NON_SILENT_RINGER_MODE_KEY,
|
||||||
)
|
ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T> LiveData<T>.asFlow(): Flow<T?> =
|
private fun <T> LiveData<T>.asFlow(): Flow<T?> =
|
||||||
conflatedCallbackFlow {
|
conflatedCallbackFlow {
|
||||||
val observer = Observer { value: T -> trySend(value) }
|
val observer = Observer { value: T -> trySend(value) }
|
||||||
observeForever(observer)
|
observeForever(observer)
|
||||||
send(value)
|
send(value)
|
||||||
awaitClose { removeObserver(observer) }
|
awaitClose { removeObserver(observer) }
|
||||||
}
|
}.flowOn(mainDispatcher)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
|
const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
|
||||||
|
|||||||
@@ -23,15 +23,18 @@ import androidx.lifecycle.Observer
|
|||||||
import com.android.systemui.CoreStartable
|
import com.android.systemui.CoreStartable
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
import com.android.systemui.flags.FeatureFlags
|
import com.android.systemui.flags.FeatureFlags
|
||||||
import com.android.systemui.flags.Flags
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
|
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
|
||||||
import com.android.systemui.settings.UserFileManager
|
import com.android.systemui.settings.UserFileManager
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
import com.android.systemui.util.RingerModeTracker
|
import com.android.systemui.util.RingerModeTracker
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +48,7 @@ class MuteQuickAffordanceCoreStartable @Inject constructor(
|
|||||||
private val userFileManager: UserFileManager,
|
private val userFileManager: UserFileManager,
|
||||||
private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
|
private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
|
||||||
@Application private val coroutineScope: CoroutineScope,
|
@Application private val coroutineScope: CoroutineScope,
|
||||||
|
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
) : CoreStartable {
|
) : CoreStartable {
|
||||||
|
|
||||||
private val observer = Observer(this::updateLastNonSilentRingerMode)
|
private val observer = Observer(this::updateLastNonSilentRingerMode)
|
||||||
@@ -72,15 +76,17 @@ class MuteQuickAffordanceCoreStartable @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
|
private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
|
||||||
if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
|
coroutineScope.launch(backgroundDispatcher) {
|
||||||
userFileManager.getSharedPreferences(
|
if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
|
||||||
MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
|
userFileManager.getSharedPreferences(
|
||||||
Context.MODE_PRIVATE,
|
MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
|
||||||
userTracker.userId
|
Context.MODE_PRIVATE,
|
||||||
)
|
userTracker.userId
|
||||||
.edit()
|
)
|
||||||
.putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
|
.edit()
|
||||||
.apply()
|
.putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@ package com.android.systemui.keyguard.data.quickaffordance
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.settings.UserFileManager
|
import com.android.systemui.settings.UserFileManager
|
||||||
@@ -27,10 +26,12 @@ import com.android.systemui.settings.UserTracker
|
|||||||
import com.android.systemui.util.RingerModeTracker
|
import com.android.systemui.util.RingerModeTracker
|
||||||
import com.android.systemui.util.mockito.any
|
import com.android.systemui.util.mockito.any
|
||||||
import com.android.systemui.util.mockito.argumentCaptor
|
import com.android.systemui.util.mockito.argumentCaptor
|
||||||
import com.android.systemui.util.mockito.mock
|
|
||||||
import com.android.systemui.util.mockito.whenever
|
import com.android.systemui.util.mockito.whenever
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -57,13 +58,15 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var userFileManager: UserFileManager
|
private lateinit var userFileManager: UserFileManager
|
||||||
|
|
||||||
|
private lateinit var testDispatcher: TestDispatcher
|
||||||
private lateinit var testScope: TestScope
|
private lateinit var testScope: TestScope
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
testScope = TestScope()
|
testDispatcher = StandardTestDispatcher()
|
||||||
|
testScope = TestScope(testDispatcher)
|
||||||
|
|
||||||
whenever(userTracker.userContext).thenReturn(context)
|
whenever(userTracker.userContext).thenReturn(context)
|
||||||
whenever(userFileManager.getSharedPreferences(any(), any(), any()))
|
whenever(userFileManager.getSharedPreferences(any(), any(), any()))
|
||||||
@@ -74,7 +77,10 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() {
|
|||||||
userTracker,
|
userTracker,
|
||||||
userFileManager,
|
userFileManager,
|
||||||
ringerModeTracker,
|
ringerModeTracker,
|
||||||
audioManager
|
audioManager,
|
||||||
|
testScope.backgroundScope,
|
||||||
|
testDispatcher,
|
||||||
|
testDispatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,17 +109,16 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() {
|
fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() = testScope.runTest {
|
||||||
//given
|
//given
|
||||||
val ringerModeCapture = argumentCaptor<Int>()
|
val ringerModeCapture = argumentCaptor<Int>()
|
||||||
val ringerModeInternal = mock<LiveData<Int>>()
|
whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
|
||||||
whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
|
|
||||||
whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
|
|
||||||
underTest.onTriggered(null)
|
underTest.onTriggered(null)
|
||||||
whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT)
|
whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
|
||||||
|
|
||||||
//when
|
//when
|
||||||
val result = underTest.onTriggered(null)
|
val result = underTest.onTriggered(null)
|
||||||
|
runCurrent()
|
||||||
verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
|
verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
|
||||||
|
|
||||||
//then
|
//then
|
||||||
@@ -122,15 +127,14 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `triggered - state is not SILENT - move to SILENT ringer`() {
|
fun `triggered - state is not SILENT - move to SILENT ringer`() = testScope.runTest {
|
||||||
//given
|
//given
|
||||||
val ringerModeCapture = argumentCaptor<Int>()
|
val ringerModeCapture = argumentCaptor<Int>()
|
||||||
val ringerModeInternal = mock<LiveData<Int>>()
|
whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
|
||||||
whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
|
|
||||||
whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
val result = underTest.onTriggered(null)
|
val result = underTest.onTriggered(null)
|
||||||
|
runCurrent()
|
||||||
verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
|
verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
|
||||||
|
|
||||||
//then
|
//then
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import com.android.systemui.util.mockito.whenever
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -67,6 +69,7 @@ class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
|
private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
|
||||||
|
|
||||||
|
private lateinit var testDispatcher: TestDispatcher
|
||||||
private lateinit var testScope: TestScope
|
private lateinit var testScope: TestScope
|
||||||
|
|
||||||
private lateinit var underTest: MuteQuickAffordanceCoreStartable
|
private lateinit var underTest: MuteQuickAffordanceCoreStartable
|
||||||
@@ -83,7 +86,8 @@ class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
|
|||||||
val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
|
val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
|
||||||
whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
|
whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
|
||||||
|
|
||||||
testScope = TestScope()
|
testDispatcher = StandardTestDispatcher()
|
||||||
|
testScope = TestScope(testDispatcher)
|
||||||
|
|
||||||
underTest = MuteQuickAffordanceCoreStartable(
|
underTest = MuteQuickAffordanceCoreStartable(
|
||||||
featureFlags,
|
featureFlags,
|
||||||
@@ -91,7 +95,8 @@ class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
|
|||||||
ringerModeTracker,
|
ringerModeTracker,
|
||||||
userFileManager,
|
userFileManager,
|
||||||
keyguardQuickAffordanceRepository,
|
keyguardQuickAffordanceRepository,
|
||||||
testScope,
|
testScope.backgroundScope,
|
||||||
|
testDispatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +163,7 @@ class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
|
|||||||
runCurrent()
|
runCurrent()
|
||||||
verify(ringerModeInternal).observeForever(observerCaptor.capture())
|
verify(ringerModeInternal).observeForever(observerCaptor.capture())
|
||||||
observerCaptor.value.onChanged(newRingerMode)
|
observerCaptor.value.onChanged(newRingerMode)
|
||||||
|
runCurrent()
|
||||||
val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
|
val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
|
||||||
|
|
||||||
//then
|
//then
|
||||||
|
|||||||
Reference in New Issue
Block a user