1 /* 2 * Copyright (C) 2019 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.car.settings.applications; 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.util.ArrayMap; 27 import android.util.SparseArray; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.VisibleForTesting; 31 32 import com.android.car.settings.R; 33 import com.android.car.settings.common.Logger; 34 import com.android.settingslib.applications.AppUtils; 35 import com.android.settingslib.applications.ApplicationsState; 36 import com.android.settingslib.utils.ThreadUtils; 37 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Calendar; 41 import java.util.Collections; 42 import java.util.Comparator; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Class for fetching and returning recently used apps. Largely derived from 48 * {@link com.android.settings.applications.RecentAppStatsMixin}. 49 */ 50 public class RecentAppsItemManager implements Comparator<UsageStats> { 51 52 private static final Logger LOG = new Logger(RecentAppsItemManager.class); 53 54 @VisibleForTesting 55 final List<UsageStats> mRecentApps; 56 private final int mUserId; 57 private final int mMaximumApps; 58 private final Context mContext; 59 private final PackageManager mPm; 60 private final UsageStatsManager mUsageStatsManager; 61 private final ApplicationsState mApplicationsState; 62 private final SparseArray<RecentAppStatsListener> mAppStatsListeners; 63 private final int mDaysThreshold; 64 private final List<String> mIgnoredPackages; 65 private Calendar mCalendar; 66 RecentAppsItemManager(Context context, int maximumApps)67 public RecentAppsItemManager(Context context, int maximumApps) { 68 this(context, maximumApps, ApplicationsState.getInstance( 69 (Application) context.getApplicationContext())); 70 } 71 72 @VisibleForTesting RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState)73 RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState) { 74 mContext = context; 75 mMaximumApps = maximumApps; 76 mUserId = UserHandle.myUserId(); 77 mPm = mContext.getPackageManager(); 78 mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); 79 mApplicationsState = applicationsState; 80 mRecentApps = new ArrayList<>(); 81 mAppStatsListeners = new SparseArray<>(); 82 mDaysThreshold = mContext.getResources() 83 .getInteger(R.integer.recent_apps_days_threshold); 84 mIgnoredPackages = Arrays.asList(mContext.getResources() 85 .getStringArray(R.array.recent_apps_ignored_packages)); 86 } 87 88 /** 89 * Starts fetching recently used apps 90 */ startLoading()91 public void startLoading() { 92 ThreadUtils.postOnBackgroundThread(() -> { 93 loadDisplayableRecentApps(mMaximumApps); 94 for (int i = 0; i < mAppStatsListeners.size(); i++) { 95 int finalIndex = i; 96 ThreadUtils.postOnMainThread(() -> mAppStatsListeners.valueAt(finalIndex) 97 .onRecentAppStatsLoaded(mRecentApps)); 98 } 99 }); 100 } 101 102 @Override compare(UsageStats a, UsageStats b)103 public final int compare(UsageStats a, UsageStats b) { 104 // return by descending order 105 return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); 106 } 107 108 /** 109 * Registers a listener that will be notified once the data is loaded. 110 */ addListener(@onNull RecentAppStatsListener listener)111 public void addListener(@NonNull RecentAppStatsListener listener) { 112 mAppStatsListeners.append(mAppStatsListeners.size(), listener); 113 } 114 115 @VisibleForTesting loadDisplayableRecentApps(int number)116 void loadDisplayableRecentApps(int number) { 117 mRecentApps.clear(); 118 mCalendar = Calendar.getInstance(); 119 mCalendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold); 120 List<UsageStats> mStats = mUsageStatsManager.queryUsageStats( 121 UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(), 122 System.currentTimeMillis()); 123 124 Map<String, UsageStats> map = new ArrayMap<>(); 125 for (UsageStats pkgStats : mStats) { 126 if (!shouldIncludePkgInRecents(pkgStats)) { 127 continue; 128 } 129 String pkgName = pkgStats.getPackageName(); 130 UsageStats existingStats = map.get(pkgName); 131 if (existingStats == null) { 132 map.put(pkgName, pkgStats); 133 } else { 134 existingStats.add(pkgStats); 135 } 136 } 137 List<UsageStats> packageStats = new ArrayList<>(); 138 packageStats.addAll(map.values()); 139 Collections.sort(packageStats, /* comparator= */ this); 140 int count = 0; 141 for (UsageStats stat : packageStats) { 142 ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 143 stat.getPackageName(), mUserId); 144 if (appEntry == null) { 145 continue; 146 } 147 mRecentApps.add(stat); 148 count++; 149 if (count >= number) { 150 break; 151 } 152 } 153 } 154 155 /** 156 * Whether or not the app should be included in recent list. 157 */ shouldIncludePkgInRecents(UsageStats stat)158 private boolean shouldIncludePkgInRecents(UsageStats stat) { 159 String pkgName = stat.getPackageName(); 160 if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) { 161 LOG.d("Invalid timestamp (usage time is more than 24 hours ago), skipping " 162 + pkgName); 163 return false; 164 } 165 166 if (mIgnoredPackages.contains(pkgName)) { 167 LOG.d("System package, skipping " + pkgName); 168 return false; 169 } 170 171 if (AppUtils.isHiddenSystemModule(mContext, pkgName)) { 172 return false; 173 } 174 175 Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 176 .setPackage(pkgName); 177 178 if (mPm.resolveActivity(launchIntent, 0) == null) { 179 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 180 ApplicationsState.AppEntry appEntry = 181 mApplicationsState.getEntry(pkgName, mUserId); 182 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 183 LOG.d("Not a user visible or instant app, skipping " + pkgName); 184 return false; 185 } 186 } 187 return true; 188 } 189 190 /** 191 * Callback that is called once the recently used apps have been fetched. 192 */ 193 public interface RecentAppStatsListener { 194 /** 195 * Called when the recently used apps are successfully loaded 196 */ onRecentAppStatsLoaded(List<UsageStats> recentAppStats)197 void onRecentAppStatsLoaded(List<UsageStats> recentAppStats); 198 } 199 } 200