summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chaohui Wang <chaohuiw@google.com> 2025-01-09 14:37:27 +0800
committer Chaohui Wang <chaohuiw@google.com> 2025-01-09 18:11:43 -0800
commit1a655df724a8883d6286001c60c83396e474a002 (patch)
tree330daeb3482f0585af632324d1367693da9e2296
parent17016bdc9083e70df007da9a632ae7e4f8cd445f (diff)
New AppStorageRepository to format app size
Change "480 B" to "480 byte". Bug: 321861088 Flag: EXEMPT bug fix Test: manual - on All apps and App info Test: unit test Change-Id: Ie90f8a522cf5982a998b20b4938290c714cdcedc
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt17
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt92
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt40
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt94
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt85
5 files changed, 228 insertions, 100 deletions
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
index 5b7e2a86135a..e6cc8a80ee38 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
@@ -24,6 +24,7 @@ import android.icu.text.NumberFormat
import android.icu.text.UnicodeSet
import android.icu.text.UnicodeSetSpanner
import android.icu.util.Measure
+import android.text.BidiFormatter
import android.text.format.Formatter
import android.text.format.Formatter.RoundedBytesResult
import java.math.BigDecimal
@@ -40,11 +41,17 @@ class BytesFormatter(resources: Resources) {
constructor(context: Context) : this(context.resources)
private val locale = resources.configuration.locales[0]
+ private val bidiFormatter = BidiFormatter.getInstance(locale)
fun format(bytes: Long, useCase: UseCase): String {
val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
val numberFormatter = getNumberFormatter(rounded.fractionDigits)
- return numberFormatter.formatRoundedBytesResult(rounded)
+ val formattedString = numberFormatter.formatRoundedBytesResult(rounded)
+ return if (useCase == UseCase.FileSize) {
+ formattedString.bidiWrap()
+ } else {
+ formattedString
+ }
}
fun formatWithUnits(bytes: Long, useCase: UseCase): Result {
@@ -74,6 +81,14 @@ class BytesFormatter(resources: Resources) {
}
}
+ /** Wraps the source string in bidi formatting characters in RTL locales. */
+ private fun String.bidiWrap(): String =
+ if (bidiFormatter.isRtlContext) {
+ bidiFormatter.unicodeWrap(this)
+ } else {
+ this
+ }
+
private companion object {
fun String.removeFirst(removed: String): String =
SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString()
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt
new file mode 100644
index 000000000000..6fd470c1e7aa
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2025 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.settingslib.spaprivileged.model.app
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.util.Log
+import com.android.settingslib.spaprivileged.framework.common.BytesFormatter
+import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
+
+/** A repository interface for accessing and formatting app storage information. */
+interface AppStorageRepository {
+ /**
+ * Formats the size of an application into a human-readable string.
+ *
+ * This function retrieves the total size of the application, including APK file and its
+ * associated data.
+ *
+ * This function takes an [ApplicationInfo] object as input and returns a formatted string
+ * representing the size of the application. The size is formatted in units like kB, MB, GB,
+ * etc.
+ *
+ * @param app The [ApplicationInfo] object representing the application.
+ * @return A formatted string representing the size of the application.
+ */
+ fun formatSize(app: ApplicationInfo): String
+
+ /**
+ * Formats the size about an application into a human-readable string.
+ *
+ * @param sizeBytes The size in bytes to format.
+ * @return A formatted string representing the size about application.
+ */
+ fun formatSizeBytes(sizeBytes: Long): String
+
+ /**
+ * Calculates the size of an application in bytes.
+ *
+ * This function retrieves the total size of the application, including APK file and its
+ * associated data.
+ *
+ * @param app The [ApplicationInfo] object representing the application.
+ * @return The total size of the application in bytes, or null if the size could not be
+ * determined.
+ */
+ fun calculateSizeBytes(app: ApplicationInfo): Long?
+}
+
+class AppStorageRepositoryImpl(context: Context) : AppStorageRepository {
+ private val storageStatsManager = context.storageStatsManager
+ private val bytesFormatter = BytesFormatter(context)
+
+ override fun formatSize(app: ApplicationInfo): String {
+ val sizeBytes = calculateSizeBytes(app)
+ return if (sizeBytes != null) formatSizeBytes(sizeBytes) else ""
+ }
+
+ override fun formatSizeBytes(sizeBytes: Long): String =
+ bytesFormatter.format(sizeBytes, BytesFormatter.UseCase.FileSize)
+
+ override fun calculateSizeBytes(app: ApplicationInfo): Long? =
+ try {
+ val stats =
+ storageStatsManager.queryStatsForPackage(
+ app.storageUuid,
+ app.packageName,
+ app.userHandle,
+ )
+ stats.codeBytes + stats.dataBytes
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to query stats", e)
+ null
+ }
+
+ companion object {
+ private const val TAG = "AppStorageRepository"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt
index 7a4f81cc1321..7c98e9cd813b 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt
@@ -16,42 +16,30 @@
package com.android.settingslib.spaprivileged.template.app
-import android.content.Context
import android.content.pm.ApplicationInfo
-import android.text.format.Formatter
-import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
+import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spaprivileged.framework.compose.placeholder
-import com.android.settingslib.spaprivileged.model.app.userHandle
+import com.android.settingslib.spaprivileged.model.app.AppStorageRepository
+import com.android.settingslib.spaprivileged.model.app.AppStorageRepositoryImpl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
-private const val TAG = "AppStorageSize"
-
@Composable
-fun ApplicationInfo.getStorageSize(): State<String> {
- val context = LocalContext.current
- return remember(this) {
- flow {
- val sizeBytes = calculateSizeBytes(context)
- this.emit(if (sizeBytes != null) Formatter.formatFileSize(context, sizeBytes) else "")
- }.flowOn(Dispatchers.IO)
- }.collectAsStateWithLifecycle(initialValue = placeholder())
-}
+fun ApplicationInfo.getStorageSize(): State<String> =
+ getStorageSize(rememberContext(::AppStorageRepositoryImpl))
-fun ApplicationInfo.calculateSizeBytes(context: Context): Long? {
- val storageStatsManager = context.storageStatsManager
- return try {
- val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle)
- stats.codeBytes + stats.dataBytes
- } catch (e: Exception) {
- Log.w(TAG, "Failed to query stats: $e")
- null
- }
+@VisibleForTesting
+@Composable
+fun ApplicationInfo.getStorageSize(appStorageRepository: AppStorageRepository): State<String> {
+ val app = this
+ return remember(app) {
+ flow { emit(appStorageRepository.formatSize(app)) }.flowOn(Dispatchers.Default)
+ }
+ .collectAsStateWithLifecycle(initialValue = placeholder())
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt
new file mode 100644
index 000000000000..e8ec974bb0b8
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2025 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.settingslib.spaprivileged.model.app
+
+import android.app.usage.StorageStats
+import android.app.usage.StorageStatsManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager.NameNotFoundException
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doThrow
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class AppStorageRepositoryTest {
+ private val app = ApplicationInfo().apply { storageUuid = UUID.randomUUID() }
+
+ private val mockStorageStatsManager =
+ mock<StorageStatsManager> {
+ on { queryStatsForPackage(app.storageUuid, app.packageName, app.userHandle) } doReturn
+ STATS
+ }
+
+ private val context: Context =
+ spy(ApplicationProvider.getApplicationContext()) {
+ on { storageStatsManager } doReturn mockStorageStatsManager
+ }
+
+ private val repository = AppStorageRepositoryImpl(context)
+
+ @Test
+ fun calculateSizeBytes() {
+ val sizeBytes = repository.calculateSizeBytes(app)
+
+ assertThat(sizeBytes).isEqualTo(120)
+ }
+
+ @Test
+ fun formatSize() {
+ val fileSize = repository.formatSize(app)
+
+ assertThat(fileSize).isEqualTo("120 byte")
+ }
+
+ @Test
+ fun formatSize_throwException() {
+ mockStorageStatsManager.stub {
+ on { queryStatsForPackage(app.storageUuid, app.packageName, app.userHandle) } doThrow
+ NameNotFoundException()
+ }
+
+ val fileSize = repository.formatSize(app)
+
+ assertThat(fileSize).isEqualTo("")
+ }
+
+ @Test
+ fun formatSizeBytes() {
+ val fileSize = repository.formatSizeBytes(120)
+
+ assertThat(fileSize).isEqualTo("120 byte")
+ }
+
+ companion object {
+ private val STATS =
+ StorageStats().apply {
+ codeBytes = 100
+ dataBytes = 20
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
index 60f3d0ce1be3..4f42c8254c39 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
@@ -16,98 +16,37 @@
package com.android.settingslib.spaprivileged.template.app
-import android.app.usage.StorageStats
-import android.app.usage.StorageStatsManager
-import android.content.Context
import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager.NameNotFoundException
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.compose.stateOf
-import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
-import com.android.settingslib.spaprivileged.model.app.userHandle
-import java.util.UUID
-import org.junit.Before
+import com.android.settingslib.spaprivileged.model.app.AppStorageRepository
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Spy
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.whenever
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import java.util.UUID
@RunWith(AndroidJUnit4::class)
class AppStorageSizeTest {
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
-
- @get:Rule
- val composeTestRule = createComposeRule()
+ @get:Rule val composeTestRule = createComposeRule()
- @Spy
- private val context: Context = ApplicationProvider.getApplicationContext()
-
- @Mock
- private lateinit var storageStatsManager: StorageStatsManager
-
- private val app = ApplicationInfo().apply {
- storageUuid = UUID.randomUUID()
- }
+ private val app = ApplicationInfo().apply { storageUuid = UUID.randomUUID() }
- @Before
- fun setUp() {
- whenever(context.storageStatsManager).thenReturn(storageStatsManager)
- whenever(
- storageStatsManager.queryStatsForPackage(
- app.storageUuid,
- app.packageName,
- app.userHandle,
- )
- ).thenReturn(STATS)
- }
+ private val mockAppStorageRepository =
+ mock<AppStorageRepository> { on { formatSize(app) } doReturn SIZE }
@Test
fun getStorageSize() {
var storageSize = stateOf("")
- composeTestRule.setContent {
- CompositionLocalProvider(LocalContext provides context) {
- storageSize = app.getStorageSize()
- }
- }
-
- composeTestRule.waitUntil { storageSize.value == "120 B" }
- }
-
- @Test
- fun getStorageSize_throwException() {
- var storageSize = stateOf("Computing")
- whenever(
- storageStatsManager.queryStatsForPackage(
- app.storageUuid,
- app.packageName,
- app.userHandle,
- )
- ).thenThrow(NameNotFoundException())
-
- composeTestRule.setContent {
- CompositionLocalProvider(LocalContext provides context) {
- storageSize = app.getStorageSize()
- }
- }
+ composeTestRule.setContent { storageSize = app.getStorageSize(mockAppStorageRepository) }
- composeTestRule.waitUntil { storageSize.value == "" }
+ composeTestRule.waitUntil { storageSize.value == SIZE }
}
- companion object {
- private val STATS = StorageStats().apply {
- codeBytes = 100
- dataBytes = 20
- cacheBytes = 3
- }
+ private companion object {
+ const val SIZE = "120 kB"
}
}