Merge "Optimize DataUsagePreferenceController" into main
diff --git a/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
index 0479be4..49235b5 100644
--- a/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
+++ b/src/com/android/settings/datausage/ChartDataUsagePreferenceController.kt
@@ -74,7 +74,7 @@
         preference.setTime(startTime, endTime)
         lifecycleScope.launch {
             val chartData = withContext(Dispatchers.Default) {
-                repository.querySummary(startTime, endTime)
+                repository.queryChartData(startTime, endTime)
             }
             preference.setNetworkCycleData(chartData)
         }
diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt
index 9ac7161..1eb883c 100644
--- a/src/com/android/settings/datausage/DataUsageList.kt
+++ b/src/com/android/settings/datausage/DataUsageList.kt
@@ -195,8 +195,7 @@
         lastDisplayedUsageData = usageData
         Log.d(TAG, "showing cycle $usageData")
 
-        val totalPhrase = DataUsageUtils.formatDataUsage(requireContext(), usageData.usage)
-        usageAmount.title = getString(R.string.data_used_template, totalPhrase)
+        usageAmount.title = usageData.getDataUsedString(requireContext())
 
         updateChart(usageData)
         updateApps(usageData)
diff --git a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt
index 1ff7a81..4e97c6d 100644
--- a/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt
+++ b/src/com/android/settings/datausage/lib/NetworkCycleDataRepository.kt
@@ -23,15 +23,13 @@
 import android.text.format.DateUtils
 import android.util.Range
 import com.android.settingslib.NetworkPolicyEditor
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
+import com.android.settingslib.spa.framework.util.asyncMap
 
 interface INetworkCycleDataRepository {
     suspend fun loadCycles(): List<NetworkUsageData>
     fun getCycles(): List<Range<Long>>
     fun getPolicy(): NetworkPolicy?
-    suspend fun querySummary(startTime: Long, endTime: Long): NetworkCycleChartData?
+    suspend fun queryChartData(startTime: Long, endTime: Long): NetworkCycleChartData?
 }
 
 class NetworkCycleDataRepository(
@@ -46,6 +44,8 @@
     override suspend fun loadCycles(): List<NetworkUsageData> =
         getCycles().queryUsage().filter { it.usage > 0 }
 
+    fun loadFirstCycle(): NetworkUsageData? = getCycles().firstOrNull()?.let { queryUsage(it) }
+
     override fun getCycles(): List<Range<Long>> {
         val policy = getPolicy() ?: return queryCyclesAsFourWeeks()
         return policy.cycleIterator().asSequence().map {
@@ -68,7 +68,7 @@
             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)
         if (usage > 0L) {
             return NetworkCycleChartData(
@@ -83,17 +83,14 @@
         return null
     }
 
-    private suspend fun List<Range<Long>>.queryUsage(): List<NetworkUsageData> = coroutineScope {
-        map { range ->
-            async {
-                NetworkUsageData(
-                    startTime = range.lower,
-                    endTime = range.upper,
-                    usage = networkStatsRepository.querySummaryForDevice(range.lower, range.upper),
-                )
-            }
-        }.awaitAll()
-    }
+    private suspend fun List<Range<Long>>.queryUsage(): List<NetworkUsageData> =
+        asyncMap { queryUsage(it) }
+
+    fun queryUsage(range: Range<Long>) = NetworkUsageData(
+        startTime = range.lower,
+        endTime = range.upper,
+        usage = networkStatsRepository.querySummaryForDevice(range.lower, range.upper),
+    )
 
     private fun bucketRange(startTime: Long, endTime: Long, bucketSize: Long): List<Range<Long>> {
         val buckets = mutableListOf<Range<Long>>()
diff --git a/src/com/android/settings/datausage/lib/NetworkUsageData.kt b/src/com/android/settings/datausage/lib/NetworkUsageData.kt
index 5bdd7ed..93cde5f 100644
--- a/src/com/android/settings/datausage/lib/NetworkUsageData.kt
+++ b/src/com/android/settings/datausage/lib/NetworkUsageData.kt
@@ -16,7 +16,11 @@
 
 package com.android.settings.datausage.lib
 
+import android.content.Context
+import android.text.format.DateUtils
 import android.util.Range
+import com.android.settings.R
+import com.android.settings.datausage.DataUsageUtils
 
 /**
  * Base data structure representing usage data in a period.
@@ -27,6 +31,21 @@
     val usage: Long,
 ) {
     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 {
diff --git a/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt
index 34433c4..d133955 100644
--- a/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt
+++ b/src/com/android/settings/network/telephony/DataUsagePreferenceController.kt
@@ -31,7 +31,8 @@
 import com.android.settings.R
 import com.android.settings.datausage.DataUsageUtils
 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.launch
 import kotlinx.coroutines.withContext
@@ -45,9 +46,6 @@
     private lateinit var preference: Preference
     private var networkTemplate: NetworkTemplate? = null
 
-    @VisibleForTesting
-    var dataUsageControllerFactory: (Context) -> DataUsageController = { DataUsageController(it) }
-
     fun init(subId: Int) {
         mSubId = subId
     }
@@ -103,25 +101,21 @@
         else -> null
     }
 
+    @VisibleForTesting
+    fun createNetworkCycleDataRepository(): NetworkCycleDataRepository? =
+        networkTemplate?.let { NetworkCycleDataRepository(mContext, it) }
+
     private fun getDataUsageSummary(): String? {
-        val networkTemplate = networkTemplate ?: return null
-        val controller = dataUsageControllerFactory(mContext).apply {
-            setSubscriptionId(mSubId)
-        }
-        val usageInfo = controller.getDataUsageInfo(networkTemplate)
-        if (usageInfo != null && usageInfo.usageLevel > 0) {
+        val repository = createNetworkCycleDataRepository() ?: return null
+        repository.loadFirstCycle()?.takeIf { it.usage > 0 }?.let { usageData ->
             return mContext.getString(
                 R.string.data_usage_template,
-                DataUsageUtils.formatDataUsage(mContext, usageInfo.usageLevel),
-                usageInfo.period,
+                usageData.formatUsage(mContext),
+                usageData.formatDateRange(mContext),
             )
         }
 
-        return controller.getHistoricalUsageLevel(networkTemplate).takeIf { it > 0 }?.let {
-            mContext.getString(
-                R.string.data_used_template,
-                DataUsageUtils.formatDataUsage(mContext, it),
-            )
-        }
+        return repository.queryUsage(AllTimeRange).takeIf { it.usage > 0 }
+            ?.getDataUsedString(mContext)
     }
 }
diff --git a/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
index 6b14da0..ceb3986 100644
--- a/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppDataUsagePreference.kt
@@ -19,8 +19,6 @@
 import android.content.Context
 import android.content.pm.ApplicationInfo
 import android.net.NetworkTemplate
-import android.text.format.DateUtils
-import android.text.format.Formatter
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
@@ -114,8 +112,8 @@
         } else {
             context.getString(
                 R.string.data_summary_format,
-                Formatter.formatFileSize(context, appUsageData.usage, Formatter.FLAG_IEC_UNITS),
-                DateUtils.formatDateTime(context, appUsageData.startTime, DATE_FORMAT),
+                appUsageData.formatUsage(context),
+                appUsageData.formatStartDate(context),
             )
         }
     }
@@ -128,8 +126,4 @@
             AppInfoSettingsProvider.METRICS_CATEGORY,
         )
     }
-
-    private companion object {
-        const val DATE_FORMAT = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_ABBREV_MONTH
-    }
 }
diff --git a/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt
index e0eb789..ae09ef9 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/ChartDataUsagePreferenceControllerTest.kt
@@ -43,7 +43,7 @@
         override fun getCycles() = emptyList<Range<Long>>()
         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
             else -> null
         }
diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt
index 581f7ba..3580e68 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt
@@ -51,7 +51,7 @@
         override suspend fun loadCycles() = emptyList<NetworkUsageData>()
         override fun getCycles() = emptyList<Range<Long>>()
         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 =
diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt
index feb195d..5678503 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/lib/NetworkCycleDataRepositoryTest.kt
@@ -98,7 +98,7 @@
 
     @Test
     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(
             NetworkCycleChartData(
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt
index a6d1531..069145d 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/DataUsagePreferenceControllerTest.kt
@@ -25,7 +25,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.preference.Preference
-import androidx.preference.PreferenceScreen
+import androidx.preference.PreferenceManager
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.dx.mockito.inline.extended.ExtendedMockito
@@ -33,45 +33,46 @@
 import com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE
 import com.android.settings.datausage.DataUsageUtils
 import com.android.settings.datausage.lib.DataUsageLib
-import com.android.settingslib.net.DataUsageController
-import com.android.settingslib.net.DataUsageController.DataUsageInfo
+import com.android.settings.datausage.lib.NetworkCycleDataRepository
+import com.android.settings.datausage.lib.NetworkUsageData
 import com.android.settingslib.spa.testutils.waitUntil
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 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.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.Mockito.`when` as whenever
 
 @RunWith(AndroidJUnit4::class)
 class DataUsagePreferenceControllerTest {
 
     private lateinit var mockSession: MockitoSession
 
-    @Spy
-    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val context: Context = spy(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)
-
-    @Mock
-    private lateinit var networkTemplate: NetworkTemplate
-
-    @Mock
-    private lateinit var dataUsageController: DataUsageController
-
-    @Mock
-    private lateinit var preferenceScreen: PreferenceScreen
+    private val controller = spy(DataUsagePreferenceController(context, TEST_KEY)) {
+        doReturn(repository).whenever(mock).createNetworkCycleDataRepository()
+    }
 
     @Before
     fun setUp() {
@@ -85,17 +86,15 @@
 
         whenever(SubscriptionManager.isValidSubscriptionId(SUB_ID)).thenReturn(true)
         ExtendedMockito.doReturn(true).`when` { DataUsageUtils.hasMobileData(context) }
-        ExtendedMockito.doReturn(networkTemplate)
-            .`when` { DataUsageLib.getMobileTemplate(context, SUB_ID) }
-        preference.key = TEST_KEY
-        whenever(preferenceScreen.findPreference<Preference>(TEST_KEY)).thenReturn(preference)
+        ExtendedMockito.doReturn(networkTemplate).`when` {
+            DataUsageLib.getMobileTemplate(context, SUB_ID)
+        }
 
-        controller =
-            DataUsagePreferenceController(context, TEST_KEY).apply {
-                init(SUB_ID)
-                displayPreference(preferenceScreen)
-                dataUsageControllerFactory = { dataUsageController }
-            }
+        preferenceScreen.addPreference(preference)
+        controller.apply {
+            init(SUB_ID)
+            displayPreference(preferenceScreen)
+        }
     }
 
     @After
@@ -116,26 +115,25 @@
     }
 
     @Test
-    fun handlePreferenceTreeClick_startActivity() = runTest {
-        val usageInfo = DataUsageInfo().apply {
-            usageLevel = DataUnit.MEBIBYTES.toBytes(1)
+    fun handlePreferenceTreeClick_startActivity() = runBlocking {
+        val usageData = NetworkUsageData(START_TIME, END_TIME, 1L)
+        repository.stub {
+            on { loadFirstCycle() } doReturn usageData
         }
-        whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
-        doNothing().`when`(context).startActivity(any())
         controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
         waitUntil { preference.summary != null }
 
         controller.handlePreferenceTreeClick(preference)
 
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(context).startActivity(captor.capture())
-        val intent = captor.value
+        val intent = argumentCaptor<Intent> {
+            verify(context).startActivity(capture())
+        }.firstValue
         assertThat(intent.action).isEqualTo(Settings.ACTION_MOBILE_DATA_USAGE)
         assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID)
     }
 
     @Test
-    fun updateState_invalidSubId_disabled() = runTest {
+    fun updateState_invalidSubId_disabled() = runBlocking {
         controller.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
 
         controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
@@ -144,9 +142,11 @@
     }
 
     @Test
-    fun updateState_noUsageData_shouldDisablePreference() = runTest {
-        val usageInfo = DataUsageInfo()
-        whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
+    fun updateState_noUsageData_shouldDisablePreference() = runBlocking {
+        val usageData = NetworkUsageData(START_TIME, END_TIME, 0L)
+        repository.stub {
+            on { loadFirstCycle() } doReturn usageData
+        }
 
         controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
 
@@ -154,11 +154,11 @@
     }
 
     @Test
-    fun updateState_shouldUseIecUnit() = runTest {
-        val usageInfo = DataUsageInfo().apply {
-            usageLevel = DataUnit.MEBIBYTES.toBytes(1)
+    fun updateState_shouldUseIecUnit() = runBlocking {
+        val usageData = NetworkUsageData(START_TIME, END_TIME, DataUnit.MEBIBYTES.toBytes(1))
+        repository.stub {
+            on { loadFirstCycle() } doReturn usageData
         }
-        whenever(dataUsageController.getDataUsageInfo(networkTemplate)).thenReturn(usageInfo)
 
         controller.onViewCreated(TestLifecycleOwner(initialState = Lifecycle.State.STARTED))
 
@@ -168,5 +168,7 @@
     private companion object {
         const val TEST_KEY = "test_key"
         const val SUB_ID = 2
+        const val START_TIME = 10L
+        const val END_TIME = 30L
     }
 }