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