• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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