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