• 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.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