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