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