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.storagemanager.deletionhelper; 18 19import android.app.usage.UsageStats; 20import android.app.usage.UsageStatsManager; 21import android.content.ComponentName; 22import android.content.Context; 23import android.content.pm.ApplicationInfo; 24import android.content.pm.PackageManager; 25import android.content.pm.ResolveInfo; 26import android.graphics.drawable.Drawable; 27import android.os.SystemProperties; 28import android.os.UserHandle; 29import android.support.annotation.VisibleForTesting; 30import android.text.format.DateUtils; 31import android.util.ArrayMap; 32import android.util.ArraySet; 33import android.util.Log; 34import com.android.settingslib.applications.StorageStatsSource; 35import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; 36import com.android.settingslib.wrapper.PackageManagerWrapper; 37import com.android.storagemanager.deletionhelper.AppsAsyncLoader.PackageInfo; 38import com.android.storagemanager.utils.AsyncLoader; 39 40import java.io.IOException; 41import java.text.Collator; 42import java.util.ArrayList; 43import java.util.Comparator; 44import java.util.List; 45import java.util.Map; 46import java.util.concurrent.TimeUnit; 47import java.util.Collections; 48import 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 */ 54public 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 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 96 public List<PackageInfo> loadInBackground() { 97 return loadApps(); 98 } 99 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 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 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 206 protected void onDiscardResult(List<PackageInfo> result) {} 207 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 235 public Builder(Context context) { 236 mContext = context; 237 } 238 239 public Builder setUid(int uid) { 240 mUid = uid; 241 return this; 242 } 243 244 public Builder setUuid(String uuid) { 245 this.mUuid = uuid; 246 return this; 247 } 248 249 public Builder setStorageStatsSource(StorageStatsSource storageStatsSource) { 250 this.mStorageStatsSource = storageStatsSource; 251 return this; 252 } 253 254 public Builder setPackageManager(PackageManagerWrapper packageManager) { 255 this.mPackageManager = packageManager; 256 return this; 257 } 258 259 public Builder setUsageStatsManager(UsageStatsManager usageStatsManager) { 260 this.mUsageStatsManager = usageStatsManager; 261 return this; 262 } 263 264 public Builder setFilter(AppFilter filter) { 265 this.mFilter = filter; 266 return this; 267 } 268 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 348 private static boolean isBundled(PackageInfo info) { 349 return (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 350 } 351 352 private static boolean isPersistentProcess(PackageInfo info) { 353 return (info.flags & ApplicationInfo.FLAG_PERSISTENT) != 0; 354 } 355 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 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 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 */ 417 void init(); 418 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 */ 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 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 477 public Builder setDaysSinceLastUse(long daysSinceLastUse) { 478 this.mDaysSinceLastUse = daysSinceLastUse; 479 return this; 480 } 481 482 public Builder setDaysSinceFirstInstall(long daysSinceFirstInstall) { 483 this.mDaysSinceFirstInstall = daysSinceFirstInstall; 484 return this; 485 } 486 487 public Builder setUserId(int userId) { 488 this.mUserId = userId; 489 return this; 490 } 491 492 public Builder setPackageName(String packageName) { 493 this.mPackageName = packageName; 494 return this; 495 } 496 497 public Builder setSize(long size) { 498 this.mSize = size; 499 return this; 500 } 501 502 public Builder setFlags(int flags) { 503 this.mFlags = flags; 504 return this; 505 } 506 507 public Builder setIcon(Drawable icon) { 508 this.mIcon = icon; 509 return this; 510 } 511 512 public Builder setLabel(CharSequence label) { 513 this.mLabel = label; 514 return this; 515 } 516 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 { 533 public long getCurrentTime() { 534 return System.currentTimeMillis(); 535 } 536 } 537} 538