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.settings.applications; 18 19 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 20 import static com.android.settings.Utils.SYSTEMUI_PACKAGE_NAME; 21 22 import android.app.Application; 23 import android.app.usage.UsageStats; 24 import android.app.usage.UsageStatsManager; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.os.PowerManager; 29 import android.os.UserHandle; 30 import android.os.UserManager; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.Log; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.VisibleForTesting; 37 38 import com.android.settingslib.applications.AppUtils; 39 import com.android.settingslib.applications.ApplicationsState; 40 import com.android.settingslib.core.lifecycle.LifecycleObserver; 41 import com.android.settingslib.core.lifecycle.events.OnStart; 42 import com.android.settingslib.utils.ThreadUtils; 43 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Calendar; 47 import java.util.Comparator; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Optional; 51 import java.util.Set; 52 import java.util.stream.Collectors; 53 54 /** 55 * A helper class that loads recent app data in the background and sends it in a callback to a 56 * listener. 57 */ 58 public class RecentAppStatsMixin implements LifecycleObserver, OnStart { 59 60 private static final String TAG = "RecentAppStatsMixin"; 61 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 62 63 @VisibleForTesting 64 List<UsageStatsWrapper> mRecentApps; 65 66 private final int mMaximumApps; 67 private final Context mContext; 68 private final PackageManager mPm; 69 private final UserManager mUserManager; 70 private final PowerManager mPowerManager; 71 private final ApplicationsState mApplicationsState; 72 private final List<RecentAppStatsListener> mAppStatsListeners; 73 private Calendar mCalendar; 74 75 static { 76 SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( 77 "android", 78 "com.android.phone", 79 SETTINGS_PACKAGE_NAME, 80 SYSTEMUI_PACKAGE_NAME, 81 "com.android.providers.calendar", 82 "com.android.providers.media" 83 )); 84 } 85 RecentAppStatsMixin(Context context, int maximumApps)86 public RecentAppStatsMixin(Context context, int maximumApps) { 87 mContext = context; 88 mMaximumApps = maximumApps; 89 mPm = mContext.getPackageManager(); 90 mPowerManager = mContext.getSystemService(PowerManager.class); 91 mUserManager = mContext.getSystemService(UserManager.class); 92 mApplicationsState = ApplicationsState.getInstance( 93 (Application) mContext.getApplicationContext()); 94 mRecentApps = new ArrayList<>(); 95 mAppStatsListeners = new ArrayList<>(); 96 } 97 98 @Override onStart()99 public void onStart() { 100 ThreadUtils.postOnBackgroundThread(() -> { 101 loadDisplayableRecentApps(mMaximumApps); 102 for (RecentAppStatsListener listener : mAppStatsListeners) { 103 ThreadUtils.postOnMainThread(() -> listener.onReloadDataCompleted(mRecentApps)); 104 } 105 }); 106 } 107 addListener(@onNull RecentAppStatsListener listener)108 public void addListener(@NonNull RecentAppStatsListener listener) { 109 mAppStatsListeners.add(listener); 110 } 111 112 @VisibleForTesting loadDisplayableRecentApps(int limit)113 void loadDisplayableRecentApps(int limit) { 114 mRecentApps.clear(); 115 mCalendar = Calendar.getInstance(); 116 mCalendar.add(Calendar.DAY_OF_YEAR, -1); 117 118 List<UsageStatsWrapper> usageStatsAllUsers = new ArrayList<>(); 119 120 List<UserHandle> profiles = mUserManager.getUserProfiles(); 121 for (UserHandle userHandle : profiles) { 122 int userId = userHandle.getIdentifier(); 123 124 final Optional<UsageStatsManager> usageStatsManager; 125 if (userHandle.getIdentifier() == UserHandle.myUserId()) { 126 usageStatsManager = Optional.ofNullable(userHandle).map( 127 handle -> mContext.getSystemService(UsageStatsManager.class)); 128 } else { 129 usageStatsManager = Optional.ofNullable(userHandle).map( 130 handle -> mContext.createContextAsUser(handle, /* flags */ 0) 131 .getSystemService(UsageStatsManager.class)); 132 } 133 134 List<UsageStats> profileStats = usageStatsManager 135 .map(statsManager -> getRecentAppsStats(statsManager, userId)) 136 .orElse(new ArrayList<>()); 137 usageStatsAllUsers.addAll(profileStats.stream() 138 .map(usageStats-> new UsageStatsWrapper(usageStats, userId)) 139 .collect(Collectors.toList())); 140 } 141 142 // Sort apps by latest timestamp. 143 usageStatsAllUsers.sort( 144 Comparator.comparingLong(a -> -1 * a.mUsageStats.getLastTimeUsed())); 145 mRecentApps.addAll(usageStatsAllUsers.stream().limit(limit).collect(Collectors.toList())); 146 } 147 getRecentAppsStats(UsageStatsManager usageStatsManager, int userId)148 private List<UsageStats> getRecentAppsStats(UsageStatsManager usageStatsManager, int userId) { 149 final List<UsageStats> recentAppStats = mPowerManager.isPowerSaveMode() 150 ? new ArrayList<>() 151 : usageStatsManager.queryUsageStats( 152 UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(), 153 System.currentTimeMillis()); 154 155 final Map<String, UsageStats> map = new ArrayMap<>(); 156 for (final UsageStats pkgStats : recentAppStats) { 157 if (!shouldIncludePkgInRecents(pkgStats, userId)) { 158 continue; 159 } 160 final String pkgName = pkgStats.getPackageName(); 161 final UsageStats existingStats = map.get(pkgName); 162 if (existingStats == null) { 163 map.put(pkgName, pkgStats); 164 } else { 165 existingStats.add(pkgStats); 166 } 167 } 168 final List<UsageStats> packageStats = new ArrayList<>(map.values()); 169 packageStats.sort(Comparator.comparingLong(UsageStats::getLastTimeUsed).reversed()); 170 return packageStats; 171 } 172 173 /** 174 * Whether or not the app should be included in recent list. 175 */ shouldIncludePkgInRecents(UsageStats stat, int userId)176 private boolean shouldIncludePkgInRecents(UsageStats stat, int userId) { 177 final String pkgName = stat.getPackageName(); 178 if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) { 179 Log.d(TAG, "Invalid timestamp (usage time is more than 24 hours ago), skipping " 180 + pkgName); 181 return false; 182 } 183 184 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 185 Log.d(TAG, "System package, skipping " + pkgName); 186 return false; 187 } 188 189 if (!android.content.pm.Flags.removeHiddenModuleUsage() 190 && AppUtils.isHiddenSystemModule(mContext, pkgName)) { 191 return false; 192 } 193 194 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, userId); 195 if (appEntry == null) { 196 return false; 197 } 198 199 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 200 .setPackage(pkgName); 201 if (mPm.resolveActivityAsUser(launchIntent, 0, userId) == null) { 202 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 203 if (appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 204 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 205 return false; 206 } 207 } 208 209 return true; 210 } 211 212 public interface RecentAppStatsListener { 213 214 /** A callback after loading the recent app data. */ onReloadDataCompleted(List<UsageStatsWrapper> recentApps)215 void onReloadDataCompleted(List<UsageStatsWrapper> recentApps); 216 } 217 218 static class UsageStatsWrapper { 219 220 public final UsageStats mUsageStats; 221 public final int mUserId; 222 UsageStatsWrapper(UsageStats usageStats, int userId)223 UsageStatsWrapper(UsageStats usageStats, int userId) { 224 mUsageStats = usageStats; 225 mUserId = userId; 226 } 227 228 @Override toString()229 public String toString() { 230 return String.format("UsageStatsWrapper(pkg:%s,uid:%s)", 231 mUsageStats.getPackageName(), mUserId); 232 } 233 } 234 } 235