New CarrierConfigRepository

Benefices,
- Gets the configuration values of the specified keys, for better
  performance
- Check key suffix for correctness
- Support cache
- If CarrierConfigManager throw exception, use default value

Bug: 337417520
Flag: EXEMPT refactor
Test: manual on Sim Status
Test: unit
Change-Id: I68f41ef66d495080f628794ade63cf807efba619
This commit is contained in:
Chaohui Wang
2024-06-21 11:30:36 +08:00
parent 1430348455
commit ef12f1ddb5
5 changed files with 419 additions and 61 deletions

View File

@@ -23,10 +23,10 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.android.settings.network.telephony.CarrierConfigRepository
import com.android.settings.network.telephony.SimSlotRepository import com.android.settings.network.telephony.SimSlotRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepository import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
import com.android.settings.network.telephony.safeGetConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -39,7 +39,9 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SimStatusDialogRepository @JvmOverloads constructor( class SimStatusDialogRepository
@JvmOverloads
constructor(
private val context: Context, private val context: Context,
private val simSlotRepository: SimSlotRepository = SimSlotRepository(context), private val simSlotRepository: SimSlotRepository = SimSlotRepository(context),
private val signalStrengthRepository: SignalStrengthRepository = private val signalStrengthRepository: SignalStrengthRepository =
@@ -48,7 +50,7 @@ class SimStatusDialogRepository @JvmOverloads constructor(
ImsMmTelRepositoryImpl(context, subId) ImsMmTelRepositoryImpl(context, subId)
}, },
) { ) {
private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)!! private val carrierConfigRepository = CarrierConfigRepository(context)
data class SimStatusDialogInfo( data class SimStatusDialogInfo(
val signalStrength: String? = null, val signalStrength: String? = null,
@@ -73,7 +75,8 @@ class SimStatusDialogRepository @JvmOverloads constructor(
} }
private fun simStatusDialogInfoBySlotFlow(simSlotIndex: Int): Flow<SimStatusDialogInfo> = private fun simStatusDialogInfoBySlotFlow(simSlotIndex: Int): Flow<SimStatusDialogInfo> =
simSlotRepository.subIdInSimSlotFlow(simSlotIndex) simSlotRepository
.subIdInSimSlotFlow(simSlotIndex)
.flatMapLatest { subId -> .flatMapLatest { subId ->
if (SubscriptionManager.isValidSubscriptionId(subId)) { if (SubscriptionManager.isValidSubscriptionId(subId)) {
simStatusDialogInfoFlow(subId) simStatusDialogInfoFlow(subId)
@@ -99,22 +102,16 @@ class SimStatusDialogRepository @JvmOverloads constructor(
} }
private fun showUpFlow(subId: Int) = flow { private fun showUpFlow(subId: Int) = flow {
val config = carrierConfigManager.safeGetConfig( val visibility =
keys = listOf( carrierConfigRepository.transformConfig(subId) {
CarrierConfigManager.KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL, SimStatusDialogVisibility(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, signalStrengthShowUp =
), getBoolean(
subId = subId, CarrierConfigManager.KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL),
) imsRegisteredShowUp =
val visibility = SimStatusDialogVisibility( getBoolean(CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL),
signalStrengthShowUp = config.getBoolean( )
CarrierConfigManager.KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL, }
true, // by default we show the signal strength in sim status
),
imsRegisteredShowUp = config.getBoolean(
CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL
),
)
emit(visibility) emit(visibility)
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.core.os.persistableBundleOf
/** /**
* Gets the configuration values of the specified config keys applied. * Gets the configuration values of the specified config keys applied.
*/ */
@Deprecated("Use CarrierConfigRepository instead")
fun CarrierConfigManager.safeGetConfig( fun CarrierConfigManager.safeGetConfig(
keys: List<String>, keys: List<String>,
subId: Int = SubscriptionManager.getDefaultSubscriptionId(), subId: Int = SubscriptionManager.getDefaultSubscriptionId(),

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2024 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.settings.network.telephony
import android.content.Context
import android.os.PersistableBundle
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import java.util.concurrent.ConcurrentHashMap
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
class CarrierConfigRepository(private val context: Context) {
private val carrierConfigManager: CarrierConfigManager? =
context.getSystemService(CarrierConfigManager::class.java)
private enum class KeyType {
BOOLEAN,
INT,
STRING
}
interface CarrierConfigAccessor {
fun getBoolean(key: String): Boolean
fun getInt(key: String): Int
fun getString(key: String): String?
}
private class Accessor(private val cache: ConfigCache) : CarrierConfigAccessor {
private val keysToRetrieve = mutableMapOf<String, KeyType>()
override fun getBoolean(key: String): Boolean {
check(key.endsWith("_bool")) { "Boolean key should ends with _bool" }
val value = cache[key]
return if (value == null) {
keysToRetrieve += key to KeyType.BOOLEAN
DefaultConfig.getBoolean(key)
} else {
check(value is BooleanConfigValue) { "Boolean value type wrong" }
value.value
}
}
override fun getInt(key: String): Int {
check(key.endsWith("_int")) { "Int key should ends with _int" }
val value = cache[key]
return if (value == null) {
keysToRetrieve += key to KeyType.INT
DefaultConfig.getInt(key)
} else {
check(value is IntConfigValue) { "Int value type wrong" }
value.value
}
}
override fun getString(key: String): String? {
check(key.endsWith("_string")) { "String key should ends with _string" }
val value = cache[key]
return if (value == null) {
keysToRetrieve += key to KeyType.STRING
DefaultConfig.getString(key)
} else {
check(value is StringConfigValue) { "String value type wrong" }
value.value
}
}
fun getKeysToRetrieve(): Map<String, KeyType> = keysToRetrieve
}
/**
* Gets the configuration values for the given [subId].
*
* Configuration values could be accessed in [block]. Note: [block] could be called multiple
* times, so it should be pure function without side effort.
*/
fun <T> transformConfig(subId: Int, block: CarrierConfigAccessor.() -> T): T {
val perSubCache = getPerSubCache(subId)
val accessor = Accessor(perSubCache)
val result = accessor.block()
val keysToRetrieve = accessor.getKeysToRetrieve()
// If all keys found in the first pass, no need to collect again
if (keysToRetrieve.isEmpty()) return result
perSubCache.update(subId, keysToRetrieve)
return accessor.block()
}
/** Gets the configuration boolean for the given [subId] and [key]. */
fun getBoolean(subId: Int, key: String): Boolean = transformConfig(subId) { getBoolean(key) }
/** Gets the configuration int for the given [subId] and [key]. */
fun getInt(subId: Int, key: String): Int = transformConfig(subId) { getInt(key) }
/** Gets the configuration string for the given [subId] and [key]. */
fun getString(subId: Int, key: String): String? = transformConfig(subId) { getString(key) }
private fun ConfigCache.update(subId: Int, keysToRetrieve: Map<String, KeyType>) {
val config = safeGetConfig(subId, keysToRetrieve.keys) ?: return
for ((key, type) in keysToRetrieve) {
when (type) {
KeyType.BOOLEAN -> this[key] = BooleanConfigValue(config.getBoolean(key))
KeyType.INT -> this[key] = IntConfigValue(config.getInt(key))
KeyType.STRING -> this[key] = StringConfigValue(config.getString(key))
}
}
}
/** Gets the configuration values of the specified config keys applied. */
private fun safeGetConfig(subId: Int, keys: Collection<String>): PersistableBundle? {
if (carrierConfigManager == null || !SubscriptionManager.isValidSubscriptionId(subId)) {
return null
}
tryRegisterListener(context)
return try {
carrierConfigManager.getConfigForSubId(subId, *keys.toTypedArray())
} catch (e: Exception) {
Log.e(TAG, "safeGetConfig: exception", e)
// The CarrierConfigLoader (the service implemented the CarrierConfigManager) hasn't
// been initialized yet. This may occurs during very early phase of phone booting up
// or when Phone process has been restarted.
// Settings should not assume Carrier config loader (and any other system services
// as well) are always available. If not available, use default value instead.
null
}
}
companion object {
private const val TAG = "CarrierConfigRepository"
private val DefaultConfig = CarrierConfigManager.getDefaultConfig()
/** Cache of config values for each subscription. */
private val Cache = ConcurrentHashMap<Int, ConfigCache>()
private fun getPerSubCache(subId: Int) =
Cache.computeIfAbsent(subId) { ConcurrentHashMap() }
/** To make sure the registerCarrierConfigChangeListener is only called once. */
private val ListenerRegistered = atomic(false)
private fun tryRegisterListener(context: Context) {
if (ListenerRegistered.compareAndSet(expect = false, update = true)) {
val carrierConfigManager =
context.applicationContext.getSystemService(CarrierConfigManager::class.java)
if (carrierConfigManager != null) {
carrierConfigManager.registerCarrierConfigChangeListener()
} else {
ListenerRegistered.getAndSet(false)
}
}
}
private fun CarrierConfigManager.registerCarrierConfigChangeListener() {
val executor = Dispatchers.Default.asExecutor()
registerCarrierConfigChangeListener(executor) { _, subId, _, _ ->
Log.d(TAG, "[$subId] onCarrierConfigChanged")
Cache.remove(subId)
}
}
@VisibleForTesting
fun resetForTest() {
Cache.clear()
ListenerRegistered.getAndSet(false)
}
@VisibleForTesting
fun setBooleanForTest(subId: Int, key: String, value: Boolean) {
check(key.endsWith("_bool")) { "Boolean key should ends with _bool" }
getPerSubCache(subId)[key] = BooleanConfigValue(value)
}
@VisibleForTesting
fun setIntForTest(subId: Int, key: String, value: Int) {
check(key.endsWith("_int")) { "Int key should ends with _int" }
getPerSubCache(subId)[key] = IntConfigValue(value)
}
@VisibleForTesting
fun setStringForTest(subId: Int, key: String, value: String) {
check(key.endsWith("_string")) { "String key should ends with _string" }
getPerSubCache(subId)[key] = StringConfigValue(value)
}
}
}
private sealed interface ConfigValue
private data class BooleanConfigValue(val value: Boolean) : ConfigValue
private data class IntConfigValue(val value: Int) : ConfigValue
private data class StringConfigValue(val value: String?) : ConfigValue
private typealias ConfigCache = ConcurrentHashMap<String, ConfigValue>

View File

@@ -17,65 +17,65 @@
package com.android.settings.deviceinfo.simstatus package com.android.settings.deviceinfo.simstatus
import android.content.Context import android.content.Context
import android.os.PersistableBundle
import android.telephony.CarrierConfigManager import android.telephony.CarrierConfigManager
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.deviceinfo.simstatus.SimStatusDialogRepository.SimStatusDialogInfo import com.android.settings.deviceinfo.simstatus.SimStatusDialogRepository.SimStatusDialogInfo
import com.android.settings.network.telephony.CarrierConfigRepository
import com.android.settings.network.telephony.SimSlotRepository import com.android.settings.network.telephony.SimSlotRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepository import com.android.settings.network.telephony.ims.ImsMmTelRepository
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.anyVararg
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SimStatusDialogRepositoryTest { class SimStatusDialogRepositoryTest {
private val carrierConfig = PersistableBundle().apply { private val context: Context = ApplicationProvider.getApplicationContext()
putBoolean(CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, true)
}
private val mockCarrierConfigManager = mock<CarrierConfigManager> { private val mockSimSlotRepository =
on { getConfigForSubId(eq(SUB_ID), anyVararg()) } doReturn carrierConfig mock<SimSlotRepository> {
} on { subIdInSimSlotFlow(SIM_SLOT_INDEX) } doReturn flowOf(SUB_ID)
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) { private val mockSignalStrengthRepository =
on { getSystemService(CarrierConfigManager::class.java) } doReturn mockCarrierConfigManager mock<SignalStrengthRepository> {
} on { signalStrengthDisplayFlow(SUB_ID) } doReturn flowOf(SIGNAL_STRENGTH)
}
private val mockSimSlotRepository = mock<SimSlotRepository> { private val mockImsMmTelRepository =
on { subIdInSimSlotFlow(SIM_SLOT_INDEX) } doReturn flowOf(SUB_ID) mock<ImsMmTelRepository> { on { imsRegisteredFlow() } doReturn flowOf(true) }
}
private val mockSignalStrengthRepository = mock<SignalStrengthRepository> { private val controller =
on { signalStrengthDisplayFlow(SUB_ID) } doReturn flowOf(SIGNAL_STRENGTH) SimStatusDialogRepository(
} context = context,
simSlotRepository = mockSimSlotRepository,
signalStrengthRepository = mockSignalStrengthRepository,
imsMmTelRepositoryFactory = { subId ->
assertThat(subId).isEqualTo(SUB_ID)
mockImsMmTelRepository
},
)
private val mockImsMmTelRepository = mock<ImsMmTelRepository> { @Before
on { imsRegisteredFlow() } doReturn flowOf(true) fun setUp() {
CarrierConfigRepository.resetForTest()
} }
private val controller = SimStatusDialogRepository(
context = context,
simSlotRepository = mockSimSlotRepository,
signalStrengthRepository = mockSignalStrengthRepository,
imsMmTelRepositoryFactory = { subId ->
assertThat(subId).isEqualTo(SUB_ID)
mockImsMmTelRepository
},
)
@Test @Test
fun collectSimStatusDialogInfo() = runBlocking { fun collectSimStatusDialogInfo() = runBlocking {
CarrierConfigRepository.setBooleanForTest(
subId = SUB_ID,
key = CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL,
value = true,
)
var simStatusDialogInfo = SimStatusDialogInfo() var simStatusDialogInfo = SimStatusDialogInfo()
controller.collectSimStatusDialogInfo(TestLifecycleOwner(), SIM_SLOT_INDEX) { controller.collectSimStatusDialogInfo(TestLifecycleOwner(), SIM_SLOT_INDEX) {
@@ -83,19 +83,20 @@ class SimStatusDialogRepositoryTest {
} }
delay(100) delay(100)
assertThat(simStatusDialogInfo).isEqualTo( assertThat(simStatusDialogInfo)
SimStatusDialogInfo( .isEqualTo(
signalStrength = SIGNAL_STRENGTH, SimStatusDialogInfo(
imsRegistered = true, signalStrength = SIGNAL_STRENGTH,
) imsRegistered = true,
) ))
} }
@Test @Test
fun collectSimStatusDialogInfo_doNotShowSignalStrength() = runBlocking { fun collectSimStatusDialogInfo_doNotShowSignalStrength() = runBlocking {
carrierConfig.putBoolean( CarrierConfigRepository.setBooleanForTest(
CarrierConfigManager.KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL, subId = SUB_ID,
false key = CarrierConfigManager.KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL,
value = false,
) )
var simStatusDialogInfo = SimStatusDialogInfo() var simStatusDialogInfo = SimStatusDialogInfo()
@@ -109,7 +110,11 @@ class SimStatusDialogRepositoryTest {
@Test @Test
fun collectSimStatusDialogInfo_doNotShowImsRegistration() = runBlocking { fun collectSimStatusDialogInfo_doNotShowImsRegistration() = runBlocking {
carrierConfig.putBoolean(CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false) CarrierConfigRepository.setBooleanForTest(
subId = SUB_ID,
key = CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL,
value = false,
)
var simStatusDialogInfo = SimStatusDialogInfo() var simStatusDialogInfo = SimStatusDialogInfo()
controller.collectSimStatusDialogInfo(TestLifecycleOwner(), SIM_SLOT_INDEX) { controller.collectSimStatusDialogInfo(TestLifecycleOwner(), SIM_SLOT_INDEX) {

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2024 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.settings.network.telephony
import android.content.Context
import android.telephony.CarrierConfigManager
import androidx.core.os.persistableBundleOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyVararg
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class CarrierConfigRepositoryTest {
private val mockCarrierConfigManager = mock<CarrierConfigManager>()
private val context =
mock<Context> {
on { applicationContext } doReturn mock
on { getSystemService(CarrierConfigManager::class.java) } doReturn
mockCarrierConfigManager
}
private val repository = CarrierConfigRepository(context)
@Before
fun setUp() {
CarrierConfigRepository.resetForTest()
}
@Test
fun getBoolean_returnValue() {
val key = CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL
mockCarrierConfigManager.stub {
on { getConfigForSubId(any(), eq(key)) } doReturn persistableBundleOf(key to true)
}
val value = repository.getBoolean(SUB_ID, key)
assertThat(value).isTrue()
}
@Test
fun getInt_returnValue() {
val key = CarrierConfigManager.KEY_GBA_MODE_INT
mockCarrierConfigManager.stub {
on { getConfigForSubId(any(), eq(key)) } doReturn persistableBundleOf(key to 99)
}
val value = repository.getInt(SUB_ID, key)
assertThat(value).isEqualTo(99)
}
@Test
fun getString_returnValue() {
val key = CarrierConfigManager.KEY_CARRIER_NAME_STRING
mockCarrierConfigManager.stub {
on { getConfigForSubId(any(), eq(key)) } doReturn
persistableBundleOf(key to STRING_VALUE)
}
val value = repository.getString(SUB_ID, key)
assertThat(value).isEqualTo(STRING_VALUE)
}
@Test
fun transformConfig_managerThrowIllegalStateException_returnDefaultValue() {
mockCarrierConfigManager.stub {
on { getConfigForSubId(any(), anyVararg()) } doThrow IllegalStateException()
}
val carrierName =
repository.transformConfig(SUB_ID) {
getInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT)
}
assertThat(carrierName)
.isEqualTo(
CarrierConfigManager.getDefaultConfig()
.getInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT))
}
@Test
fun transformConfig_getValueTwice_cached() {
val key = CarrierConfigManager.KEY_CARRIER_NAME_STRING
mockCarrierConfigManager.stub {
on { getConfigForSubId(any(), eq(key)) } doReturn
persistableBundleOf(key to STRING_VALUE)
}
repository.transformConfig(SUB_ID) { getString(key) }
repository.transformConfig(SUB_ID) { getString(key) }
verify(mockCarrierConfigManager, times(1)).getConfigForSubId(any(), anyVararg())
}
@Test
fun transformConfig_registerCarrierConfigChangeListener() {
val key = CarrierConfigManager.KEY_CARRIER_NAME_STRING
repository.transformConfig(SUB_ID) { getString(key) }
repository.transformConfig(SUB_ID) { getString(key) }
verify(mockCarrierConfigManager, times(1)).registerCarrierConfigChangeListener(any(), any())
}
private companion object {
const val SUB_ID = 123
const val STRING_VALUE = "value"
}
}