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.storagemanager.deletionhelper; 18 19 import android.app.usage.UsageStats; 20 import android.app.usage.UsageStatsManager; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.graphics.drawable.Drawable; 27 import android.os.SystemProperties; 28 import android.os.UserHandle; 29 import android.support.annotation.VisibleForTesting; 30 import android.text.format.DateUtils; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import com.android.settingslib.applications.PackageManagerWrapper; 35 import com.android.settingslib.applications.StorageStatsSource; 36 import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; 37 import com.android.storagemanager.deletionhelper.AppsAsyncLoader.PackageInfo; 38 import com.android.storagemanager.utils.AsyncLoader; 39 40 import java.io.IOException; 41 import java.text.Collator; 42 import java.util.ArrayList; 43 import java.util.Comparator; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.concurrent.TimeUnit; 47 import java.util.Collections; 48 import java.util.stream.Collectors; 49 50 /** 51 * AppsAsyncLoader is a Loader which loads app storage information and categories it by the app's 52 * specified categorization. 53 */ 54 public class AppsAsyncLoader extends AsyncLoader<List<PackageInfo>> { 55 private static final String TAG = "AppsAsyncLoader"; 56 57 public static final long NEVER_USED = Long.MAX_VALUE; 58 public static final long UNKNOWN_LAST_USE = -1; 59 public static final long UNUSED_DAYS_DELETION_THRESHOLD = 90; 60 public static final long MIN_DELETION_THRESHOLD = Long.MIN_VALUE; 61 public static final int NORMAL_THRESHOLD = 0; 62 public static final int SIZE_UNKNOWN = -1; 63 public static final int SIZE_INVALID = -2; 64 public static final int NO_THRESHOLD = 1; 65 private static final String DEBUG_APP_UNUSED_OVERRIDE = "debug.asm.app_unused_limit"; 66 private static final long DAYS_IN_A_TYPICAL_YEAR = 365; 67 68 protected Clock mClock; 69 protected AppsAsyncLoader.AppFilter mFilter; 70 private int mUserId; 71 private String mUuid; 72 private StorageStatsSource mStatsManager; 73 private PackageManagerWrapper mPackageManager; 74 75 private UsageStatsManager mUsageStatsManager; 76 AppsAsyncLoader( Context context, int userId, String uuid, StorageStatsSource source, PackageManagerWrapper pm, UsageStatsManager um, AppsAsyncLoader.AppFilter filter)77 private AppsAsyncLoader( 78 Context context, 79 int userId, 80 String uuid, 81 StorageStatsSource source, 82 PackageManagerWrapper pm, 83 UsageStatsManager um, 84 AppsAsyncLoader.AppFilter filter) { 85 super(context); 86 mUserId = userId; 87 mUuid = uuid; 88 mStatsManager = source; 89 mPackageManager = pm; 90 mUsageStatsManager = um; 91 mClock = new Clock(); 92 mFilter = filter; 93 } 94 95 @Override loadInBackground()96 public List<PackageInfo> loadInBackground() { 97 return loadApps(); 98 } 99 loadApps()100 private List<PackageInfo> loadApps() { 101 ArraySet<Integer> seenUid = new ArraySet<>(); // some apps share a uid 102 103 long now = mClock.getCurrentTime(); 104 long startTime = now - DateUtils.YEAR_IN_MILLIS; 105 final Map<String, UsageStats> map = 106 mUsageStatsManager.queryAndAggregateUsageStats(startTime, now); 107 final Map<String, UsageStats> alternateMap = 108 getLatestUsageStatsByPackageName(startTime, now); 109 110 List<ApplicationInfo> applicationInfos = 111 mPackageManager.getInstalledApplicationsAsUser(0, mUserId); 112 List<PackageInfo> stats = new ArrayList<>(); 113 int size = applicationInfos.size(); 114 mFilter.init(); 115 for (int i = 0; i < size; i++) { 116 ApplicationInfo app = applicationInfos.get(i); 117 if (seenUid.contains(app.uid)) { 118 continue; 119 } 120 121 UsageStats usageStats = map.get(app.packageName); 122 UsageStats alternateUsageStats = alternateMap.get(app.packageName); 123 124 final AppStorageStats appSpace; 125 try { 126 appSpace = mStatsManager.getStatsForUid(app.volumeUuid, app.uid); 127 } catch (IOException e) { 128 Log.w(TAG, e); 129 continue; 130 } 131 132 PackageInfo extraInfo = 133 new PackageInfo.Builder() 134 .setDaysSinceLastUse( 135 getDaysSinceLastUse( 136 getGreaterUsageStats( 137 app.packageName, 138 usageStats, 139 alternateUsageStats))) 140 .setDaysSinceFirstInstall(getDaysSinceInstalled(app.packageName)) 141 .setUserId(UserHandle.getUserId(app.uid)) 142 .setPackageName(app.packageName) 143 .setSize(appSpace.getTotalBytes()) 144 .setFlags(app.flags) 145 .setIcon(mPackageManager.getUserBadgedIcon(app)) 146 .setLabel(mPackageManager.loadLabel(app)) 147 .build(); 148 seenUid.add(app.uid); 149 if (mFilter.filterApp(extraInfo) && !isDefaultLauncher(mPackageManager, extraInfo)) { 150 stats.add(extraInfo); 151 } 152 } 153 stats.sort(PACKAGE_INFO_COMPARATOR); 154 return stats; 155 } 156 157 @VisibleForTesting getGreaterUsageStats(String packageName, UsageStats primary, UsageStats alternate)158 UsageStats getGreaterUsageStats(String packageName, UsageStats primary, UsageStats alternate) { 159 long primaryLastUsed = primary != null ? primary.getLastTimeUsed() : 0; 160 long alternateLastUsed = alternate != null ? alternate.getLastTimeUsed() : 0; 161 162 if (primaryLastUsed != alternateLastUsed) { 163 Log.w( 164 TAG, 165 new StringBuilder("Usage stats mismatch for ") 166 .append(packageName) 167 .append(" ") 168 .append(primaryLastUsed) 169 .append(" ") 170 .append(alternateLastUsed) 171 .toString()); 172 } 173 174 return (primaryLastUsed > alternateLastUsed) ? primary : alternate; 175 } 176 getLatestUsageStatsByPackageName(long startTime, long endTime)177 private Map<String, UsageStats> getLatestUsageStatsByPackageName(long startTime, long endTime) { 178 List<UsageStats> usageStats = 179 mUsageStatsManager.queryUsageStats( 180 UsageStatsManager.INTERVAL_YEARLY, startTime, endTime); 181 Map<String, List<UsageStats>> groupedByPackageName = 182 usageStats.stream().collect(Collectors.groupingBy(UsageStats::getPackageName)); 183 184 ArrayMap<String, UsageStats> latestStatsByPackageName = new ArrayMap<>(); 185 groupedByPackageName 186 .entrySet() 187 .stream() 188 .forEach( 189 // Flattens the list of UsageStats to only have the latest by 190 // getLastTimeUsed, retaining the package name as the key. 191 (Map.Entry<String, List<UsageStats>> item) -> { 192 latestStatsByPackageName.put( 193 item.getKey(), 194 Collections.max( 195 item.getValue(), 196 (UsageStats o1, UsageStats o2) -> 197 Long.compare( 198 o1.getLastTimeUsed(), 199 o2.getLastTimeUsed()))); 200 }); 201 202 return latestStatsByPackageName; 203 } 204 205 @Override onDiscardResult(List<PackageInfo> result)206 protected void onDiscardResult(List<PackageInfo> result) {} 207 isDefaultLauncher( PackageManagerWrapper packageManager, PackageInfo info)208 private static boolean isDefaultLauncher( 209 PackageManagerWrapper packageManager, PackageInfo info) { 210 if (packageManager == null) { 211 return false; 212 } 213 214 final List<ResolveInfo> homeActivities = new ArrayList<>(); 215 ComponentName defaultActivity = packageManager.getHomeActivities(homeActivities); 216 if (defaultActivity != null) { 217 String packageName = defaultActivity.getPackageName(); 218 return packageName == null 219 ? false 220 : defaultActivity.getPackageName().equals(info.packageName); 221 } 222 223 return false; 224 } 225 226 public static class Builder { 227 private Context mContext; 228 private int mUid; 229 private String mUuid; 230 private StorageStatsSource mStorageStatsSource; 231 private PackageManagerWrapper mPackageManager; 232 private UsageStatsManager mUsageStatsManager; 233 private AppsAsyncLoader.AppFilter mFilter; 234 Builder(Context context)235 public Builder(Context context) { 236 mContext = context; 237 } 238 setUid(int uid)239 public Builder setUid(int uid) { 240 mUid = uid; 241 return this; 242 } 243 setUuid(String uuid)244 public Builder setUuid(String uuid) { 245 this.mUuid = uuid; 246 return this; 247 } 248 setStorageStatsSource(StorageStatsSource storageStatsSource)249 public Builder setStorageStatsSource(StorageStatsSource storageStatsSource) { 250 this.mStorageStatsSource = storageStatsSource; 251 return this; 252 } 253 setPackageManager(PackageManagerWrapper packageManager)254 public Builder setPackageManager(PackageManagerWrapper packageManager) { 255 this.mPackageManager = packageManager; 256 return this; 257 } 258 setUsageStatsManager(UsageStatsManager usageStatsManager)259 public Builder setUsageStatsManager(UsageStatsManager usageStatsManager) { 260 this.mUsageStatsManager = usageStatsManager; 261 return this; 262 } 263 setFilter(AppFilter filter)264 public Builder setFilter(AppFilter filter) { 265 this.mFilter = filter; 266 return this; 267 } 268 build()269 public AppsAsyncLoader build() { 270 return new AppsAsyncLoader( 271 mContext, 272 mUid, 273 mUuid, 274 mStorageStatsSource, 275 mPackageManager, 276 mUsageStatsManager, 277 mFilter); 278 } 279 } 280 281 /** 282 * Comparator that checks PackageInfo to see if it describes the same app based on the name and 283 * user it belongs to. This comparator does NOT fulfill the standard java equality contract 284 * because it only checks a few fields. 285 */ 286 public static final Comparator<PackageInfo> PACKAGE_INFO_COMPARATOR = 287 new Comparator<PackageInfo>() { 288 private final Collator sCollator = Collator.getInstance(); 289 290 @Override 291 public int compare(PackageInfo object1, PackageInfo object2) { 292 if (object1.size < object2.size) return 1; 293 if (object1.size > object2.size) return -1; 294 int compareResult = sCollator.compare(object1.label, object2.label); 295 if (compareResult != 0) { 296 return compareResult; 297 } 298 compareResult = sCollator.compare(object1.packageName, object2.packageName); 299 if (compareResult != 0) { 300 return compareResult; 301 } 302 return object1.userId - object2.userId; 303 } 304 }; 305 306 public static final AppFilter FILTER_NO_THRESHOLD = 307 new AppFilter() { 308 @Override 309 public void init() {} 310 311 @Override 312 public boolean filterApp(PackageInfo info) { 313 if (info == null) { 314 return false; 315 } 316 return !isBundled(info) 317 && !isPersistentProcess(info) 318 && isExtraInfoValid(info, MIN_DELETION_THRESHOLD); 319 } 320 }; 321 322 /** 323 * Filters only non-system apps which haven't been used in the last 60 days. If an app's last 324 * usage is unknown, it is skipped. 325 */ 326 public static final AppFilter FILTER_USAGE_STATS = 327 new AppFilter() { 328 private long mUnusedDaysThreshold; 329 330 @Override 331 public void init() { 332 mUnusedDaysThreshold = 333 SystemProperties.getLong( 334 DEBUG_APP_UNUSED_OVERRIDE, UNUSED_DAYS_DELETION_THRESHOLD); 335 } 336 337 @Override 338 public boolean filterApp(PackageInfo info) { 339 if (info == null) { 340 return false; 341 } 342 return !isBundled(info) 343 && !isPersistentProcess(info) 344 && isExtraInfoValid(info, mUnusedDaysThreshold); 345 } 346 }; 347 isBundled(PackageInfo info)348 private static boolean isBundled(PackageInfo info) { 349 return (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 350 } 351 isPersistentProcess(PackageInfo info)352 private static boolean isPersistentProcess(PackageInfo info) { 353 return (info.flags & ApplicationInfo.FLAG_PERSISTENT) != 0; 354 } 355 isExtraInfoValid(Object extraInfo, long unusedDaysThreshold)356 private static boolean isExtraInfoValid(Object extraInfo, long unusedDaysThreshold) { 357 if (extraInfo == null || !(extraInfo instanceof PackageInfo)) { 358 return false; 359 } 360 361 PackageInfo state = (PackageInfo) extraInfo; 362 363 // If we are missing information, let's be conservative and not show it. 364 if (state.daysSinceFirstInstall == UNKNOWN_LAST_USE 365 || state.daysSinceLastUse == UNKNOWN_LAST_USE) { 366 Log.w(TAG, "Missing information. Skipping app"); 367 return false; 368 } 369 370 // If the app has never been used, daysSinceLastUse is Long.MAX_VALUE, so the first 371 // install is always the most recent use. 372 long mostRecentUse = Math.min(state.daysSinceFirstInstall, state.daysSinceLastUse); 373 if (mostRecentUse >= unusedDaysThreshold) { 374 Log.i(TAG, "Accepting " + state.packageName + " with a minimum of " + mostRecentUse); 375 } 376 return mostRecentUse >= unusedDaysThreshold; 377 } 378 getDaysSinceLastUse(UsageStats stats)379 private long getDaysSinceLastUse(UsageStats stats) { 380 if (stats == null) { 381 return NEVER_USED; 382 } 383 long lastUsed = stats.getLastTimeUsed(); 384 // Sometimes, a usage is recorded without a time and we don't know when the use was. 385 if (lastUsed <= 0) { 386 return UNKNOWN_LAST_USE; 387 } 388 389 // Theoretically, this should be impossible, but UsageStatsService, uh, finds a way. 390 long days = (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - lastUsed)); 391 if (days > DAYS_IN_A_TYPICAL_YEAR) { 392 return NEVER_USED; 393 } 394 return days; 395 } 396 getDaysSinceInstalled(String packageName)397 private long getDaysSinceInstalled(String packageName) { 398 android.content.pm.PackageInfo pi = null; 399 try { 400 pi = mPackageManager.getPackageInfo(packageName, 0); 401 } catch (PackageManager.NameNotFoundException e) { 402 Log.e(TAG, packageName + " was not found."); 403 } 404 405 if (pi == null) { 406 return UNKNOWN_LAST_USE; 407 } 408 return (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - pi.firstInstallTime)); 409 } 410 411 public interface AppFilter { 412 413 /** 414 * Note: This method must be manually called before using an app filter. It does not get 415 * called on construction. 416 */ init()417 void init(); 418 init(Context context)419 default void init(Context context) { 420 init(); 421 } 422 423 /** 424 * Returns true or false depending on whether the app should be filtered or not. 425 * 426 * @param info the PackageInfo for the app in question. 427 * @return true if the app should be included, false if it should be filtered out. 428 */ filterApp(PackageInfo info)429 boolean filterApp(PackageInfo info); 430 } 431 432 /** PackageInfo contains all the information needed to present apps for deletion to users. */ 433 public static class PackageInfo { 434 435 public long daysSinceLastUse; 436 public long daysSinceFirstInstall; 437 public int userId; 438 public String packageName; 439 public long size; 440 public Drawable icon; 441 public CharSequence label; 442 /** 443 * Flags from {@link ApplicationInfo} that set whether the app is a regular app or something 444 * special like a system app. 445 */ 446 public int flags; 447 PackageInfo( long daysSinceLastUse, long daysSinceFirstInstall, int userId, String packageName, long size, int flags, Drawable icon, CharSequence label)448 private PackageInfo( 449 long daysSinceLastUse, 450 long daysSinceFirstInstall, 451 int userId, 452 String packageName, 453 long size, 454 int flags, 455 Drawable icon, 456 CharSequence label) { 457 this.daysSinceLastUse = daysSinceLastUse; 458 this.daysSinceFirstInstall = daysSinceFirstInstall; 459 this.userId = userId; 460 this.packageName = packageName; 461 this.size = size; 462 this.flags = flags; 463 this.icon = icon; 464 this.label = label; 465 } 466 467 public static class Builder { 468 private long mDaysSinceLastUse; 469 private long mDaysSinceFirstInstall; 470 private int mUserId; 471 private String mPackageName; 472 private long mSize; 473 private int mFlags; 474 private Drawable mIcon; 475 private CharSequence mLabel; 476 setDaysSinceLastUse(long daysSinceLastUse)477 public Builder setDaysSinceLastUse(long daysSinceLastUse) { 478 this.mDaysSinceLastUse = daysSinceLastUse; 479 return this; 480 } 481 setDaysSinceFirstInstall(long daysSinceFirstInstall)482 public Builder setDaysSinceFirstInstall(long daysSinceFirstInstall) { 483 this.mDaysSinceFirstInstall = daysSinceFirstInstall; 484 return this; 485 } 486 setUserId(int userId)487 public Builder setUserId(int userId) { 488 this.mUserId = userId; 489 return this; 490 } 491 setPackageName(String packageName)492 public Builder setPackageName(String packageName) { 493 this.mPackageName = packageName; 494 return this; 495 } 496 setSize(long size)497 public Builder setSize(long size) { 498 this.mSize = size; 499 return this; 500 } 501 setFlags(int flags)502 public Builder setFlags(int flags) { 503 this.mFlags = flags; 504 return this; 505 } 506 setIcon(Drawable icon)507 public Builder setIcon(Drawable icon) { 508 this.mIcon = icon; 509 return this; 510 } 511 setLabel(CharSequence label)512 public Builder setLabel(CharSequence label) { 513 this.mLabel = label; 514 return this; 515 } 516 build()517 public PackageInfo build() { 518 return new PackageInfo( 519 mDaysSinceLastUse, 520 mDaysSinceFirstInstall, 521 mUserId, 522 mPackageName, 523 mSize, 524 mFlags, 525 mIcon, 526 mLabel); 527 } 528 } 529 } 530 531 /** Clock provides the current time. */ 532 static class Clock { getCurrentTime()533 public long getCurrentTime() { 534 return System.currentTimeMillis(); 535 } 536 } 537 } 538