From d22619e995ecae79689081d4b60684c05d129ff2 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 8 Sep 2022 16:06:09 +0800 Subject: [PATCH] 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 --- AndroidManifest.xml | 27 +- res/layout/usage_stats.xml | 45 ---- res/layout/usage_stats_item.xml | 50 ---- res/values/arrays.xml | 7 - res/values/strings.xml | 13 +- .../android/settings/UsageStatsActivity.java | 255 ------------------ src/com/android/settings/spa/SpaActivity.kt | 4 +- .../android/settings/spa/SpaBridgeActivity.kt | 49 ++++ .../android/settings/spa/SpaEnvironment.kt | 6 +- .../settings/spa/development/UsageStats.kt | 52 ++++ .../spa/development/UsageStatsListModel.kt | 101 +++++++ 11 files changed, 228 insertions(+), 381 deletions(-) delete mode 100755 res/layout/usage_stats.xml delete mode 100755 res/layout/usage_stats_item.xml delete mode 100755 src/com/android/settings/UsageStatsActivity.java create mode 100755 src/com/android/settings/spa/SpaBridgeActivity.kt create mode 100644 src/com/android/settings/spa/development/UsageStats.kt create mode 100644 src/com/android/settings/spa/development/UsageStatsListModel.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e14cba7f903..468d58702cf 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2911,15 +2911,6 @@ - - - - - - - - - + + + + + + + + + + diff --git a/res/layout/usage_stats.xml b/res/layout/usage_stats.xml deleted file mode 100755 index 013bcdb07a5..00000000000 --- a/res/layout/usage_stats.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/layout/usage_stats_item.xml b/res/layout/usage_stats_item.xml deleted file mode 100755 index 2879df072e5..00000000000 --- a/res/layout/usage_stats_item.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 3a660a19292..771ac1ca9ca 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -343,13 +343,6 @@ Excellent - - - Usage time - Last time used - App name - - MSCHAPV2 diff --git a/res/values/strings.xml b/res/values/strings.xml index c01e9b7bbda..bd3a5fbf86e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4821,15 +4821,14 @@ Always allow %1$s to create widgets and access their data - - Usage statistics - Usage statistics - - Sort by: - - App + + Sort by usage time + + Sort by last time used + + Sort by app name Last time used diff --git a/src/com/android/settings/UsageStatsActivity.java b/src/com/android/settings/UsageStatsActivity.java deleted file mode 100755 index d9803ddfb93..00000000000 --- a/src/com/android/settings/UsageStatsActivity.java +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (C) 2007 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; - -import android.app.Activity; -import android.app.usage.UsageStats; -import android.app.usage.UsageStatsManager; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import android.text.format.DateUtils; -import android.util.ArrayMap; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.BaseAdapter; -import android.widget.ListView; -import android.widget.Spinner; -import android.widget.TextView; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -/** - * Activity to display package usage statistics. - */ -public class UsageStatsActivity extends Activity implements OnItemSelectedListener { - private static final String TAG = "UsageStatsActivity"; - private static final boolean localLOGV = false; - private UsageStatsManager mUsageStatsManager; - private LayoutInflater mInflater; - private UsageStatsAdapter mAdapter; - private PackageManager mPm; - - public static class AppNameComparator implements Comparator { - private Map mAppLabelList; - - AppNameComparator(Map appList) { - mAppLabelList = appList; - } - - @Override - public final int compare(UsageStats a, UsageStats b) { - String alabel = mAppLabelList.get(a.getPackageName()); - String blabel = mAppLabelList.get(b.getPackageName()); - return alabel.compareTo(blabel); - } - } - - public static class LastTimeUsedComparator implements Comparator { - @Override - public final int compare(UsageStats a, UsageStats b) { - // return by descending order - return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); - } - } - - public static class UsageTimeComparator implements Comparator { - @Override - public final int compare(UsageStats a, UsageStats b) { - return Long.compare(b.getTotalTimeInForeground(), a.getTotalTimeInForeground()); - } - } - - // View Holder used when displaying views - static class AppViewHolder { - TextView pkgName; - TextView lastTimeUsed; - TextView usageTime; - } - - class UsageStatsAdapter extends BaseAdapter { - // Constants defining order for display order - private static final int _DISPLAY_ORDER_USAGE_TIME = 0; - private static final int _DISPLAY_ORDER_LAST_TIME_USED = 1; - private static final int _DISPLAY_ORDER_APP_NAME = 2; - - private int mDisplayOrder = _DISPLAY_ORDER_USAGE_TIME; - private LastTimeUsedComparator mLastTimeUsedComparator = new LastTimeUsedComparator(); - private UsageTimeComparator mUsageTimeComparator = new UsageTimeComparator(); - private AppNameComparator mAppLabelComparator; - private final ArrayMap mAppLabelMap = new ArrayMap<>(); - private final ArrayList mPackageStats = new ArrayList<>(); - - UsageStatsAdapter() { - Calendar cal = Calendar.getInstance(); - cal.add(Calendar.DAY_OF_YEAR, -5); - - final List stats = - mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, - cal.getTimeInMillis(), System.currentTimeMillis()); - if (stats == null) { - return; - } - - ArrayMap map = new ArrayMap<>(); - final int statCount = stats.size(); - for (int i = 0; i < statCount; i++) { - final android.app.usage.UsageStats pkgStats = stats.get(i); - - // load application labels for each application - try { - ApplicationInfo appInfo = mPm.getApplicationInfo(pkgStats.getPackageName(), 0); - String label = appInfo.loadLabel(mPm).toString(); - mAppLabelMap.put(pkgStats.getPackageName(), label); - - UsageStats existingStats = - map.get(pkgStats.getPackageName()); - if (existingStats == null) { - map.put(pkgStats.getPackageName(), pkgStats); - } else { - existingStats.add(pkgStats); - } - - } catch (NameNotFoundException e) { - // This package may be gone. - } - } - mPackageStats.addAll(map.values()); - - // Sort list - mAppLabelComparator = new AppNameComparator(mAppLabelMap); - sortList(); - } - - @Override - public int getCount() { - return mPackageStats.size(); - } - - @Override - public Object getItem(int position) { - return mPackageStats.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // A ViewHolder keeps references to children views to avoid unneccessary calls - // to findViewById() on each row. - AppViewHolder holder; - - // When convertView is not null, we can reuse it directly, there is no need - // to reinflate it. We only inflate a new View when the convertView supplied - // by ListView is null. - if (convertView == null) { - convertView = mInflater.inflate(R.layout.usage_stats_item, null); - - // Creates a ViewHolder and store references to the two children views - // we want to bind data to. - holder = new AppViewHolder(); - holder.pkgName = (TextView) convertView.findViewById(R.id.package_name); - holder.lastTimeUsed = (TextView) convertView.findViewById(R.id.last_time_used); - holder.usageTime = (TextView) convertView.findViewById(R.id.usage_time); - convertView.setTag(holder); - } else { - // Get the ViewHolder back to get fast access to the TextView - // and the ImageView. - holder = (AppViewHolder) convertView.getTag(); - } - - // Bind the data efficiently with the holder - UsageStats pkgStats = mPackageStats.get(position); - if (pkgStats != null) { - String label = mAppLabelMap.get(pkgStats.getPackageName()); - holder.pkgName.setText(label); - holder.lastTimeUsed.setText(DateUtils.formatSameDayTime(pkgStats.getLastTimeUsed(), - System.currentTimeMillis(), DateFormat.MEDIUM, DateFormat.MEDIUM)); - holder.usageTime.setText( - DateUtils.formatElapsedTime(pkgStats.getTotalTimeInForeground() / 1000)); - } else { - Log.w(TAG, "No usage stats info for package:" + position); - } - return convertView; - } - - void sortList(int sortOrder) { - if (mDisplayOrder == sortOrder) { - // do nothing - return; - } - mDisplayOrder= sortOrder; - sortList(); - } - private void sortList() { - if (mDisplayOrder == _DISPLAY_ORDER_USAGE_TIME) { - if (localLOGV) Log.i(TAG, "Sorting by usage time"); - Collections.sort(mPackageStats, mUsageTimeComparator); - } else if (mDisplayOrder == _DISPLAY_ORDER_LAST_TIME_USED) { - if (localLOGV) Log.i(TAG, "Sorting by last time used"); - Collections.sort(mPackageStats, mLastTimeUsedComparator); - } else if (mDisplayOrder == _DISPLAY_ORDER_APP_NAME) { - if (localLOGV) Log.i(TAG, "Sorting by application name"); - Collections.sort(mPackageStats, mAppLabelComparator); - } - notifyDataSetChanged(); - } - } - - /** Called when the activity is first created. */ - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setContentView(R.layout.usage_stats); - - mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); - mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mPm = getPackageManager(); - - Spinner typeSpinner = (Spinner) findViewById(R.id.typeSpinner); - typeSpinner.setOnItemSelectedListener(this); - - ListView listView = (ListView) findViewById(R.id.pkg_list); - mAdapter = new UsageStatsAdapter(); - listView.setAdapter(mAdapter); - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - mAdapter.sortList(position); - } - - @Override - public void onNothingSelected(AdapterView parent) { - // do nothing - } -} diff --git a/src/com/android/settings/spa/SpaActivity.kt b/src/com/android/settings/spa/SpaActivity.kt index fab0e081f65..f161c441fd6 100644 --- a/src/com/android/settings/spa/SpaActivity.kt +++ b/src/com/android/settings/spa/SpaActivity.kt @@ -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) } diff --git a/src/com/android/settings/spa/SpaBridgeActivity.kt b/src/com/android/settings/spa/SpaBridgeActivity.kt new file mode 100755 index 00000000000..9e433e449ca --- /dev/null +++ b/src/com/android/settings/spa/SpaBridgeActivity.kt @@ -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 + * , otherwise all its pages will be exported. + * So need this bridge activity to sit in the middle of 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" + } +} diff --git a/src/com/android/settings/spa/SpaEnvironment.kt b/src/com/android/settings/spa/SpaEnvironment.kt index 489c8a8a926..7086795bfb7 100644 --- a/src/com/android/settings/spa/SpaEnvironment.kt +++ b/src/com/android/settings/spa/SpaEnvironment.kt @@ -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), diff --git a/src/com/android/settings/spa/development/UsageStats.kt b/src/com/android/settings/spa/development/UsageStats.kt new file mode 100644 index 00000000000..302f2010b31 --- /dev/null +++ b/src/com/android/settings/spa/development/UsageStats.kt @@ -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) + }) + } +} diff --git a/src/com/android/settings/spa/development/UsageStatsListModel.kt b/src/com/android/settings/spa/development/UsageStatsListModel.kt new file mode 100644 index 00000000000..caa30cc2535 --- /dev/null +++ b/src/com/android/settings/spa/development/UsageStatsListModel.kt @@ -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 { + private val usageStatsManager = + context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + private val now = System.currentTimeMillis() + + override fun transform( + userIdFlow: Flow, + appListFlow: Flow>, + ) = 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, + option: Int, + recordListFlow: Flow>, + ) = 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> { 0 } + }.then(super.getComparator(option)) + + @Composable + override fun getSummary(option: Int, record: UsageStatsAppRecord): State? { + 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 { + 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] + } + } +}