1 /*
2  * Copyright (C) 2016 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 package com.android.storagemanager.deletionhelper;
17 
18 import android.app.usage.UsageStats;
19 import android.app.usage.UsageStatsManager;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.os.SystemProperties;
26 import android.os.UserHandle;
27 import android.text.format.DateUtils;
28 import android.util.Log;
29 import com.android.storagemanager.deletionhelper.AppStateBaseBridge.Callback;
30 import com.android.settingslib.applications.ApplicationsState;
31 import com.android.settingslib.applications.ApplicationsState.AppEntry;
32 import com.android.settingslib.applications.ApplicationsState.AppFilter;
33 
34 import java.util.ArrayList;
35 import java.util.Map;
36 import java.util.concurrent.TimeUnit;
37 
38 /**
39  * Connects data from the UsageStatsManager to the ApplicationsState.
40  */
41 public class AppStateUsageStatsBridge extends AppStateBaseBridge {
42     private static final String TAG = "AppStateUsageStatsBridge";
43 
44     private static final String DEBUG_APP_UNUSED_OVERRIDE = "debug.asm.app_unused_limit";
45     public static final long NEVER_USED = Long.MAX_VALUE;
46     public static final long UNKNOWN_LAST_USE = -1;
47     public static final long UNUSED_DAYS_DELETION_THRESHOLD = 90;
48     private static final long DAYS_IN_A_TYPICAL_YEAR = 365;
49     public static final long MIN_DELETION_THRESHOLD = Long.MIN_VALUE;
50     public static final int NORMAL_THRESHOLD = 0;
51     public static final int NO_THRESHOLD = 1;
52 
53     private UsageStatsManager mUsageStatsManager;
54     private PackageManager mPm;
55     // This clock is used to provide the time. By default, it uses the system clock, but can be
56     // replaced for test purposes.
57     protected Clock mClock;
58 
AppStateUsageStatsBridge(Context context, ApplicationsState appState, Callback callback)59     public AppStateUsageStatsBridge(Context context, ApplicationsState appState,
60             Callback callback) {
61         super(appState, callback);
62         mUsageStatsManager =
63                 (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
64         mPm = context.getPackageManager();
65         mClock = new Clock();
66     }
67 
68     @Override
loadAllExtraInfo()69     protected void loadAllExtraInfo() {
70         ArrayList<AppEntry> apps = mAppSession.getAllApps();
71         if (apps == null) return;
72 
73         final Map<String, UsageStats> map = getAggregatedUsageStats();
74         for (AppEntry entry : apps) {
75             UsageStats usageStats = map.get(entry.info.packageName);
76             entry.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
77                     getDaysSinceInstalled(entry.info.packageName),
78                     UserHandle.getUserId(entry.info.uid));
79         }
80     }
81 
82     @Override
updateExtraInfo(AppEntry app, String pkg, int uid)83     protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
84         Map<String, UsageStats> map = getAggregatedUsageStats();
85         UsageStats usageStats = map.get(app.info.packageName);
86         app.extraInfo = new UsageStatsState(getDaysSinceLastUse(usageStats),
87                 getDaysSinceInstalled(app.info.packageName),
88                 UserHandle.getUserId(app.info.uid));
89     }
90 
getDaysSinceLastUse(UsageStats stats)91     private long getDaysSinceLastUse(UsageStats stats) {
92         if (stats == null) {
93             return NEVER_USED;
94         }
95         long lastUsed = stats.getLastTimeUsed();
96         // Sometimes, a usage is recorded without a time and we don't know when the use was.
97         if (lastUsed <= 0) {
98             return UNKNOWN_LAST_USE;
99         }
100 
101         // Theoretically, this should be impossible, but UsageStatsService, uh, finds a way.
102         long days = (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - lastUsed));
103         if (days > DAYS_IN_A_TYPICAL_YEAR) {
104             return NEVER_USED;
105         }
106         return days;
107     }
108 
getDaysSinceInstalled(String packageName)109     private long getDaysSinceInstalled(String packageName) {
110         PackageInfo pi = null;
111         try {
112             pi = mPm.getPackageInfo(packageName, 0);
113         } catch (PackageManager.NameNotFoundException e) {
114             Log.e(TAG, packageName + " was not found.");
115         }
116 
117         if (pi == null) {
118             return UNKNOWN_LAST_USE;
119         }
120         return (TimeUnit.MILLISECONDS.toDays(mClock.getCurrentTime() - pi.firstInstallTime));
121     }
122 
getAggregatedUsageStats()123     private Map<String, UsageStats> getAggregatedUsageStats() {
124         long now = mClock.getCurrentTime();
125         long startTime = now - DateUtils.YEAR_IN_MILLIS;
126         return mUsageStatsManager.queryAndAggregateUsageStats(startTime, now);
127     }
128 
isBundled(AppEntry info)129     private static boolean isBundled(AppEntry info) {
130         return (info.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
131     }
132 
isPersistentProcess(AppEntry info)133     private static boolean isPersistentProcess(AppEntry info) {
134         return (info.info.flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
135     }
136 
isExtraInfoValid(Object extraInfo, long unusedDaysThreshold)137     private static boolean isExtraInfoValid(Object extraInfo, long unusedDaysThreshold) {
138         if (extraInfo == null || !(extraInfo instanceof UsageStatsState)) {
139             return false;
140         }
141 
142         UsageStatsState state = (UsageStatsState) extraInfo;
143 
144         // If we are missing information, let's be conservative and not show it.
145         if (state.daysSinceFirstInstall == UNKNOWN_LAST_USE
146                 || state.daysSinceLastUse == UNKNOWN_LAST_USE) {
147             Log.w(TAG, "Missing information. Skipping app");
148             return false;
149         }
150 
151         // If the app has never been used, daysSinceLastUse is Long.MAX_VALUE, so the first
152         // install is always the most recent use.
153         long mostRecentUse = Math.min(state.daysSinceFirstInstall, state.daysSinceLastUse);
154         return mostRecentUse >= unusedDaysThreshold;
155     }
156 
157     public static final AppFilter FILTER_NO_THRESHOLD =
158             new AppFilter() {
159                 @Override
160                 public void init() {}
161 
162                 @Override
163                 public boolean filterApp(AppEntry info) {
164                     if (info == null) {
165                         return false;
166                     }
167                     return isExtraInfoValid(info.extraInfo, MIN_DELETION_THRESHOLD)
168                             && !isBundled(info)
169                             && !isPersistentProcess(info);
170                 }
171             };
172 
173     /**
174      * Filters only non-system apps which haven't been used in the last 60 days. If an app's last
175      * usage is unknown, it is skipped.
176      */
177     public static final AppFilter FILTER_USAGE_STATS =
178             new AppFilter() {
179                 private long mUnusedDaysThreshold;
180 
181                 @Override
182                 public void init() {
183                     mUnusedDaysThreshold =
184                             SystemProperties.getLong(
185                                     DEBUG_APP_UNUSED_OVERRIDE, UNUSED_DAYS_DELETION_THRESHOLD);
186                 }
187 
188                 @Override
189                 public boolean filterApp(AppEntry info) {
190                     if (info == null) {
191                         return false;
192                     }
193                     return isExtraInfoValid(info.extraInfo, mUnusedDaysThreshold)
194                             && !isBundled(info)
195                             && !isPersistentProcess(info);
196                 }
197             };
198 
199     /**
200      * UsageStatsState contains the days since the last use and first install of a given app.
201      */
202     public static class UsageStatsState {
203         public long daysSinceLastUse;
204         public long daysSinceFirstInstall;
205         public int userId;
206 
UsageStatsState(long daysSinceLastUse, long daysSinceFirstInstall, int userId)207         public UsageStatsState(long daysSinceLastUse, long daysSinceFirstInstall, int userId) {
208             this.daysSinceLastUse = daysSinceLastUse;
209             this.daysSinceFirstInstall = daysSinceFirstInstall;
210             this.userId = userId;
211         }
212     }
213 
214     /**
215      * Clock provides the current time.
216      */
217     static class Clock {
getCurrentTime()218         public long getCurrentTime() {
219             return System.currentTimeMillis();
220         }
221     }
222 }
223