Create SubscriptionRepository.phoneNumberFlow

And use it in the MobileNetworkSettings.

Fix: 341318273
Flag: EXEMPT bug fix
Test: manual - on MobileNetworkSettings
Test: unit test
Change-Id: I886373c1ed5129ebd8bcceedd513e9d1776106c8
This commit is contained in:
Chaohui Wang
2024-07-25 15:46:03 +08:00
parent 8a4a5f207e
commit cf13fe7776
5 changed files with 100 additions and 141 deletions

View File

@@ -17,47 +17,35 @@
package com.android.settings.network.telephony package com.android.settings.network.telephony
import android.content.Context import android.content.Context
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.android.settings.R import com.android.settings.R
import com.android.settings.flags.Flags import com.android.settings.flags.Flags
import com.android.settings.network.SubscriptionInfoListViewModel
import com.android.settings.network.SubscriptionUtil import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** /** Preference controller for "Phone number" */
* Preference controller for "Phone number" class MobileNetworkPhoneNumberPreferenceController
*/ @JvmOverloads
class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String) : constructor(
TelephonyBasePreferenceController(context, key) { context: Context,
key: String,
private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context),
) : TelephonyBasePreferenceController(context, key) {
private lateinit var lazyViewModel: Lazy<SubscriptionInfoListViewModel>
private lateinit var preference: Preference private lateinit var preference: Preference
private var phoneNumber = String() fun init(subId: Int) {
fun init(fragment: Fragment, subId: Int) {
lazyViewModel = fragment.viewModels()
mSubId = subId mSubId = subId
} }
override fun getAvailabilityStatus(subId: Int): Int = when { override fun getAvailabilityStatus(subId: Int): Int =
when {
!Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE !Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE
SubscriptionManager.isValidSubscriptionId(subId) SubscriptionManager.isValidSubscriptionId(subId) &&
&& SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE
else -> CONDITIONALLY_UNAVAILABLE else -> CONDITIONALLY_UNAVAILABLE
} }
@@ -67,51 +55,10 @@ class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String
} }
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
if (!this::lazyViewModel.isInitialized) { subscriptionRepository.phoneNumberFlow(mSubId).collectLatestWithLifecycle(
Log.e( viewLifecycleOwner) { phoneNumber ->
this.javaClass.simpleName, preference.summary = phoneNumber ?: getStringUnknown()
"lateinit property lazyViewModel has not been initialized"
)
return
} }
val viewModel by lazyViewModel
val coroutineScope = viewLifecycleOwner.lifecycleScope
viewModel.subscriptionInfoListFlow
.map { subscriptionInfoList ->
subscriptionInfoList
.firstOrNull { subInfo -> subInfo.subscriptionId == mSubId }
}
.flowOn(Dispatchers.Default)
.collectLatestWithLifecycle(viewLifecycleOwner) {
it?.let {
coroutineScope.launch {
refreshData(it)
}
}
}
}
@VisibleForTesting
suspend fun refreshData(subscriptionInfo: SubscriptionInfo){
withContext(Dispatchers.Default) {
phoneNumber = getFormattedPhoneNumber(subscriptionInfo)
}
refreshUi()
}
private fun refreshUi(){
preference.summary = phoneNumber
}
private fun getFormattedPhoneNumber(subscriptionInfo: SubscriptionInfo?): String {
val phoneNumber = SubscriptionUtil.getBidiFormattedPhoneNumber(
mContext,
subscriptionInfo
)
return phoneNumber
?.let { return it.ifEmpty { getStringUnknown() } }
?: getStringUnknown()
} }
private fun getStringUnknown(): String { private fun getStringUnknown(): String {

View File

@@ -257,7 +257,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme
use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId); use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId);
use(MobileNetworkSpnPreferenceController.class).init(this, mSubId); use(MobileNetworkSpnPreferenceController.class).init(this, mSubId);
use(MobileNetworkPhoneNumberPreferenceController.class).init(this, mSubId); use(MobileNetworkPhoneNumberPreferenceController.class).init(mSubId);
use(MobileNetworkImeiPreferenceController.class).init(this, mSubId); use(MobileNetworkImeiPreferenceController.class).init(this, mSubId);
final MobileDataPreferenceController mobileDataPreferenceController = final MobileDataPreferenceController mobileDataPreferenceController =

View File

@@ -24,13 +24,14 @@ import androidx.lifecycle.LifecycleOwner
import com.android.settings.network.SubscriptionUtil import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -52,7 +53,7 @@ class SubscriptionRepository(private val context: Context) {
/** Flow of whether the subscription enabled for the given [subId]. */ /** Flow of whether the subscription enabled for the given [subId]. */
fun isSubscriptionEnabledFlow(subId: Int): Flow<Boolean> { fun isSubscriptionEnabledFlow(subId: Int): Flow<Boolean> {
if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
return context.subscriptionsChangedFlow() return subscriptionsChangedFlow()
.map { subscriptionManager.isSubscriptionEnabled(subId) } .map { subscriptionManager.isSubscriptionEnabled(subId) }
.conflate() .conflate()
.onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") } .onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") }
@@ -87,12 +88,30 @@ class SubscriptionRepository(private val context: Context) {
}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default)
/** Flow of active subscription ids. */ /** Flow of active subscription ids. */
fun activeSubscriptionIdListFlow(): Flow<List<Int>> = context.subscriptionsChangedFlow() fun activeSubscriptionIdListFlow(): Flow<List<Int>> =
subscriptionsChangedFlow()
.map { subscriptionManager.activeSubscriptionIdList.sorted() } .map { subscriptionManager.activeSubscriptionIdList.sorted() }
.distinctUntilChanged() .distinctUntilChanged()
.conflate() .conflate()
.onEach { Log.d(TAG, "activeSubscriptionIdList: $it") } .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
fun activeSubscriptionInfoFlow(subId: Int): Flow<SubscriptionInfo?> =
subscriptionsChangedFlow()
.map { subscriptionManager.getActiveSubscriptionInfo(subId) }
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
fun phoneNumberFlow(subId: Int): Flow<String?> =
activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo ->
if (subInfo != null) {
context.phoneNumberFlow(subInfo)
} else {
flowOf(null)
}
}
} }
val Context.subscriptionManager: SubscriptionManager? val Context.subscriptionManager: SubscriptionManager?
@@ -100,9 +119,12 @@ val Context.subscriptionManager: SubscriptionManager?
fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!! fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map { fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo): Flow<String?> =
SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) subscriptionsChangedFlow()
}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default) .map { SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) }
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
fun Context.subscriptionsChangedFlow(): Flow<Unit> = fun Context.subscriptionsChangedFlow(): Flow<Unit> =
SubscriptionRepository(this).subscriptionsChangedFlow() SubscriptionRepository(this).subscriptionsChangedFlow()

View File

@@ -17,8 +17,7 @@
package com.android.settings.network.telephony package com.android.settings.network.telephony
import android.content.Context import android.content.Context
import android.telephony.SubscriptionInfo import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
@@ -26,17 +25,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.settings.R import com.android.settings.R
import com.android.settings.core.BasePreferenceController import com.android.settings.core.BasePreferenceController
import com.android.settings.network.SubscriptionInfoListViewModel
import com.android.settings.network.SubscriptionUtil import com.android.settings.network.SubscriptionUtil
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Before 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.MockitoSession import org.mockito.MockitoSession
import org.mockito.kotlin.any import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness import org.mockito.quality.Strictness
@@ -44,29 +45,25 @@ import org.mockito.quality.Strictness
class MobileNetworkPhoneNumberPreferenceControllerTest { class MobileNetworkPhoneNumberPreferenceControllerTest {
private lateinit var mockSession: MockitoSession private lateinit var mockSession: MockitoSession
private val mockViewModels = mock<Lazy<SubscriptionInfoListViewModel>>()
private val mockFragment = mock<Fragment>{
val viewmodel = mockViewModels
}
private var mockPhoneNumber = String()
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
private val controller = MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY) private val mockSubscriptionRepository = mock<SubscriptionRepository>()
private val controller =
MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY, mockSubscriptionRepository)
private val preference = Preference(context).apply { key = TEST_KEY } private val preference = Preference(context).apply { key = TEST_KEY }
private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
@Before @Before
fun setUp() { fun setUp() {
mockSession = ExtendedMockito.mockitoSession() mockSession =
.initMocks(this) ExtendedMockito.mockitoSession()
.mockStatic(SubscriptionUtil::class.java) .mockStatic(SubscriptionUtil::class.java)
.strictness(Strictness.LENIENT) .strictness(Strictness.LENIENT)
.startMocking() .startMocking()
preferenceScreen.addPreference(preference) preferenceScreen.addPreference(preference)
controller.init(SUB_ID)
controller.displayPreference(preferenceScreen) controller.displayPreference(preferenceScreen)
whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
} }
@After @After
@@ -75,41 +72,29 @@ class MobileNetworkPhoneNumberPreferenceControllerTest {
} }
@Test @Test
fun refreshData_getEmptyPhoneNumber_preferenceIsNotVisible() = runBlocking { fun onViewCreated_cannotGetPhoneNumber_displayUnknown() = runBlocking {
whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true) whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn( mockSubscriptionRepository.stub {
listOf( on { phoneNumberFlow(SUB_ID) } doReturn flowOf(null)
SUB_INFO_1, }
SUB_INFO_2
)
)
var mockSubId = 2
controller.init(mockFragment, mockSubId)
mockPhoneNumber = String()
controller.refreshData(SUB_INFO_2) controller.onViewCreated(TestLifecycleOwner())
delay(100)
assertThat(preference.summary).isEqualTo( assertThat(preference.summary).isEqualTo(context.getString(R.string.device_info_default))
context.getString(R.string.device_info_default))
} }
@Test @Test
fun refreshData_getPhoneNumber_preferenceSummaryIsExpected() = runBlocking { fun onViewCreated_canGetPhoneNumber_displayPhoneNumber() = runBlocking {
whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true) whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn( mockSubscriptionRepository.stub {
listOf( on { phoneNumberFlow(SUB_ID) } doReturn flowOf(PHONE_NUMBER)
SUB_INFO_1, }
SUB_INFO_2
)
)
var mockSubId = 2
controller.init(mockFragment, mockSubId)
mockPhoneNumber = "test phone number"
whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
controller.refreshData(SUB_INFO_2) controller.onViewCreated(TestLifecycleOwner())
delay(100)
assertThat(preference.summary).isEqualTo(mockPhoneNumber) assertThat(preference.summary).isEqualTo(PHONE_NUMBER)
} }
@Test @Test
@@ -123,18 +108,7 @@ class MobileNetworkPhoneNumberPreferenceControllerTest {
private companion object { private companion object {
const val TEST_KEY = "test_key" const val TEST_KEY = "test_key"
const val DISPLAY_NAME_1 = "Sub 1" const val SUB_ID = 10
const val DISPLAY_NAME_2 = "Sub 2" const val PHONE_NUMBER = "1234567890"
val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(1)
setDisplayName(DISPLAY_NAME_1)
}.build()
val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(2)
setDisplayName(DISPLAY_NAME_2)
}.build()
} }
} }

View File

@@ -204,6 +204,22 @@ class SubscriptionRepositoryTest {
assertThat(phoneNumber).isEqualTo(NUMBER_1) assertThat(phoneNumber).isEqualTo(NUMBER_1)
} }
@Test
fun phoneNumberFlow_withSubId() = runBlocking {
val subInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_IN_SLOT_1)
setMcc(MCC)
}.build()
mockSubscriptionManager.stub {
on { getActiveSubscriptionInfo(SUB_ID_IN_SLOT_1) } doReturn subInfo
on { getPhoneNumber(SUB_ID_IN_SLOT_1) } doReturn NUMBER_1
}
val phoneNumber = repository.phoneNumberFlow(SUB_ID_IN_SLOT_1).firstWithTimeoutOrNull()
assertThat(phoneNumber).isEqualTo(NUMBER_1)
}
private companion object { private companion object {
const val SIM_SLOT_INDEX_0 = 0 const val SIM_SLOT_INDEX_0 = 0
const val SUB_ID_IN_SLOT_0 = 2 const val SUB_ID_IN_SLOT_0 = 2