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