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 17 package com.android.settings.applications; 18 19 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent 20 .SETTINGS_APP_NOTIF_CATEGORY; 21 22 import android.app.Application; 23 import android.app.Fragment; 24 import android.app.usage.UsageStats; 25 import android.app.usage.UsageStatsManager; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.os.UserHandle; 30 import android.support.annotation.VisibleForTesting; 31 import android.support.v7.preference.Preference; 32 import android.support.v7.preference.PreferenceCategory; 33 import android.support.v7.preference.PreferenceScreen; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.ArraySet; 37 import android.util.IconDrawableFactory; 38 import android.util.Log; 39 40 import com.android.settings.R; 41 import com.android.settings.applications.appinfo.AppInfoDashboardFragment; 42 import com.android.settings.core.PreferenceControllerMixin; 43 import com.android.settings.widget.AppPreference; 44 import com.android.settingslib.applications.AppUtils; 45 import com.android.settingslib.applications.ApplicationsState; 46 import com.android.settingslib.core.AbstractPreferenceController; 47 import com.android.settingslib.utils.StringUtil; 48 import com.android.settingslib.wrapper.PackageManagerWrapper; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Calendar; 53 import java.util.Collections; 54 import java.util.Comparator; 55 import java.util.List; 56 import java.util.Map; 57 import 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 */ 63 public 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 RecentAppsPreferenceController(Context context, Application app, Fragment host)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) RecentAppsPreferenceController(Context context, ApplicationsState appState, Fragment host)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 isAvailable()118 public boolean isAvailable() { 119 return true; 120 } 121 122 @Override getPreferenceKey()123 public String getPreferenceKey() { 124 return KEY_PREF_CATEGORY; 125 } 126 127 @Override updateNonIndexableKeys(List<String> keys)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 displayPreference(PreferenceScreen screen)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 updateState(Preference preference)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 compare(UsageStats a, UsageStats b)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 refreshUi(Context prefContext)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 reloadData()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 displayOnlyAppInfo()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 displayRecentApps(Context prefContext, List<UsageStats> recentApps)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(StringUtil.formatRelativeTime(mContext, 243 System.currentTimeMillis() - stat.getLastTimeUsed(), false)); 244 pref.setOrder(i); 245 pref.setOnPreferenceClickListener(preference -> { 246 AppInfoBase.startAppInfoFragment(AppInfoDashboardFragment.class, 247 R.string.application_info_label, pkgName, appEntry.info.uid, mHost, 248 1001 /*RequestCode*/, SETTINGS_APP_NOTIF_CATEGORY); 249 return true; 250 }); 251 if (!rebindPref) { 252 mCategory.addPreference(pref); 253 } 254 } 255 // Remove unused prefs from pref cache pool 256 for (Preference unusedPrefs : appPreferences.values()) { 257 mCategory.removePreference(unusedPrefs); 258 } 259 } 260 getDisplayableRecentAppList()261 private List<UsageStats> getDisplayableRecentAppList() { 262 final List<UsageStats> recentApps = new ArrayList<>(); 263 final Map<String, UsageStats> map = new ArrayMap<>(); 264 final int statCount = mStats.size(); 265 for (int i = 0; i < statCount; i++) { 266 final UsageStats pkgStats = mStats.get(i); 267 if (!shouldIncludePkgInRecents(pkgStats)) { 268 continue; 269 } 270 final String pkgName = pkgStats.getPackageName(); 271 final UsageStats existingStats = map.get(pkgName); 272 if (existingStats == null) { 273 map.put(pkgName, pkgStats); 274 } else { 275 existingStats.add(pkgStats); 276 } 277 } 278 final List<UsageStats> packageStats = new ArrayList<>(); 279 packageStats.addAll(map.values()); 280 Collections.sort(packageStats, this /* comparator */); 281 int count = 0; 282 for (UsageStats stat : packageStats) { 283 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 284 stat.getPackageName(), mUserId); 285 if (appEntry == null) { 286 continue; 287 } 288 recentApps.add(stat); 289 count++; 290 if (count >= SHOW_RECENT_APP_COUNT) { 291 break; 292 } 293 } 294 return recentApps; 295 } 296 297 298 /** 299 * Whether or not the app should be included in recent list. 300 */ shouldIncludePkgInRecents(UsageStats stat)301 private boolean shouldIncludePkgInRecents(UsageStats stat) { 302 final String pkgName = stat.getPackageName(); 303 if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) { 304 Log.d(TAG, "Invalid timestamp, skipping " + pkgName); 305 return false; 306 } 307 308 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 309 Log.d(TAG, "System package, skipping " + pkgName); 310 return false; 311 } 312 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 313 .setPackage(pkgName); 314 315 if (mPm.resolveActivity(launchIntent, 0) == null) { 316 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 317 final ApplicationsState.AppEntry appEntry = 318 mApplicationsState.getEntry(pkgName, mUserId); 319 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 320 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 321 return false; 322 } 323 } 324 return true; 325 } 326 } 327