Home | History | Annotate | Download | only in apps
      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