1 /* 2 * Copyright (C) 2018 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.tv.settings.device.apps; 18 19 import android.app.Application; 20 import android.app.usage.UsageStats; 21 import android.app.usage.UsageStatsManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.os.UserHandle; 26 import android.support.annotation.VisibleForTesting; 27 import android.support.v7.preference.Preference; 28 import android.support.v7.preference.PreferenceCategory; 29 import android.support.v7.preference.PreferenceScreen; 30 import android.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.IconDrawableFactory; 34 import android.util.Log; 35 36 import com.android.settingslib.applications.AppUtils; 37 import com.android.settingslib.applications.ApplicationsState; 38 import com.android.settingslib.core.AbstractPreferenceController; 39 import com.android.settingslib.utils.StringUtil; 40 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Calendar; 44 import java.util.Collections; 45 import java.util.Comparator; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 50 /** 51 * This controller displays a list of recently used apps and a "See all" button. 52 */ 53 public class RecentAppsPreferenceController extends AbstractPreferenceController 54 implements Comparator<UsageStats> { 55 56 private static final String TAG = "RecentAppsPreferenceController"; 57 private static final String KEY_PREF_CATEGORY = "recently_used_apps_category"; 58 @VisibleForTesting 59 static final String KEY_SEE_ALL = "see_all_apps"; 60 private static final int SHOW_RECENT_APP_COUNT = 5; 61 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 62 63 private final PackageManager mPm; 64 private final UsageStatsManager mUsageStatsManager; 65 private final ApplicationsState mApplicationsState; 66 private final int mUserId; 67 private final IconDrawableFactory mIconDrawableFactory; 68 69 private Calendar mCal; 70 private List<UsageStats> mStats; 71 72 private PreferenceCategory mCategory; 73 74 static { 75 SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( 76 "android", 77 "com.android.tv.settings", 78 "com.android.systemui", 79 "com.android.providers.calendar", 80 "com.android.providers.media" 81 )); 82 } 83 84 public RecentAppsPreferenceController(Context context, Application app) { 85 this(context, app == null ? null : ApplicationsState.getInstance(app)); 86 } 87 88 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 89 RecentAppsPreferenceController(Context context, ApplicationsState appState) { 90 super(context); 91 mIconDrawableFactory = IconDrawableFactory.newInstance(context); 92 mUserId = UserHandle.myUserId(); 93 mPm = context.getPackageManager(); 94 mUsageStatsManager = 95 (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); 96 mApplicationsState = appState; 97 } 98 99 @Override 100 public boolean isAvailable() { 101 return true; 102 } 103 104 @Override 105 public String getPreferenceKey() { 106 return KEY_PREF_CATEGORY; 107 } 108 109 @Override 110 public void displayPreference(PreferenceScreen screen) { 111 super.displayPreference(screen); 112 mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); 113 refreshUi(mCategory.getContext()); 114 } 115 116 @VisibleForTesting 117 void refreshUi(Context prefContext) { 118 reloadData(); 119 final List<UsageStats> recentApps = getDisplayableRecentAppList(); 120 if (recentApps != null && !recentApps.isEmpty()) { 121 displayRecentApps(prefContext, recentApps); 122 } else { 123 displayOnlyAllApps(); 124 } 125 } 126 127 private void displayOnlyAllApps() { 128 mCategory.setVisible(false); 129 int prefCount = mCategory.getPreferenceCount(); 130 for (int i = prefCount - 1; i >= 0; i--) { 131 final Preference pref = mCategory.getPreference(i); 132 if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { 133 mCategory.removePreference(pref); 134 } 135 } 136 } 137 138 private void displayRecentApps(Context prefContext, List<UsageStats> recentApps) { 139 mCategory.setVisible(true); 140 141 // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. 142 // Build a cached preference pool 143 final Map<String, Preference> appPreferences = new ArrayMap<>(); 144 int prefCount = mCategory.getPreferenceCount(); 145 for (int i = 0; i < prefCount; i++) { 146 final Preference pref = mCategory.getPreference(i); 147 final String key = pref.getKey(); 148 if (!TextUtils.equals(key, KEY_SEE_ALL)) { 149 appPreferences.put(key, pref); 150 } 151 } 152 final int recentAppsCount = recentApps.size(); 153 for (int i = 0; i < recentAppsCount; i++) { 154 final UsageStats stat = recentApps.get(i); 155 // Bind recent apps to existing prefs if possible, or create a new pref. 156 final String pkgName = stat.getPackageName(); 157 final ApplicationsState.AppEntry appEntry = 158 mApplicationsState.getEntry(pkgName, mUserId); 159 if (appEntry == null) { 160 continue; 161 } 162 163 boolean rebindPref = true; 164 Preference pref = appPreferences.remove(pkgName); 165 if (pref == null) { 166 pref = new Preference(prefContext); 167 rebindPref = false; 168 } 169 pref.setKey(pkgName); 170 pref.setTitle(appEntry.label); 171 pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); 172 pref.setSummary(StringUtil.formatRelativeTime(mContext, 173 System.currentTimeMillis() - stat.getLastTimeUsed(), false)); 174 pref.setOrder(i); 175 AppManagementFragment.prepareArgs(pref.getExtras(), pkgName); 176 pref.setFragment(AppManagementFragment.class.getName()); 177 if (!rebindPref) { 178 mCategory.addPreference(pref); 179 } 180 } 181 // Remove unused prefs from pref cache pool 182 for (Preference unusedPrefs : appPreferences.values()) { 183 mCategory.removePreference(unusedPrefs); 184 } 185 } 186 187 @Override 188 public void updateState(Preference preference) { 189 super.updateState(preference); 190 refreshUi(mCategory.getContext()); 191 } 192 193 @Override 194 public final int compare(UsageStats a, UsageStats b) { 195 // return by descending order 196 return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); 197 } 198 199 @VisibleForTesting 200 void reloadData() { 201 mCal = Calendar.getInstance(); 202 mCal.add(Calendar.DAY_OF_YEAR, -1); 203 mStats = mUsageStatsManager.queryUsageStats( 204 UsageStatsManager.INTERVAL_BEST, mCal.getTimeInMillis(), 205 System.currentTimeMillis()); 206 } 207 208 private List<UsageStats> getDisplayableRecentAppList() { 209 final List<UsageStats> recentApps = new ArrayList<>(); 210 final Map<String, UsageStats> map = new ArrayMap<>(); 211 final int statCount = mStats.size(); 212 for (int i = 0; i < statCount; i++) { 213 final UsageStats pkgStats = mStats.get(i); 214 if (!shouldIncludePkgInRecents(pkgStats)) { 215 continue; 216 } 217 final String pkgName = pkgStats.getPackageName(); 218 final UsageStats existingStats = map.get(pkgName); 219 if (existingStats == null) { 220 map.put(pkgName, pkgStats); 221 } else { 222 existingStats.add(pkgStats); 223 } 224 } 225 final List<UsageStats> packageStats = new ArrayList<>(); 226 packageStats.addAll(map.values()); 227 Collections.sort(packageStats, this /* comparator */); 228 int count = 0; 229 for (UsageStats stat : packageStats) { 230 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 231 stat.getPackageName(), mUserId); 232 if (appEntry == null) { 233 continue; 234 } 235 recentApps.add(stat); 236 count++; 237 if (count >= SHOW_RECENT_APP_COUNT) { 238 break; 239 } 240 } 241 return recentApps; 242 } 243 244 /** 245 * Whether or not the app should be included in recent list. 246 */ 247 private boolean shouldIncludePkgInRecents(UsageStats stat) { 248 final String pkgName = stat.getPackageName(); 249 if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) { 250 Log.d(TAG, "Invalid timestamp, skipping " + pkgName); 251 return false; 252 } 253 254 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 255 Log.d(TAG, "System package, skipping " + pkgName); 256 return false; 257 } 258 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER) 259 .setPackage(pkgName); 260 261 if (mPm.resolveActivity(launchIntent, 0) == null) { 262 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 263 final ApplicationsState.AppEntry appEntry = 264 mApplicationsState.getEntry(pkgName, mUserId); 265 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 266 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 267 return false; 268 } 269 } 270 return true; 271 } 272 } 273