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