RecentAppsPreferenceController.java revision ec4064cdbd8df16b5c7d49a71cb62b53e9cbb6b1
1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.settings.applications; 18 19import android.app.Application; 20import android.app.Fragment; 21import android.app.usage.UsageStats; 22import android.app.usage.UsageStatsManager; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.PackageManager; 26import android.os.UserHandle; 27import android.support.annotation.VisibleForTesting; 28import android.support.v7.preference.Preference; 29import android.support.v7.preference.PreferenceCategory; 30import android.support.v7.preference.PreferenceScreen; 31import android.text.TextUtils; 32import android.text.format.DateUtils; 33import android.util.ArrayMap; 34import android.util.ArraySet; 35import android.util.IconDrawableFactory; 36import android.util.Log; 37 38import com.android.settings.R; 39import com.android.settings.Utils; 40import com.android.settings.core.PreferenceController; 41import com.android.settingslib.applications.ApplicationsState; 42 43import java.util.ArrayList; 44import java.util.Arrays; 45import java.util.Calendar; 46import java.util.Collections; 47import java.util.Comparator; 48import java.util.List; 49import java.util.Map; 50import java.util.Set; 51 52import static com.android.internal.logging.nano.MetricsProto.MetricsEvent 53 .SETTINGS_APP_NOTIF_CATEGORY; 54 55/** 56 * This controller displays a list of recently used apps and a "See all" button. If there is 57 * no recently used app, "See all" will be displayed as "App info". 58 */ 59public class RecentAppsPreferenceController extends PreferenceController 60 implements Comparator<UsageStats> { 61 62 private static final String TAG = "RecentAppsCtrl"; 63 private static final String KEY_PREF_CATEGORY = "recent_apps_category"; 64 @VisibleForTesting 65 static final String KEY_SEE_ALL = "all_app_info"; 66 private static final int SHOW_RECENT_APP_COUNT = 5; 67 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 68 69 private final Fragment mHost; 70 private final PackageManager mPm; 71 private final UsageStatsManager mUsageStatsManager; 72 private final ApplicationsState mApplicationsState; 73 private final int mUserId; 74 private final IconDrawableFactory mIconDrawableFactory; 75 76 private Calendar mCal; 77 private List<UsageStats> mStats; 78 79 private PreferenceCategory mCategory; 80 private Preference mSeeAllPref; 81 private boolean mHasRecentApps; 82 83 static { 84 SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( 85 "android", 86 "com.android.phone", 87 "com.android.settings", 88 "com.android.systemui", 89 "com.android.providers.calendar", 90 "com.android.providers.media" 91 )); 92 } 93 94 public RecentAppsPreferenceController(Context context, Application app, Fragment host) { 95 this(context, app == null ? null : ApplicationsState.getInstance(app), host); 96 } 97 98 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 99 RecentAppsPreferenceController(Context context, ApplicationsState appState, Fragment host) { 100 super(context); 101 mIconDrawableFactory = IconDrawableFactory.newInstance(context); 102 mUserId = UserHandle.myUserId(); 103 mPm = context.getPackageManager(); 104 mHost = host; 105 mUsageStatsManager = 106 (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); 107 mApplicationsState = appState; 108 } 109 110 @Override 111 public boolean isAvailable() { 112 return true; 113 } 114 115 @Override 116 public String getPreferenceKey() { 117 return KEY_PREF_CATEGORY; 118 } 119 120 @Override 121 public void updateNonIndexableKeys(List<String> keys) { 122 super.updateNonIndexableKeys(keys); 123 // Don't index category name into search. It's not actionable. 124 keys.add(KEY_PREF_CATEGORY); 125 } 126 127 @Override 128 public void displayPreference(PreferenceScreen screen) { 129 mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); 130 mSeeAllPref = screen.findPreference(KEY_SEE_ALL); 131 super.displayPreference(screen); 132 refreshUi(mCategory.getContext()); 133 } 134 135 @Override 136 public void updateState(Preference preference) { 137 super.updateState(preference); 138 refreshUi(mCategory.getContext()); 139 // Show total number of installed apps as See all's summary. 140 new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON, 141 new PackageManagerWrapperImpl(mContext.getPackageManager())) { 142 @Override 143 protected void onCountComplete(int num) { 144 if (mHasRecentApps) { 145 mSeeAllPref.setTitle(mContext.getString(R.string.see_all_apps_title, num)); 146 } else { 147 mSeeAllPref.setSummary(mContext.getString(R.string.apps_summary, num)); 148 } 149 } 150 }.execute(); 151 152 } 153 154 @Override 155 public final int compare(UsageStats a, UsageStats b) { 156 // return by descending order 157 return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); 158 } 159 160 @VisibleForTesting 161 void refreshUi(Context prefContext) { 162 reloadData(); 163 final List<UsageStats> recentApps = getDisplayableRecentAppList(); 164 if (recentApps != null && !recentApps.isEmpty()) { 165 mHasRecentApps = true; 166 displayRecentApps(prefContext, recentApps); 167 } else { 168 mHasRecentApps = false; 169 displayOnlyAppInfo(); 170 } 171 } 172 173 @VisibleForTesting 174 void reloadData() { 175 mCal = Calendar.getInstance(); 176 mCal.add(Calendar.DAY_OF_YEAR, -1); 177 mStats = mUsageStatsManager.queryUsageStats( 178 UsageStatsManager.INTERVAL_BEST, mCal.getTimeInMillis(), 179 System.currentTimeMillis()); 180 } 181 182 private void displayOnlyAppInfo() { 183 mCategory.setTitle(null); 184 mSeeAllPref.setTitle(R.string.applications_settings); 185 mSeeAllPref.setIcon(null); 186 int prefCount = mCategory.getPreferenceCount(); 187 for (int i = prefCount - 1; i >= 0; i--) { 188 final Preference pref = mCategory.getPreference(i); 189 if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { 190 mCategory.removePreference(pref); 191 } 192 } 193 } 194 195 private void displayRecentApps(Context prefContext, List<UsageStats> recentApps) { 196 mCategory.setTitle(R.string.recent_app_category_title); 197 mSeeAllPref.setSummary(null); 198 mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); 199 200 // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. 201 // Build a cached preference pool 202 final Map<String, Preference> appPreferences = new ArrayMap<>(); 203 int prefCount = mCategory.getPreferenceCount(); 204 for (int i = 0; i < prefCount; i++) { 205 final Preference pref = mCategory.getPreference(i); 206 final String key = pref.getKey(); 207 if (!TextUtils.equals(key, KEY_SEE_ALL)) { 208 appPreferences.put(key, pref); 209 } 210 } 211 final int recentAppsCount = recentApps.size(); 212 for (int i = 0; i < recentAppsCount; i++) { 213 final UsageStats stat = recentApps.get(i); 214 // Bind recent apps to existing prefs if possible, or create a new pref. 215 final String pkgName = stat.getPackageName(); 216 final ApplicationsState.AppEntry appEntry = 217 mApplicationsState.getEntry(pkgName, mUserId); 218 if (appEntry == null) { 219 continue; 220 } 221 222 boolean rebindPref = true; 223 Preference pref = appPreferences.remove(pkgName); 224 if (pref == null) { 225 pref = new Preference(prefContext); 226 rebindPref = false; 227 } 228 pref.setKey(pkgName); 229 pref.setTitle(appEntry.label); 230 pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); 231 pref.setSummary(TextUtils.expandTemplate( 232 mContext.getResources().getText(R.string.recent_app_summary), 233 Utils.formatElapsedTime(mContext, 234 System.currentTimeMillis() - stat.getLastTimeUsed(), false))); 235 pref.setOrder(i); 236 pref.setOnPreferenceClickListener(preference -> { 237 AppInfoBase.startAppInfoFragment(InstalledAppDetails.class, 238 R.string.application_info_label, pkgName, appEntry.info.uid, mHost, 239 1001 /*RequestCode*/, SETTINGS_APP_NOTIF_CATEGORY); 240 return true; 241 }); 242 if (!rebindPref) { 243 mCategory.addPreference(pref); 244 } 245 } 246 // Remove unused prefs from pref cache pool 247 for (Preference unusedPrefs : appPreferences.values()) { 248 mCategory.removePreference(unusedPrefs); 249 } 250 } 251 252 private List<UsageStats> getDisplayableRecentAppList() { 253 final List<UsageStats> recentApps = new ArrayList<>(); 254 final Map<String, UsageStats> map = new ArrayMap<>(); 255 final int statCount = mStats.size(); 256 for (int i = 0; i < statCount; i++) { 257 final UsageStats pkgStats = mStats.get(i); 258 if (!shouldIncludePkgInRecents(pkgStats)) { 259 continue; 260 } 261 final String pkgName = pkgStats.getPackageName(); 262 final UsageStats existingStats = map.get(pkgName); 263 if (existingStats == null) { 264 map.put(pkgName, pkgStats); 265 } else { 266 existingStats.add(pkgStats); 267 } 268 } 269 final List<UsageStats> packageStats = new ArrayList<>(); 270 packageStats.addAll(map.values()); 271 Collections.sort(packageStats, this /* comparator */); 272 int count = 0; 273 for (UsageStats stat : packageStats) { 274 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 275 stat.getPackageName(), mUserId); 276 if (appEntry == null) { 277 continue; 278 } 279 recentApps.add(stat); 280 count++; 281 if (count >= SHOW_RECENT_APP_COUNT) { 282 break; 283 } 284 } 285 return recentApps; 286 } 287 288 289 /** 290 * Whether or not the app should be included in recent list. 291 */ 292 private boolean shouldIncludePkgInRecents(UsageStats stat) { 293 final String pkgName = stat.getPackageName(); 294 if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) { 295 Log.d(TAG, "Invalid timestamp, skipping " + pkgName); 296 return false; 297 } 298 299 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 300 Log.d(TAG, "System package, skipping " + pkgName); 301 return false; 302 } 303 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 304 .setPackage(pkgName); 305 306 if (mPm.resolveActivity(launchIntent, 0) == null) { 307 // Not visible on launcher -> likely not a user visible app, skip 308 Log.d(TAG, "Not a user visible app, skipping " + pkgName); 309 return false; 310 } 311 return true; 312 } 313} 314