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