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.server.storage; 18 19 import android.annotation.MainThread; 20 import android.app.usage.CacheQuotaHint; 21 import android.app.usage.CacheQuotaService; 22 import android.app.usage.ICacheQuotaService; 23 import android.app.usage.UsageStats; 24 import android.app.usage.UsageStatsManager; 25 import android.app.usage.UsageStatsManagerInternal; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.ServiceConnection; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.pm.ServiceInfo; 34 import android.content.pm.UserInfo; 35 import android.os.AsyncTask; 36 import android.os.Bundle; 37 import android.os.Environment; 38 import android.os.IBinder; 39 import android.os.RemoteCallback; 40 import android.os.RemoteException; 41 import android.os.UserHandle; 42 import android.os.UserManager; 43 import android.text.format.DateUtils; 44 import android.util.ArrayMap; 45 import android.util.Pair; 46 import android.util.Slog; 47 import android.util.SparseLongArray; 48 import android.util.Xml; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 import com.android.internal.os.AtomicFile; 52 import com.android.internal.util.FastXmlSerializer; 53 import com.android.internal.util.Preconditions; 54 import com.android.server.pm.Installer; 55 56 import org.xmlpull.v1.XmlPullParser; 57 import org.xmlpull.v1.XmlPullParserException; 58 import org.xmlpull.v1.XmlSerializer; 59 60 import java.io.File; 61 import java.io.FileInputStream; 62 import java.io.FileNotFoundException; 63 import java.io.FileOutputStream; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.nio.charset.StandardCharsets; 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.Map; 70 71 /** 72 * CacheQuotaStrategy is a strategy for determining cache quotas using usage stats and foreground 73 * time using the calculation as defined in the refuel rocket. 74 */ 75 public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { 76 private static final String TAG = "CacheQuotaStrategy"; 77 78 private final Object mLock = new Object(); 79 80 // XML Constants 81 private static final String CACHE_INFO_TAG = "cache-info"; 82 private static final String ATTR_PREVIOUS_BYTES = "previousBytes"; 83 private static final String TAG_QUOTA = "quota"; 84 private static final String ATTR_UUID = "uuid"; 85 private static final String ATTR_UID = "uid"; 86 private static final String ATTR_QUOTA_IN_BYTES = "bytes"; 87 88 private final Context mContext; 89 private final UsageStatsManagerInternal mUsageStats; 90 private final Installer mInstaller; 91 private final ArrayMap<String, SparseLongArray> mQuotaMap; 92 private ServiceConnection mServiceConnection; 93 private ICacheQuotaService mRemoteService; 94 private AtomicFile mPreviousValuesFile; 95 CacheQuotaStrategy( Context context, UsageStatsManagerInternal usageStatsManager, Installer installer, ArrayMap<String, SparseLongArray> quotaMap)96 public CacheQuotaStrategy( 97 Context context, UsageStatsManagerInternal usageStatsManager, Installer installer, 98 ArrayMap<String, SparseLongArray> quotaMap) { 99 mContext = Preconditions.checkNotNull(context); 100 mUsageStats = Preconditions.checkNotNull(usageStatsManager); 101 mInstaller = Preconditions.checkNotNull(installer); 102 mQuotaMap = Preconditions.checkNotNull(quotaMap); 103 mPreviousValuesFile = new AtomicFile(new File( 104 new File(Environment.getDataDirectory(), "system"), "cachequota.xml")); 105 } 106 107 /** 108 * Recalculates the quotas and stores them to installd. 109 */ recalculateQuotas()110 public void recalculateQuotas() { 111 createServiceConnection(); 112 113 ComponentName component = getServiceComponentName(); 114 if (component != null) { 115 Intent intent = new Intent(); 116 intent.setComponent(component); 117 mContext.bindServiceAsUser( 118 intent, mServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.CURRENT); 119 } 120 } 121 createServiceConnection()122 private void createServiceConnection() { 123 // If we're already connected, don't create a new connection. 124 if (mServiceConnection != null) { 125 return; 126 } 127 128 mServiceConnection = new ServiceConnection() { 129 @Override 130 @MainThread 131 public void onServiceConnected(ComponentName name, IBinder service) { 132 Runnable runnable = new Runnable() { 133 @Override 134 public void run() { 135 synchronized (mLock) { 136 mRemoteService = ICacheQuotaService.Stub.asInterface(service); 137 List<CacheQuotaHint> requests = getUnfulfilledRequests(); 138 final RemoteCallback remoteCallback = 139 new RemoteCallback(CacheQuotaStrategy.this); 140 try { 141 mRemoteService.computeCacheQuotaHints(remoteCallback, requests); 142 } catch (RemoteException ex) { 143 Slog.w(TAG, 144 "Remote exception occurred while trying to get cache quota", 145 ex); 146 } 147 } 148 } 149 }; 150 AsyncTask.execute(runnable); 151 } 152 153 @Override 154 @MainThread 155 public void onServiceDisconnected(ComponentName name) { 156 synchronized (mLock) { 157 mRemoteService = null; 158 } 159 } 160 }; 161 } 162 163 /** 164 * Returns a list of CacheQuotaHints which do not have their quotas filled out for apps 165 * which have been used in the last year. 166 */ getUnfulfilledRequests()167 private List<CacheQuotaHint> getUnfulfilledRequests() { 168 long timeNow = System.currentTimeMillis(); 169 long oneYearAgo = timeNow - DateUtils.YEAR_IN_MILLIS; 170 171 List<CacheQuotaHint> requests = new ArrayList<>(); 172 UserManager um = mContext.getSystemService(UserManager.class); 173 final List<UserInfo> users = um.getUsers(); 174 final int userCount = users.size(); 175 final PackageManager packageManager = mContext.getPackageManager(); 176 for (int i = 0; i < userCount; i++) { 177 UserInfo info = users.get(i); 178 List<UsageStats> stats = 179 mUsageStats.queryUsageStatsForUser(info.id, UsageStatsManager.INTERVAL_BEST, 180 oneYearAgo, timeNow, /*obfuscateInstantApps=*/ false); 181 if (stats == null) { 182 continue; 183 } 184 185 for (UsageStats stat : stats) { 186 String packageName = stat.getPackageName(); 187 try { 188 // We need the app info to determine the uid and the uuid of the volume 189 // where the app is installed. 190 ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser( 191 packageName, 0, info.id); 192 requests.add( 193 new CacheQuotaHint.Builder() 194 .setVolumeUuid(appInfo.volumeUuid) 195 .setUid(appInfo.uid) 196 .setUsageStats(stat) 197 .setQuota(CacheQuotaHint.QUOTA_NOT_SET) 198 .build()); 199 } catch (PackageManager.NameNotFoundException e) { 200 // This may happen if an app has a recorded usage, but has been uninstalled. 201 continue; 202 } 203 } 204 } 205 return requests; 206 } 207 208 @Override onResult(Bundle data)209 public void onResult(Bundle data) { 210 final List<CacheQuotaHint> processedRequests = 211 data.getParcelableArrayList( 212 CacheQuotaService.REQUEST_LIST_KEY); 213 pushProcessedQuotas(processedRequests); 214 writeXmlToFile(processedRequests); 215 } 216 pushProcessedQuotas(List<CacheQuotaHint> processedRequests)217 private void pushProcessedQuotas(List<CacheQuotaHint> processedRequests) { 218 final int requestSize = processedRequests.size(); 219 for (int i = 0; i < requestSize; i++) { 220 CacheQuotaHint request = processedRequests.get(i); 221 long proposedQuota = request.getQuota(); 222 if (proposedQuota == CacheQuotaHint.QUOTA_NOT_SET) { 223 continue; 224 } 225 226 try { 227 int uid = request.getUid(); 228 mInstaller.setAppQuota(request.getVolumeUuid(), 229 UserHandle.getUserId(uid), 230 UserHandle.getAppId(uid), proposedQuota); 231 insertIntoQuotaMap(request.getVolumeUuid(), 232 UserHandle.getUserId(uid), 233 UserHandle.getAppId(uid), proposedQuota); 234 } catch (Installer.InstallerException ex) { 235 Slog.w(TAG, 236 "Failed to set cache quota for " + request.getUid(), 237 ex); 238 } 239 } 240 241 disconnectService(); 242 } 243 insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota)244 private void insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota) { 245 SparseLongArray volumeMap = mQuotaMap.get(volumeUuid); 246 if (volumeMap == null) { 247 volumeMap = new SparseLongArray(); 248 mQuotaMap.put(volumeUuid, volumeMap); 249 } 250 volumeMap.put(UserHandle.getUid(userId, appId), quota); 251 } 252 disconnectService()253 private void disconnectService() { 254 if (mServiceConnection != null) { 255 mContext.unbindService(mServiceConnection); 256 mServiceConnection = null; 257 } 258 } 259 getServiceComponentName()260 private ComponentName getServiceComponentName() { 261 String packageName = 262 mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); 263 if (packageName == null) { 264 Slog.w(TAG, "could not access the cache quota service: no package!"); 265 return null; 266 } 267 268 Intent intent = new Intent(CacheQuotaService.SERVICE_INTERFACE); 269 intent.setPackage(packageName); 270 ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 271 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 272 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 273 Slog.w(TAG, "No valid components found."); 274 return null; 275 } 276 ServiceInfo serviceInfo = resolveInfo.serviceInfo; 277 return new ComponentName(serviceInfo.packageName, serviceInfo.name); 278 } 279 writeXmlToFile(List<CacheQuotaHint> processedRequests)280 private void writeXmlToFile(List<CacheQuotaHint> processedRequests) { 281 FileOutputStream fileStream = null; 282 try { 283 XmlSerializer out = new FastXmlSerializer(); 284 fileStream = mPreviousValuesFile.startWrite(); 285 out.setOutput(fileStream, StandardCharsets.UTF_8.name()); 286 saveToXml(out, processedRequests, 0); 287 mPreviousValuesFile.finishWrite(fileStream); 288 } catch (Exception e) { 289 Slog.e(TAG, "An error occurred while writing the cache quota file.", e); 290 mPreviousValuesFile.failWrite(fileStream); 291 } 292 } 293 294 /** 295 * Initializes the quotas from the file. 296 * @return the number of bytes that were free on the device when the quotas were last calced. 297 */ setupQuotasFromFile()298 public long setupQuotasFromFile() throws IOException { 299 FileInputStream stream; 300 try { 301 stream = mPreviousValuesFile.openRead(); 302 } catch (FileNotFoundException e) { 303 // The file may not exist yet -- this isn't truly exceptional. 304 return -1; 305 } 306 307 Pair<Long, List<CacheQuotaHint>> cachedValues = null; 308 try { 309 cachedValues = readFromXml(stream); 310 } catch (XmlPullParserException e) { 311 throw new IllegalStateException(e.getMessage()); 312 } 313 314 if (cachedValues == null) { 315 Slog.e(TAG, "An error occurred while parsing the cache quota file."); 316 return -1; 317 } 318 pushProcessedQuotas(cachedValues.second); 319 return cachedValues.first; 320 } 321 322 @VisibleForTesting saveToXml(XmlSerializer out, List<CacheQuotaHint> requests, long bytesWhenCalculated)323 static void saveToXml(XmlSerializer out, 324 List<CacheQuotaHint> requests, long bytesWhenCalculated) throws IOException { 325 out.startDocument(null, true); 326 out.startTag(null, CACHE_INFO_TAG); 327 int requestSize = requests.size(); 328 out.attribute(null, ATTR_PREVIOUS_BYTES, Long.toString(bytesWhenCalculated)); 329 330 for (int i = 0; i < requestSize; i++) { 331 CacheQuotaHint request = requests.get(i); 332 out.startTag(null, TAG_QUOTA); 333 String uuid = request.getVolumeUuid(); 334 if (uuid != null) { 335 out.attribute(null, ATTR_UUID, request.getVolumeUuid()); 336 } 337 out.attribute(null, ATTR_UID, Integer.toString(request.getUid())); 338 out.attribute(null, ATTR_QUOTA_IN_BYTES, Long.toString(request.getQuota())); 339 out.endTag(null, TAG_QUOTA); 340 } 341 out.endTag(null, CACHE_INFO_TAG); 342 out.endDocument(); 343 } 344 readFromXml(InputStream inputStream)345 protected static Pair<Long, List<CacheQuotaHint>> readFromXml(InputStream inputStream) 346 throws XmlPullParserException, IOException { 347 XmlPullParser parser = Xml.newPullParser(); 348 parser.setInput(inputStream, StandardCharsets.UTF_8.name()); 349 350 int eventType = parser.getEventType(); 351 while (eventType != XmlPullParser.START_TAG && 352 eventType != XmlPullParser.END_DOCUMENT) { 353 eventType = parser.next(); 354 } 355 356 if (eventType == XmlPullParser.END_DOCUMENT) { 357 Slog.d(TAG, "No quotas found in quota file."); 358 return null; 359 } 360 361 String tagName = parser.getName(); 362 if (!CACHE_INFO_TAG.equals(tagName)) { 363 throw new IllegalStateException("Invalid starting tag."); 364 } 365 366 final List<CacheQuotaHint> quotas = new ArrayList<>(); 367 long previousBytes; 368 try { 369 previousBytes = Long.parseLong(parser.getAttributeValue( 370 null, ATTR_PREVIOUS_BYTES)); 371 } catch (NumberFormatException e) { 372 throw new IllegalStateException( 373 "Previous bytes formatted incorrectly; aborting quota read."); 374 } 375 376 eventType = parser.next(); 377 do { 378 if (eventType == XmlPullParser.START_TAG) { 379 tagName = parser.getName(); 380 if (TAG_QUOTA.equals(tagName)) { 381 CacheQuotaHint request = getRequestFromXml(parser); 382 if (request == null) { 383 continue; 384 } 385 quotas.add(request); 386 } 387 } 388 eventType = parser.next(); 389 } while (eventType != XmlPullParser.END_DOCUMENT); 390 return new Pair<>(previousBytes, quotas); 391 } 392 393 @VisibleForTesting getRequestFromXml(XmlPullParser parser)394 static CacheQuotaHint getRequestFromXml(XmlPullParser parser) { 395 try { 396 String uuid = parser.getAttributeValue(null, ATTR_UUID); 397 int uid = Integer.parseInt(parser.getAttributeValue(null, ATTR_UID)); 398 long bytes = Long.parseLong(parser.getAttributeValue(null, ATTR_QUOTA_IN_BYTES)); 399 return new CacheQuotaHint.Builder() 400 .setVolumeUuid(uuid).setUid(uid).setQuota(bytes).build(); 401 } catch (NumberFormatException e) { 402 Slog.e(TAG, "Invalid cache quota request, skipping."); 403 return null; 404 } 405 } 406 } 407