Catch exception in telephonyRepository.isDataEnabledFlow

And migrate BillingCycleRepository to use it.

Fix: 339197552
Test: manual - on data usage
Test: unit test
Change-Id: Ieac295f37fdbf75d184d66ea11f170652af3ec5f
This commit is contained in:
Chaohui Wang
2024-05-08 17:55:30 +08:00
parent 5a85f6a72f
commit 8c507e871b
8 changed files with 52 additions and 45 deletions

View File

@@ -26,11 +26,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R import com.android.settings.R
import com.android.settings.core.SubSettingLauncher import com.android.settings.core.SubSettingLauncher
import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.network.mobileDataEnabledFlow
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.PreferenceModel
import kotlinx.coroutines.flow.map
/** /**
* Preference which displays billing cycle of subscription * Preference which displays billing cycle of subscription
@@ -46,8 +44,8 @@ class BillingCyclePreference @JvmOverloads constructor(
override fun setTemplate(template: NetworkTemplate, subId: Int) { override fun setTemplate(template: NetworkTemplate, subId: Int) {
setContent { setContent {
val isModifiable by remember { val isModifiable by remember(subId) {
context.mobileDataEnabledFlow(subId).map { repository.isModifiable(subId) } repository.isModifiableFlow(subId)
}.collectAsStateWithLifecycle(initialValue = false) }.collectAsStateWithLifecycle(initialValue = false)
Preference(object : PreferenceModel { Preference(object : PreferenceModel {

View File

@@ -35,7 +35,7 @@ import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.datausage.lib.NetworkUsageData import com.android.settings.datausage.lib.NetworkUsageData
import com.android.settings.network.MobileNetworkRepository import com.android.settings.network.MobileNetworkRepository
import com.android.settings.network.SubscriptionUtil import com.android.settings.network.SubscriptionUtil
import com.android.settings.network.mobileDataEnabledFlow import com.android.settings.network.telephony.requireSubscriptionManager
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
@@ -113,8 +113,8 @@ open class DataUsageList : DashboardFragment() {
override fun onViewCreated(v: View, savedInstanceState: Bundle?) { override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
super.onViewCreated(v, savedInstanceState) super.onViewCreated(v, savedInstanceState)
requireContext().mobileDataEnabledFlow(subId) billingCycleRepository.isModifiableFlow(subId)
.collectLatestWithLifecycle(viewLifecycleOwner) { updatePolicy() } .collectLatestWithLifecycle(viewLifecycleOwner, action = ::updatePolicy)
val template = template ?: return val template = template ?: return
viewModel.templateFlow.value = template viewModel.templateFlow.value = template
@@ -163,16 +163,14 @@ open class DataUsageList : DashboardFragment() {
} }
/** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */ /** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */
private fun updatePolicy() { private fun updatePolicy(isModifiable: Boolean) {
val isBillingCycleModifiable = isBillingCycleModifiable() val isBillingCycleModifiable = isModifiable && isActiveSubscription()
dataUsageListHeaderController?.setConfigButtonVisible(isBillingCycleModifiable) dataUsageListHeaderController?.setConfigButtonVisible(isBillingCycleModifiable)
chartDataUsagePreferenceController?.setBillingCycleModifiable(isBillingCycleModifiable) chartDataUsagePreferenceController?.setBillingCycleModifiable(isBillingCycleModifiable)
} }
private fun isBillingCycleModifiable(): Boolean = private fun isActiveSubscription(): Boolean =
billingCycleRepository.isModifiable(subId) && requireContext().requireSubscriptionManager().getActiveSubscriptionInfo(subId) != null
requireContext().getSystemService(SubscriptionManager::class.java)!!
.getActiveSubscriptionInfo(subId) != null
/** /**
* Updates the chart and detail data when initial loaded or selected cycle changed. * Updates the chart and detail data when initial loaded or selected cycle changed.

View File

@@ -19,10 +19,15 @@ package com.android.settings.datausage.lib
import android.content.Context import android.content.Context
import android.os.INetworkManagementService import android.os.INetworkManagementService
import android.os.ServiceManager import android.os.ServiceManager
import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
import androidx.annotation.OpenForTesting import androidx.annotation.OpenForTesting
import com.android.settings.network.telephony.TelephonyRepository
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@OpenForTesting @OpenForTesting
open class BillingCycleRepository @JvmOverloads constructor( open class BillingCycleRepository @JvmOverloads constructor(
@@ -31,12 +36,14 @@ open class BillingCycleRepository @JvmOverloads constructor(
INetworkManagementService.Stub.asInterface( INetworkManagementService.Stub.asInterface(
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE) ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)
), ),
private val telephonyRepository: TelephonyRepository = TelephonyRepository(context),
) { ) {
private val userManager = context.userManager private val userManager = context.userManager
private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
fun isModifiable(subId: Int): Boolean = fun isModifiableFlow(subId: Int): Flow<Boolean> =
isBandwidthControlEnabled() && userManager.isAdminUser && isDataEnabled(subId) telephonyRepository.isDataEnabledFlow(subId).map { isDataEnabled ->
isDataEnabled && isBandwidthControlEnabled() && userManager.isAdminUser
}.conflate().flowOn(Dispatchers.Default)
open fun isBandwidthControlEnabled(): Boolean = try { open fun isBandwidthControlEnabled(): Boolean = try {
networkService.isBandwidthControlEnabled networkService.isBandwidthControlEnabled
@@ -45,10 +52,6 @@ open class BillingCycleRepository @JvmOverloads constructor(
false false
} }
private fun isDataEnabled(subId: Int): Boolean =
telephonyManager.createForSubscriptionId(subId)
.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER)
companion object { companion object {
private const val TAG = "BillingCycleRepository" private const val TAG = "BillingCycleRepository"
} }

View File

@@ -29,10 +29,12 @@ import kotlinx.coroutines.channels.ProducerScope
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.catch
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
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
import kotlinx.coroutines.flow.onEach
class TelephonyRepository( class TelephonyRepository(
private val context: Context, private val context: Context,
@@ -64,19 +66,21 @@ class TelephonyRepository(
telephonyManager.setMobileDataPolicyEnabled(policy, enabled) telephonyManager.setMobileDataPolicyEnabled(policy, enabled)
} }
fun isDataEnabled( fun isDataEnabledFlow(subId: Int): Flow<Boolean> {
subId: Int,
): Flow<Boolean> {
if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
Log.d(TAG, "register mobileDataEnabledFlow: [$subId]")
return context.mobileDataEnabledFlow(subId) return context.mobileDataEnabledFlow(subId)
.map { .map {
Log.d(TAG, "mobileDataEnabledFlow: receive mobile data [$subId] start")
val telephonyManager = context.telephonyManager(subId) val telephonyManager = context.telephonyManager(subId)
telephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) telephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER)
.also { Log.d(TAG, "mobileDataEnabledFlow: [$subId] isDataEnabled(): $it") }
} }
.catch {
Log.w(TAG, "[$subId] isDataEnabledFlow: exception", it)
emit(false)
}
.onEach { Log.d(TAG, "[$subId] isDataEnabledFlow: isDataEnabled() = $it") }
.conflate()
.flowOn(Dispatchers.Default)
} }
fun setMobileData( fun setMobileData(
@@ -100,6 +104,7 @@ class TelephonyRepository(
wifiPickerTrackerHelper.setCarrierNetworkEnabled(enabled) wifiPickerTrackerHelper.setCarrierNetworkEnabled(enabled)
} }
} }
private companion object { private companion object {
private const val TAG = "TelephonyRepository" private const val TAG = "TelephonyRepository"
} }

View File

@@ -36,11 +36,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settings.R import com.android.settings.R
@@ -62,7 +62,6 @@ import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBool
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@@ -207,7 +206,7 @@ fun MobileDataSectionImpl(
}.collectAsStateWithLifecycle(initialValue = null) }.collectAsStateWithLifecycle(initialValue = null)
val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) { val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) {
TelephonyRepository(context).isDataEnabled(mobileDataSelectedId.intValue) TelephonyRepository(context).isDataEnabledFlow(mobileDataSelectedId.intValue)
}.collectAsStateWithLifecycle(initialValue = false) }.collectAsStateWithLifecycle(initialValue = false)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()

View File

@@ -27,6 +27,8 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R import com.android.settings.R
import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.BillingCycleRepository
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -39,7 +41,9 @@ class BillingCyclePreferenceTest {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
private val mockBillingCycleRepository = mock<BillingCycleRepository>() private val mockBillingCycleRepository = mock<BillingCycleRepository> {
on { isModifiableFlow(SUB_ID) } doReturn emptyFlow()
}
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
@@ -56,7 +60,7 @@ class BillingCyclePreferenceTest {
@Test @Test
fun setTemplate_modifiable_enabled() { fun setTemplate_modifiable_enabled() {
mockBillingCycleRepository.stub { mockBillingCycleRepository.stub {
on { isModifiable(SUB_ID) } doReturn true on { isModifiableFlow(SUB_ID) } doReturn flowOf(true)
} }
setTemplate() setTemplate()
@@ -67,7 +71,7 @@ class BillingCyclePreferenceTest {
@Test @Test
fun setTemplate_notModifiable_notEnabled() { fun setTemplate_notModifiable_notEnabled() {
mockBillingCycleRepository.stub { mockBillingCycleRepository.stub {
on { isModifiable(SUB_ID) } doReturn false on { isModifiableFlow(SUB_ID) } doReturn flowOf(false)
} }
setTemplate() setTemplate()

View File

@@ -22,8 +22,10 @@ import android.os.UserManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
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.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
@@ -55,43 +57,43 @@ class BillingCycleRepositoryTest {
private val repository = BillingCycleRepository(context, mockNetworkManagementService) private val repository = BillingCycleRepository(context, mockNetworkManagementService)
@Test @Test
fun isModifiable_bandwidthControlDisabled_returnFalse() { fun isModifiable_bandwidthControlDisabled_returnFalse() = runBlocking {
whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(false) whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(false)
val modifiable = repository.isModifiable(SUB_ID) val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull()
assertThat(modifiable).isFalse() assertThat(modifiable).isFalse()
} }
@Test @Test
fun isModifiable_notAdminUser_returnFalse() { fun isModifiable_notAdminUser_returnFalse() = runBlocking {
whenever(mockUserManager.isAdminUser).thenReturn(false) whenever(mockUserManager.isAdminUser).thenReturn(false)
val modifiable = repository.isModifiable(SUB_ID) val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull()
assertThat(modifiable).isFalse() assertThat(modifiable).isFalse()
} }
@Test @Test
fun isModifiable_dataDisabled_returnFalse() { fun isModifiable_dataDisabled_returnFalse() = runBlocking {
whenever( whenever(
mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER)
).thenReturn(false) ).thenReturn(false)
val modifiable = repository.isModifiable(SUB_ID) val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull()
assertThat(modifiable).isFalse() assertThat(modifiable).isFalse()
} }
@Test @Test
fun isModifiable_meetAllRequirements_returnTrue() { fun isModifiable_meetAllRequirements_returnTrue() = runBlocking {
whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(true) whenever(mockNetworkManagementService.isBandwidthControlEnabled).thenReturn(true)
whenever(mockUserManager.isAdminUser).thenReturn(true) whenever(mockUserManager.isAdminUser).thenReturn(true)
whenever( whenever(
mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) mockTelephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER)
).thenReturn(true) ).thenReturn(true)
val modifiable = repository.isModifiable(SUB_ID) val modifiable = repository.isModifiableFlow(SUB_ID).firstWithTimeoutOrNull()
assertThat(modifiable).isTrue() assertThat(modifiable).isTrue()
} }

View File

@@ -93,7 +93,7 @@ class TelephonyRepositoryTest {
@Test @Test
fun isDataEnabled_invalidSub_returnFalse() = runBlocking { fun isDataEnabled_invalidSub_returnFalse() = runBlocking {
val state = repository.isDataEnabled( val state = repository.isDataEnabledFlow(
subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID,
) )
@@ -108,9 +108,7 @@ class TelephonyRepositoryTest {
} doReturn true } doReturn true
} }
val state = repository.isDataEnabled( val state = repository.isDataEnabledFlow(subId = SUB_ID)
subId = SUB_ID,
)
assertThat(state.firstWithTimeoutOrNull()).isTrue() assertThat(state.firstWithTimeoutOrNull()).isTrue()
} }