Merge "Storage Apps Lists SPA migration" into main
This commit is contained in:
@@ -68,6 +68,7 @@ import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProv
|
|||||||
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
|
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
|
||||||
import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
|
import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
|
||||||
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
|
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
|
||||||
|
import com.android.settings.spa.app.storage.StorageAppListPageProvider
|
||||||
import com.android.settings.spa.notification.AppListNotificationsPageProvider
|
import com.android.settings.spa.notification.AppListNotificationsPageProvider
|
||||||
import com.android.settings.spa.system.AppLanguagesPageProvider
|
import com.android.settings.spa.system.AppLanguagesPageProvider
|
||||||
|
|
||||||
@@ -119,6 +120,9 @@ object ManageApplicationsUtil {
|
|||||||
LIST_TYPE_MAIN -> AllAppListPageProvider.name
|
LIST_TYPE_MAIN -> AllAppListPageProvider.name
|
||||||
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
|
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
|
||||||
LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
|
LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
|
||||||
|
// TODO(b/292165031) enable once sorting is supported
|
||||||
|
//LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name
|
||||||
|
//LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,7 @@ import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
|
|||||||
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider
|
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider
|
||||||
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
|
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
|
||||||
import com.android.settings.spa.app.specialaccess.UseFullScreenIntentAppListProvider
|
import com.android.settings.spa.app.specialaccess.UseFullScreenIntentAppListProvider
|
||||||
|
import com.android.settings.spa.app.storage.StorageAppListPageProvider
|
||||||
import com.android.settings.spa.core.instrumentation.SpaLogProvider
|
import com.android.settings.spa.core.instrumentation.SpaLogProvider
|
||||||
import com.android.settings.spa.development.UsageStatsPageProvider
|
import com.android.settings.spa.development.UsageStatsPageProvider
|
||||||
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
|
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
|
||||||
@@ -92,6 +93,8 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
|
|||||||
CloneAppInfoSettingsProvider,
|
CloneAppInfoSettingsProvider,
|
||||||
NetworkAndInternetPageProvider,
|
NetworkAndInternetPageProvider,
|
||||||
AboutPhonePageProvider,
|
AboutPhonePageProvider,
|
||||||
|
StorageAppListPageProvider.Apps,
|
||||||
|
StorageAppListPageProvider.Games,
|
||||||
) + togglePermissionAppListTemplate.createPageProviders(),
|
) + togglePermissionAppListTemplate.createPageProviders(),
|
||||||
rootPages = listOf(
|
rootPages = listOf(
|
||||||
HomePageProvider.createSettingsPage()
|
HomePageProvider.createSettingsPage()
|
||||||
|
141
src/com/android/settings/spa/app/storage/StorageAppList.kt
Normal file
141
src/com/android/settings/spa/app/storage/StorageAppList.kt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.settings.spa.app.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
|
||||||
|
import com.android.settingslib.spa.framework.common.SettingsPageProvider
|
||||||
|
import com.android.settingslib.spa.framework.util.filterItem
|
||||||
|
import com.android.settingslib.spa.framework.util.mapItem
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.AppEntry
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.AppList
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.AppListInput
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.AppListItem
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.AppListPage
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.calculateSizeBytes
|
||||||
|
import com.android.settingslib.spaprivileged.template.app.getStorageSize
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
sealed class StorageAppListPageProvider(private val type: StorageType) : SettingsPageProvider {
|
||||||
|
@Composable
|
||||||
|
override fun Page(arguments: Bundle?) {
|
||||||
|
StorageAppListPage(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Apps : StorageAppListPageProvider(StorageType.Apps) {
|
||||||
|
override val name = "StorageAppList"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Games : StorageAppListPageProvider(StorageType.Games) {
|
||||||
|
override val name = "GameStorageAppList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class StorageType(
|
||||||
|
@StringRes val titleResource: Int,
|
||||||
|
val filter: (AppRecordWithSize) -> Boolean
|
||||||
|
) {
|
||||||
|
object Apps : StorageType(
|
||||||
|
titleResource = R.string.apps_storage,
|
||||||
|
filter = {
|
||||||
|
(it.app.flags and ApplicationInfo.FLAG_IS_GAME) == 0 &&
|
||||||
|
it.app.category != ApplicationInfo.CATEGORY_GAME
|
||||||
|
}
|
||||||
|
)
|
||||||
|
object Games : StorageType(
|
||||||
|
titleResource = R.string.game_storage_settings,
|
||||||
|
filter = {
|
||||||
|
(it.app.flags and ApplicationInfo.FLAG_IS_GAME) != 0 ||
|
||||||
|
it.app.category == ApplicationInfo.CATEGORY_GAME
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StorageAppListPage(
|
||||||
|
type: StorageType,
|
||||||
|
appList: @Composable AppListInput<AppRecordWithSize>.() -> Unit = { AppList() }
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
AppListPage(
|
||||||
|
title = stringResource(type.titleResource),
|
||||||
|
listModel = when (type) {
|
||||||
|
StorageType.Apps -> remember(context) { StorageAppListModel(context, type) }
|
||||||
|
StorageType.Games -> remember(context) { StorageAppListModel(context, type) }
|
||||||
|
},
|
||||||
|
showInstantApps = true,
|
||||||
|
matchAnyUserForAdmin = true,
|
||||||
|
appList = appList,
|
||||||
|
moreOptions = { }, // TODO(b/292165031) Sorting in Options not yet supported
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AppRecordWithSize(
|
||||||
|
override val app: ApplicationInfo,
|
||||||
|
val size: Long
|
||||||
|
) : AppRecord
|
||||||
|
|
||||||
|
class StorageAppListModel(
|
||||||
|
private val context: Context,
|
||||||
|
private val type: StorageType,
|
||||||
|
private val getStorageSummary: @Composable ApplicationInfo.() -> State<String> = {
|
||||||
|
getStorageSize()
|
||||||
|
}
|
||||||
|
) : AppListModel<AppRecordWithSize> {
|
||||||
|
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
|
||||||
|
appListFlow.mapItem {
|
||||||
|
AppRecordWithSize(it, it.calculateSizeBytes(context) ?: 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filter(
|
||||||
|
userIdFlow: Flow<Int>,
|
||||||
|
option: Int,
|
||||||
|
recordListFlow: Flow<List<AppRecordWithSize>>
|
||||||
|
): Flow<List<AppRecordWithSize>> = recordListFlow.filterItem { type.filter(it) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun getSummary(option: Int, record: AppRecordWithSize): State<String> {
|
||||||
|
val storageSummary = record.app.getStorageSummary()
|
||||||
|
return remember {
|
||||||
|
derivedStateOf {
|
||||||
|
storageSummary.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun AppListItemModel<AppRecordWithSize>.AppItem() {
|
||||||
|
AppListItem(onClick = AppInfoSettingsProvider.navigator(app = record.app))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComparator(option: Int) = compareByDescending<AppEntry<AppRecordWithSize>> {
|
||||||
|
it.record.size
|
||||||
|
}.then(super.getComparator(option))
|
||||||
|
}
|
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.settings.spa.app.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.icu.text.CollationKey
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.AppEntry
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class StorageAppListTest {
|
||||||
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun storageAppListPageProvider_apps_name() {
|
||||||
|
assertThat(StorageAppListPageProvider.Apps.name).isEqualTo("StorageAppList")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun storageAppListPageProvider_games_name() {
|
||||||
|
assertThat(StorageAppListPageProvider.Games.name).isEqualTo("GameStorageAppList")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun transform_containsSize() = runTest {
|
||||||
|
val listModel = StorageAppListModel(context, StorageType.Apps)
|
||||||
|
val recordListFlow = listModel.transform(flowOf(0), flowOf(listOf(APP)))
|
||||||
|
val recordList = recordListFlow.firstWithTimeoutOrNull()!!
|
||||||
|
assertThat(recordList).hasSize(1)
|
||||||
|
assertThat(recordList.first().app).isSameInstanceAs(APP)
|
||||||
|
assertThat(recordList.first().size).isEqualTo(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filter_apps_appWithoutGame() = runTest {
|
||||||
|
val listModel = StorageAppListModel(context, StorageType.Apps)
|
||||||
|
val recordListFlow = listModel.filter(
|
||||||
|
flowOf(0),
|
||||||
|
0,
|
||||||
|
flowOf(
|
||||||
|
listOf(
|
||||||
|
AppRecordWithSize(APP, 1L),
|
||||||
|
AppRecordWithSize(APP2, 1L),
|
||||||
|
AppRecordWithSize(GAME, 1L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val recordList = recordListFlow.firstWithTimeoutOrNull()!!
|
||||||
|
assertThat(recordList).hasSize(2)
|
||||||
|
assertThat(recordList.none { it.app === GAME }).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filter_games_gameWithoutApp() = runTest {
|
||||||
|
val listModel = StorageAppListModel(context, StorageType.Games)
|
||||||
|
val recordListFlow = listModel.filter(
|
||||||
|
flowOf(0),
|
||||||
|
0,
|
||||||
|
flowOf(
|
||||||
|
listOf(
|
||||||
|
AppRecordWithSize(APP, 1L),
|
||||||
|
AppRecordWithSize(APP2, 1L),
|
||||||
|
AppRecordWithSize(GAME, 1L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val recordList = recordListFlow.firstWithTimeoutOrNull()!!
|
||||||
|
assertThat(recordList).hasSize(1)
|
||||||
|
assertThat(recordList.all { it.app === GAME }).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getComparator_sortsByDescendingSize() {
|
||||||
|
val listModel = StorageAppListModel(context, StorageType.Apps)
|
||||||
|
val source = listOf(
|
||||||
|
AppEntry(
|
||||||
|
AppRecordWithSize(app = APP, size = 1L),
|
||||||
|
"app1",
|
||||||
|
CollationKey("first", byteArrayOf(0))
|
||||||
|
),
|
||||||
|
AppEntry(
|
||||||
|
AppRecordWithSize(app = APP2, size = 3L),
|
||||||
|
"app2",
|
||||||
|
CollationKey("second", byteArrayOf(0))
|
||||||
|
),
|
||||||
|
AppEntry(
|
||||||
|
AppRecordWithSize(app = APP3, size = 3L),
|
||||||
|
"app3",
|
||||||
|
CollationKey("third", byteArrayOf(0))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = source.sortedWith(listModel.getComparator(0))
|
||||||
|
|
||||||
|
assertThat(result[0].record.app).isSameInstanceAs(APP2)
|
||||||
|
assertThat(result[1].record.app).isSameInstanceAs(APP3)
|
||||||
|
assertThat(result[2].record.app).isSameInstanceAs(APP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val APP_PACKAGE_NAME = "app.package.name"
|
||||||
|
const val APP_PACKAGE_NAME2 = "app.package.name2"
|
||||||
|
const val APP_PACKAGE_NAME3 = "app.package.name3"
|
||||||
|
const val GAME_PACKAGE_NAME = "game.package.name"
|
||||||
|
val APP = ApplicationInfo().apply {
|
||||||
|
packageName = APP_PACKAGE_NAME
|
||||||
|
flags = ApplicationInfo.FLAG_INSTALLED
|
||||||
|
}
|
||||||
|
val APP2 = ApplicationInfo().apply {
|
||||||
|
packageName = APP_PACKAGE_NAME2
|
||||||
|
flags = ApplicationInfo.FLAG_INSTALLED
|
||||||
|
}
|
||||||
|
val APP3 = ApplicationInfo().apply {
|
||||||
|
packageName = APP_PACKAGE_NAME3
|
||||||
|
flags = ApplicationInfo.FLAG_INSTALLED
|
||||||
|
}
|
||||||
|
val GAME = ApplicationInfo().apply {
|
||||||
|
packageName = GAME_PACKAGE_NAME
|
||||||
|
flags = ApplicationInfo.FLAG_INSTALLED or ApplicationInfo.FLAG_IS_GAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user