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