• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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