Migrate UsageStats to Spa

The UsageStats page is for testing only, cannot entry through Settings
Home, but is accessible by enter *#*#4636#*#* in dialer.

Migrate UsageStats to Spa for both improving the UI & performance.

Fix: 244675756
Bug: 235727273
Test: Manual with Settings App
Change-Id: I6ec6e233075a3f79ac1231aecafabf2a71dac716
This commit is contained in:
Chaohui Wang
2022-09-08 16:06:09 +08:00
parent e68cf6df7d
commit d22619e995
11 changed files with 228 additions and 381 deletions

View File

@@ -23,9 +23,9 @@ import com.android.settingslib.spa.framework.BrowseActivity
class SpaActivity : BrowseActivity(SpaEnvironment.settingsPageProviders) {
companion object {
@JvmStatic
fun startSpaActivity(context: Context, startDestination: String) {
fun startSpaActivity(context: Context, destination: String) {
val intent = Intent(context, SpaActivity::class.java).apply {
putExtra(KEY_DESTINATION, startDestination)
putExtra(KEY_DESTINATION, destination)
}
context.startActivity(intent)
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2022 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
import android.app.Activity
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ComponentInfoFlags
import android.os.Bundle
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
/**
* Activity used as a bridge to [SpaActivity].
*
* Since [SpaActivity] is not exported, [SpaActivity] could not be the target activity of
* <activity-alias>, otherwise all its pages will be exported.
* So need this bridge activity to sit in the middle of <activity-alias> and [SpaActivity].
*/
class SpaBridgeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getDestination()?.let { destination ->
startSpaActivity(this, destination)
}
finish()
}
private fun getDestination(): String? =
packageManager.getActivityInfo(
componentName, ComponentInfoFlags.of(PackageManager.GET_META_DATA.toLong())
).metaData.getString(META_DATA_KEY_DESTINATION)
companion object {
private const val META_DATA_KEY_DESTINATION = "com.android.settings.spa.DESTINATION"
}
}

View File

@@ -20,11 +20,12 @@ import com.android.settings.spa.app.AppsMainPageProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider
import com.android.settings.spa.development.UsageStatsPageProvider
import com.android.settings.spa.home.HomePageProvider
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.notification.NotificationMainPageProvider
import com.android.settingslib.spa.framework.common.SettingsEntryRepository
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListTemplate
@@ -44,6 +45,7 @@ object SpaEnvironment {
SpecialAppAccessPageProvider,
NotificationMainPageProvider,
AppListNotificationsPageProvider,
UsageStatsPageProvider,
) + togglePermissionAppListTemplate.createPageProviders(),
rootPages = listOf(
SettingsPage(HomePageProvider.name),

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2022 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.development
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.template.app.AppListItem
import com.android.settingslib.spaprivileged.template.app.AppListPage
object UsageStatsPageProvider : SettingsPageProvider {
override val name = "UsageStats"
@Composable
override fun Page(arguments: Bundle?) {
AppListPage(
title = stringResource(R.string.testing_usage_stats),
listModel = rememberContext(::UsageStatsListModel),
primaryUserOnly = true,
) { itemModel ->
AppListItem(itemModel) {}
}
}
@Composable
fun EntryItem() {
Preference(object : PreferenceModel {
override val title = stringResource(R.string.testing_usage_stats)
override val onClick = navigator(name)
})
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2022 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.development
import android.app.usage.UsageStats
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.text.format.DateUtils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import com.android.settings.R
import com.android.settings.spa.development.UsageStatsListModel.SpinnerItem.Companion.toSpinnerItem
import com.android.settingslib.spa.framework.compose.stateOf
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 java.text.DateFormat
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
data class UsageStatsAppRecord(
override val app: ApplicationInfo,
val usageStats: UsageStats?,
) : AppRecord
class UsageStatsListModel(private val context: Context) : AppListModel<UsageStatsAppRecord> {
private val usageStatsManager =
context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
private val now = System.currentTimeMillis()
override fun transform(
userIdFlow: Flow<Int>,
appListFlow: Flow<List<ApplicationInfo>>,
) = userIdFlow.map { getUsageStats() }
.combine(appListFlow) { usageStatsMap, appList ->
appList.map { app -> UsageStatsAppRecord(app, usageStatsMap[app.packageName]) }
}
override fun getSpinnerOptions() = SpinnerItem.values().map {
context.getString(it.stringResId)
}
override fun filter(
userIdFlow: Flow<Int>,
option: Int,
recordListFlow: Flow<List<UsageStatsAppRecord>>,
) = recordListFlow.map { recordList ->
recordList.filter { it.usageStats != null }
}
override fun getComparator(option: Int) = when (option.toSpinnerItem()) {
SpinnerItem.UsageTime -> compareByDescending { it.record.usageStats?.totalTimeInForeground }
SpinnerItem.LastTimeUsed -> compareByDescending { it.record.usageStats?.lastTimeUsed }
else -> compareBy<AppEntry<UsageStatsAppRecord>> { 0 }
}.then(super.getComparator(option))
@Composable
override fun getSummary(option: Int, record: UsageStatsAppRecord): State<String>? {
val usageStats = record.usageStats ?: return null
val lastTimeUsed = DateUtils.formatSameDayTime(
usageStats.lastTimeUsed, now, DateFormat.MEDIUM, DateFormat.MEDIUM)
val lastTimeUsedLine = "${context.getString(R.string.last_time_used_label)}: $lastTimeUsed"
val usageTime = DateUtils.formatElapsedTime(usageStats.totalTimeInForeground / 1000)
val usageTimeLine = "${context.getString(R.string.usage_time_label)}: $usageTime"
return stateOf("$lastTimeUsedLine\n$usageTimeLine")
}
private fun getUsageStats(): Map<String, UsageStats> {
val startTime = now - TimeUnit.DAYS.toMillis(5)
return usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, startTime, now)
.groupingBy { it.packageName }.reduce { _, a, b -> a.add(b); a }
}
private enum class SpinnerItem(val stringResId: Int) {
UsageTime(R.string.usage_stats_sort_by_usage_time),
LastTimeUsed(R.string.usage_stats_sort_by_last_time_used),
AppName(R.string.usage_stats_sort_by_app_name);
companion object {
fun Int.toSpinnerItem(): SpinnerItem = values()[this]
}
}
}