Merge "Optimize DataUsagePreferenceController" into main

This commit is contained in:
Chaohui Wang
2023-10-12 02:58:54 +00:00
committed by Android (Google) Code Review
10 changed files with 103 additions and 98 deletions

View File

@@ -74,7 +74,7 @@ open class ChartDataUsagePreferenceController(context: Context, preferenceKey: S
preference.setTime(startTime, endTime) preference.setTime(startTime, endTime)
lifecycleScope.launch { lifecycleScope.launch {
val chartData = withContext(Dispatchers.Default) { val chartData = withContext(Dispatchers.Default) {
repository.querySummary(startTime, endTime) repository.queryChartData(startTime, endTime)
} }
preference.setNetworkCycleData(chartData) preference.setNetworkCycleData(chartData)
} }

View File

@@ -195,8 +195,7 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl
lastDisplayedUsageData = usageData lastDisplayedUsageData = usageData
Log.d(TAG, "showing cycle $usageData") Log.d(TAG, "showing cycle $usageData")
val totalPhrase = DataUsageUtils.formatDataUsage(requireContext(), usageData.usage) usageAmount.title = usageData.getDataUsedString(requireContext())
usageAmount.title = getString(R.string.data_used_template, totalPhrase)
updateChart(usageData) updateChart(usageData)
updateApps(usageData) updateApps(usageData)

View File

@@ -23,15 +23,13 @@ import android.net.NetworkTemplate
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Range import android.util.Range
import com.android.settingslib.NetworkPolicyEditor import com.android.settingslib.NetworkPolicyEditor
import kotlinx.coroutines.async import com.android.settingslib.spa.framework.util.asyncMap
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
interface INetworkCycleDataRepository { interface INetworkCycleDataRepository {
suspend fun loadCycles(): List<NetworkUsageData> suspend fun loadCycles(): List<NetworkUsageData>
fun getCycles(): List<Range<Long>> fun getCycles(): List<Range<Long>>
fun getPolicy(): NetworkPolicy? fun getPolicy(): NetworkPolicy?
suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData? suspend fun queryChartData(startTime: Long, endTime: Long): NetworkCycleChartData?
} }
class NetworkCycleDataRepository( class NetworkCycleDataRepository(
@@ -46,6 +44,8 @@ class NetworkCycleDataRepository(
override suspend fun loadCycles(): List<NetworkUsageData> = override suspend fun loadCycles(): List<NetworkUsageData> =
getCycles().queryUsage().filter { it.usage > 0 } getCycles().queryUsage().filter { it.usage > 0 }
fun loadFirstCycle(): NetworkUsageData? = getCycles().firstOrNull()?.let { queryUsage(it) }
override fun getCycles(): List<Range<Long>> { override fun getCycles(): List<Range<Long>> {
val policy = getPolicy() ?: return queryCyclesAsFourWeeks() val policy = getPolicy() ?: return queryCyclesAsFourWeeks()
return policy.cycleIterator().asSequence().map { return policy.cycleIterator().asSequence().map {
@@ -68,7 +68,7 @@ class NetworkCycleDataRepository(
getPolicy(networkTemplate) getPolicy(networkTemplate)
} }
override suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData? { override suspend fun queryChartData(startTime: Long, endTime: Long): NetworkCycleChartData? {
val usage = networkStatsRepository.querySummaryForDevice(startTime, endTime) val usage = networkStatsRepository.querySummaryForDevice(startTime, endTime)
if (usage > 0L) { if (usage > 0L) {
return NetworkCycleChartData( return NetworkCycleChartData(
@@ -83,17 +83,14 @@ class NetworkCycleDataRepository(
return null return null
} }
private suspend fun List<Range<Long>>.queryUsage(): List<NetworkUsageData> = coroutineScope { private suspend fun List<Range<Long>>.queryUsage(): List<NetworkUsageData> =
map { range -> asyncMap { queryUsage(it) }
async {
NetworkUsageData( fun queryUsage(range: Range<Long>) = NetworkUsageData(
startTime = range.lower, startTime = range.lower,
endTime = range.upper, endTime = range.upper,
usage = networkStatsRepository.querySummaryForDevice(range.lower, range.upper), usage = networkStatsRepository.querySummaryForDevice(range.lower, range.upper),
) )
}
}.awaitAll()
}
private fun bucketRange(startTime: Long, endTime: Long, bucketSize: Long): List<Range<Long>> { private fun bucketRange(startTime: Long, endTime: Long, bucketSize: Long): List<Range<Long>> {
val buckets = mutableListOf<Range<Long>>() val buckets = mutableListOf<Range<Long>>()

View File

@@ -16,7 +16,11 @@
package com.android.settings.datausage.lib package com.android.settings.datausage.lib
import android.content.Context
import android.text.format.DateUtils
import android.util.Range import android.util.Range
import com.android.settings.R
import com.android.settings.datausage.DataUsageUtils
/** /**
* Base data structure representing usage data in a period. * Base data structure representing usage data in a period.
@@ -27,6 +31,21 @@ data class NetworkUsageData(
val usage: Long, val usage: Long,
) { ) {
val timeRange = Range(startTime, endTime) val timeRange = Range(startTime, endTime)
fun formatStartDate(context: Context): String =
DateUtils.formatDateTime(context, startTime, DATE_FORMAT)
fun formatDateRange(context: Context): String =
DateUtils.formatDateRange(context, startTime, endTime, DATE_FORMAT)
fun formatUsage(context: Context): CharSequence = DataUsageUtils.formatDataUsage(context, usage)
fun getDataUsedString(context: Context): String =
context.getString(R.string.data_used_template, formatUsage(context))
private companion object {
const val DATE_FORMAT = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_ABBREV_MONTH
}
} }
fun List<NetworkUsageData>.aggregate(): NetworkUsageData? = when { fun List<NetworkUsageData>.aggregate(): NetworkUsageData? = when {

View File

@@ -31,7 +31,8 @@ import androidx.preference.PreferenceScreen
import com.android.settings.R import com.android.settings.R
import com.android.settings.datausage.DataUsageUtils import com.android.settings.datausage.DataUsageUtils
import com.android.settings.datausage.lib.DataUsageLib import com.android.settings.datausage.lib.DataUsageLib
import com.android.settingslib.net.DataUsageController import com.android.settings.datausage.lib.NetworkCycleDataRepository
import com.android.settings.datausage.lib.NetworkStatsRepository.Companion.AllTimeRange
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -45,9 +46,6 @@ class DataUsagePreferenceController(context: Context, key: String) :
private lateinit var preference: Preference private lateinit var preference: Preference
private var networkTemplate: NetworkTemplate? = null private var networkTemplate: NetworkTemplate? = null
@VisibleForTesting
var dataUsageControllerFactory: (Context) -> DataUsageController = { DataUsageController(it) }
fun init(subId: Int) { fun init(subId: Int) {
mSubId = subId mSubId = subId
} }
@@ -103,25 +101,21 @@ class DataUsagePreferenceController(context: Context, key: String) :
else -> null else -> null
} }
@VisibleForTesting
fun createNetworkCycleDataRepository(): NetworkCycleDataRepository? =
networkTemplate?.let { NetworkCycleDataRepository(mContext, it) }
private fun getDataUsageSummary(): String? { private fun getDataUsageSummary(): String? {
val networkTemplate = networkTemplate ?: return null val repository = createNetworkCycleDataRepository() ?: return null
val controller = dataUsageControllerFactory(mContext).apply { repository.loadFirstCycle()?.takeIf { it.usage > 0 }?.let { usageData ->
setSubscriptionId(mSubId)
}
val usageInfo = controller.getDataUsageInfo(networkTemplate)
if (usageInfo != null && usageInfo.usageLevel > 0) {
return mContext.getString( return mContext.getString(
R.string.data_usage_template, R.string.data_usage_template,
DataUsageUtils.formatDataUsage(mContext, usageInfo.usageLevel), usageData.formatUsage(mContext),
usageInfo.period, usageData.formatDateRange(mContext),
) )
} }
return controller.getHistoricalUsageLevel(networkTemplate).takeIf { it > 0 }?.let { return repository.queryUsage(AllTimeRange).takeIf { it.usage > 0 }
mContext.getString( ?.getDataUsedString(mContext)
R.string.data_used_template,
DataUsageUtils.formatDataUsage(mContext, it),
)
}
} }
} }

View File

@@ -19,8 +19,6 @@ package com.android.settings.spa.app.appinfo
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.net.NetworkTemplate import android.net.NetworkTemplate
import android.text.format.DateUtils
import android.text.format.Formatter
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -114,8 +112,8 @@ private class AppDataUsagePresenter(
} else { } else {
context.getString( context.getString(
R.string.data_summary_format, R.string.data_summary_format,
Formatter.formatFileSize(context, appUsageData.usage, Formatter.FLAG_IEC_UNITS), appUsageData.formatUsage(context),
DateUtils.formatDateTime(context, appUsageData.startTime, DATE_FORMAT), appUsageData.formatStartDate(context),
) )
} }
} }
@@ -128,8 +126,4 @@ private class AppDataUsagePresenter(
AppInfoSettingsProvider.METRICS_CATEGORY, AppInfoSettingsProvider.METRICS_CATEGORY,
) )
} }
private companion object {
const val DATE_FORMAT = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_ABBREV_MONTH
}
} }

View File

@@ -43,7 +43,7 @@ class ChartDataUsagePreferenceControllerTest {
override fun getCycles() = emptyList<Range<Long>>() override fun getCycles() = emptyList<Range<Long>>()
override fun getPolicy() = null override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = when { override suspend fun queryChartData(startTime: Long, endTime: Long) = when {
startTime == START_TIME && endTime == END_TIME -> CycleChartDate startTime == START_TIME && endTime == END_TIME -> CycleChartDate
else -> null else -> null
} }

View File

@@ -51,7 +51,7 @@ class DataUsageListHeaderControllerTest {
override suspend fun loadCycles() = emptyList<NetworkUsageData>() override suspend fun loadCycles() = emptyList<NetworkUsageData>()
override fun getCycles() = emptyList<Range<Long>>() override fun getCycles() = emptyList<Range<Long>>()
override fun getPolicy() = null override fun getPolicy() = null
override suspend fun querySummary(startTime: Long, endTime: Long) = null override suspend fun queryChartData(startTime: Long, endTime: Long) = null
} }
private val header = private val header =

View File

@@ -98,7 +98,7 @@ class NetworkCycleDataRepositoryTest {
@Test @Test
fun querySummary() = runTest { fun querySummary() = runTest {
val summary = repository.querySummary(CYCLE3_START_TIME, CYCLE4_END_TIME) val summary = repository.queryChartData(CYCLE3_START_TIME, CYCLE4_END_TIME)
assertThat(summary).isEqualTo( assertThat(summary).isEqualTo(
NetworkCycleChartData( NetworkCycleChartData(

View File

@@ -25,7 +25,7 @@ import android.util.DataUnit
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceManager
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.dx.mockito.inline.extended.ExtendedMockito import com.android.dx.mockito.inline.extended.ExtendedMockito
@@ -33,45 +33,46 @@ import com.android.settings.core.BasePreferenceController.AVAILABLE
import com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE import com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE
import com.android.settings.datausage.DataUsageUtils import com.android.settings.datausage.DataUsageUtils
import com.android.settings.datausage.lib.DataUsageLib import com.android.settings.datausage.lib.DataUsageLib
import com.android.settingslib.net.DataUsageController import com.android.settings.datausage.lib.NetworkCycleDataRepository
import com.android.settingslib.net.DataUsageController.DataUsageInfo import com.android.settings.datausage.lib.NetworkUsageData
import com.android.settingslib.spa.testutils.waitUntil import com.android.settingslib.spa.testutils.waitUntil
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest 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.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.verify
import org.mockito.MockitoSession import org.mockito.MockitoSession
import org.mockito.Spy import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness import org.mockito.quality.Strictness
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DataUsagePreferenceControllerTest { class DataUsagePreferenceControllerTest {
private lateinit var mockSession: MockitoSession private lateinit var mockSession: MockitoSession
@Spy private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
private val context: Context = ApplicationProvider.getApplicationContext() doNothing().whenever(mock).startActivity(any())
}
private lateinit var controller: DataUsagePreferenceController private val preference = Preference(context).apply { key = TEST_KEY }
private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
private val networkTemplate = mock<NetworkTemplate>()
private val repository = mock<NetworkCycleDataRepository> {
on { queryUsage(any()) } doReturn NetworkUsageData(START_TIME, END_TIME, 0L)
}
private val preference = Preference(context) private val controller = spy(DataUsagePreferenceController(context, TEST_KEY)) {
doReturn(repository).whenever(mock).createNetworkCycleDataRepository()
@Mock }
private lateinit var networkTemplate: NetworkTemplate
@Mock
private lateinit var dataUsageController: DataUsageController
@Mock
private lateinit var preferenceScreen: PreferenceScreen
@Before @Before
fun setUp() { fun setUp() {
@@ -85,17 +86,15 @@ class DataUsagePreferenceControllerTest {
whenever(SubscriptionManager.isValidSubscriptionId(SUB_ID)).thenReturn(true) whenever(SubscriptionManager.isValidSubscriptionId(SUB_ID)).thenReturn(true)
ExtendedMockito.doReturn(true).`when` { DataUsageUtils.hasMobileData(context) } ExtendedMockito.doReturn(true).`when` { DataUsageUtils.hasMobileData(context) }
ExtendedMockito.doReturn(networkTemplate) ExtendedMockito.doReturn(networkTemplate).`when` {
.`when` { DataUsageLib.getMobileTemplate(context, SUB_ID) } DataUsageLib.getMobileTemplate(context, SUB_ID)
preference.key = TEST_KEY }
whenever(preferenceScreen.findPreference<Preference>(TEST_KEY)).thenReturn(preference)
controller = preferenceScreen.addPreference(preference)
DataUsagePreferenceController(context, TEST_KEY).apply { controller.apply {
init(SUB_ID) init(SUB_ID)
displayPreference(preferenceScreen) displayPreference(preferenceScreen)
dataUsageControllerFactory = { dataUsageController } }
}
} }
@After @After
@@ -116,26 +115,25 @@ class DataUsagePreferenceControllerTest {
} }
@Test @Test
fun handlePreferenceTreeClick_startActivity() = runTest { fun handlePreferenceTreeClick_startActivity() = runBlocking {
val usageInfo = DataUsageInfo().apply { val usageData = NetworkUsageData(START_TIME, END_TIME, 1L)
usageLevel = DataUnit.MEBIBYTES.toBytes(1) repository.stub {
on { loadFirstCycle() } doReturn usageData
} }
whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
doNothing().`when`(context).startActivity(any())
controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED)) controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
waitUntil { preference.summary != null } waitUntil { preference.summary != null }
controller.handlePreferenceTreeClick(preference) controller.handlePreferenceTreeClick(preference)
val captor = ArgumentCaptor.forClass(Intent::class.java) val intent = argumentCaptor<Intent> {
verify(context).startActivity(captor.capture()) verify(context).startActivity(capture())
val intent = captor.value }.firstValue
assertThat(intent.action).isEqualTo(Settings.ACTION_MOBILE_DATA_USAGE) assertThat(intent.action).isEqualTo(Settings.ACTION_MOBILE_DATA_USAGE)
assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID) assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID)
} }
@Test @Test
fun updateState_invalidSubId_disabled() = runTest { fun updateState_invalidSubId_disabled() = runBlocking {
controller.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID) controller.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED)) controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
@@ -144,9 +142,11 @@ class DataUsagePreferenceControllerTest {
} }
@Test @Test
fun updateState_noUsageData_shouldDisablePreference() = runTest { fun updateState_noUsageData_shouldDisablePreference() = runBlocking {
val usageInfo = DataUsageInfo() val usageData = NetworkUsageData(START_TIME, END_TIME, 0L)
whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo) repository.stub {
on { loadFirstCycle() } doReturn usageData
}
controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED)) controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
@@ -154,11 +154,11 @@ class DataUsagePreferenceControllerTest {
} }
@Test @Test
fun updateState_shouldUseIecUnit() = runTest { fun updateState_shouldUseIecUnit() = runBlocking {
val usageInfo = DataUsageInfo().apply { val usageData = NetworkUsageData(START_TIME, END_TIME, DataUnit.MEBIBYTES.toBytes(1))
usageLevel = DataUnit.MEBIBYTES.toBytes(1) repository.stub {
on { loadFirstCycle() } doReturn usageData
} }
whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED)) controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
@@ -168,5 +168,7 @@ class DataUsagePreferenceControllerTest {
private companion object { private companion object {
const val TEST_KEY = "test_key" const val TEST_KEY = "test_key"
const val SUB_ID = 2 const val SUB_ID = 2
const val START_TIME = 10L
const val END_TIME = 30L
} }
} }