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.StatFs; 42 import android.os.UserHandle; 43 import android.os.UserManager; 44 import android.text.format.DateUtils; 45 import android.util.ArrayMap; 46 import android.util.AtomicFile; 47 import android.util.Pair; 48 import android.util.Slog; 49 import android.util.SparseLongArray; 50 import android.util.Xml; 51 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.modules.utils.TypedXmlPullParser; 54 import com.android.modules.utils.TypedXmlSerializer; 55 import com.android.server.pm.Installer; 56 57 import org.xmlpull.v1.XmlPullParser; 58 import org.xmlpull.v1.XmlPullParserException; 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.util.ArrayList; 67 import java.util.List; 68 import java.util.Objects; 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 = Objects.requireNonNull(context); 99 mUsageStats = Objects.requireNonNull(usageStatsManager); 100 mInstaller = Objects.requireNonNull(installer); 101 mQuotaMap = Objects.requireNonNull(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 PackageManager packageManager = mContext.getPackageManager(); 174 for (UserInfo info : users) { 175 List<UsageStats> stats = 176 mUsageStats.queryUsageStatsForUser(info.id, UsageStatsManager.INTERVAL_BEST, 177 oneYearAgo, timeNow, /*obfuscateInstantApps=*/ false); 178 if (stats == null) { 179 continue; 180 } 181 182 for (int i = 0; i < stats.size(); ++i) { 183 UsageStats stat = stats.get(i); 184 String packageName = stat.getPackageName(); 185 try { 186 // We need the app info to determine the uid and the uuid of the volume 187 // where the app is installed. 188 ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser( 189 packageName, 0, info.id); 190 requests.add( 191 new CacheQuotaHint.Builder() 192 .setVolumeUuid(appInfo.volumeUuid) 193 .setUid(appInfo.uid) 194 .setUsageStats(stat) 195 .setQuota(CacheQuotaHint.QUOTA_NOT_SET) 196 .build()); 197 } catch (PackageManager.NameNotFoundException e) { 198 // This may happen if an app has a recorded usage, but has been uninstalled. 199 continue; 200 } 201 } 202 } 203 return requests; 204 } 205 206 @Override onResult(Bundle data)207 public void onResult(Bundle data) { 208 final List<CacheQuotaHint> processedRequests = 209 data.getParcelableArrayList( 210 CacheQuotaService.REQUEST_LIST_KEY, android.app.usage.CacheQuotaHint.class); 211 pushProcessedQuotas(processedRequests); 212 writeXmlToFile(processedRequests); 213 } 214 pushProcessedQuotas(List<CacheQuotaHint> processedRequests)215 private void pushProcessedQuotas(List<CacheQuotaHint> processedRequests) { 216 for (CacheQuotaHint request : processedRequests) { 217 long proposedQuota = request.getQuota(); 218 if (proposedQuota == CacheQuotaHint.QUOTA_NOT_SET) { 219 continue; 220 } 221 222 try { 223 int uid = request.getUid(); 224 mInstaller.setAppQuota(request.getVolumeUuid(), 225 UserHandle.getUserId(uid), 226 UserHandle.getAppId(uid), proposedQuota); 227 insertIntoQuotaMap(request.getVolumeUuid(), 228 UserHandle.getUserId(uid), 229 UserHandle.getAppId(uid), proposedQuota); 230 } catch (Installer.InstallerException ex) { 231 Slog.w(TAG, 232 "Failed to set cache quota for " + request.getUid(), 233 ex); 234 } 235 } 236 237 disconnectService(); 238 } 239 insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota)240 private void insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota) { 241 SparseLongArray volumeMap = mQuotaMap.get(volumeUuid); 242 if (volumeMap == null) { 243 volumeMap = new SparseLongArray(); 244 mQuotaMap.put(volumeUuid, volumeMap); 245 } 246 volumeMap.put(UserHandle.getUid(userId, appId), quota); 247 } 248 disconnectService()249 private void disconnectService() { 250 if (mServiceConnection != null) { 251 mContext.unbindService(mServiceConnection); 252 mServiceConnection = null; 253 } 254 } 255 getServiceComponentName()256 private ComponentName getServiceComponentName() { 257 String packageName = 258 mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); 259 if (packageName == null) { 260 Slog.w(TAG, "could not access the cache quota service: no package!"); 261 return null; 262 } 263 264 Intent intent = new Intent(CacheQuotaService.SERVICE_INTERFACE); 265 intent.setPackage(packageName); 266 ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 267 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); 268 if (resolveInfo == null || resolveInfo.serviceInfo == null) { 269 Slog.w(TAG, "No valid components found."); 270 return null; 271 } 272 ServiceInfo serviceInfo = resolveInfo.serviceInfo; 273 return new ComponentName(serviceInfo.packageName, serviceInfo.name); 274 } 275 writeXmlToFile(List<CacheQuotaHint> processedRequests)276 private void writeXmlToFile(List<CacheQuotaHint> processedRequests) { 277 FileOutputStream fileStream = null; 278 try { 279 fileStream = mPreviousValuesFile.startWrite(); 280 TypedXmlSerializer out = Xml.resolveSerializer(fileStream); 281 final StatFs stats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); 282 saveToXml(out, processedRequests, stats.getAvailableBytes()); 283 mPreviousValuesFile.finishWrite(fileStream); 284 } catch (Exception e) { 285 Slog.e(TAG, "An error occurred while writing the cache quota file.", e); 286 mPreviousValuesFile.failWrite(fileStream); 287 } 288 } 289 290 /** 291 * Initializes the quotas from the file. 292 * @return the number of bytes that were free on the device when the quotas were last calced. 293 */ setupQuotasFromFile()294 public long setupQuotasFromFile() throws IOException { 295 Pair<Long, List<CacheQuotaHint>> cachedValues = null; 296 try (FileInputStream stream = mPreviousValuesFile.openRead()) { 297 try { 298 cachedValues = readFromXml(stream); 299 } catch (XmlPullParserException e) { 300 throw new IllegalStateException(e.getMessage()); 301 } 302 } catch (FileNotFoundException e) { 303 // The file may not exist yet -- this isn't truly exceptional. 304 return -1; 305 } 306 307 if (cachedValues == null) { 308 Slog.e(TAG, "An error occurred while parsing the cache quota file."); 309 return -1; 310 } 311 pushProcessedQuotas(cachedValues.second); 312 313 return cachedValues.first; 314 } 315 316 @VisibleForTesting saveToXml(TypedXmlSerializer out, List<CacheQuotaHint> requests, long bytesWhenCalculated)317 static void saveToXml(TypedXmlSerializer out, 318 List<CacheQuotaHint> requests, long bytesWhenCalculated) throws IOException { 319 out.startDocument(null, true); 320 out.startTag(null, CACHE_INFO_TAG); 321 out.attributeLong(null, ATTR_PREVIOUS_BYTES, bytesWhenCalculated); 322 323 for (CacheQuotaHint request : requests) { 324 out.startTag(null, TAG_QUOTA); 325 String uuid = request.getVolumeUuid(); 326 if (uuid != null) { 327 out.attribute(null, ATTR_UUID, request.getVolumeUuid()); 328 } 329 out.attributeInt(null, ATTR_UID, request.getUid()); 330 out.attributeLong(null, ATTR_QUOTA_IN_BYTES, request.getQuota()); 331 out.endTag(null, TAG_QUOTA); 332 } 333 out.endTag(null, CACHE_INFO_TAG); 334 out.endDocument(); 335 } 336 readFromXml(InputStream inputStream)337 protected static Pair<Long, List<CacheQuotaHint>> readFromXml(InputStream inputStream) 338 throws XmlPullParserException, IOException { 339 TypedXmlPullParser parser = Xml.resolvePullParser(inputStream); 340 341 int eventType = parser.getEventType(); 342 while (eventType != XmlPullParser.START_TAG && 343 eventType != XmlPullParser.END_DOCUMENT) { 344 eventType = parser.next(); 345 } 346 347 if (eventType == XmlPullParser.END_DOCUMENT) { 348 Slog.d(TAG, "No quotas found in quota file."); 349 return null; 350 } 351 352 String tagName = parser.getName(); 353 if (!CACHE_INFO_TAG.equals(tagName)) { 354 throw new IllegalStateException("Invalid starting tag."); 355 } 356 357 final List<CacheQuotaHint> quotas = new ArrayList<>(); 358 long previousBytes; 359 try { 360 previousBytes = parser.getAttributeLong(null, ATTR_PREVIOUS_BYTES); 361 } catch (NumberFormatException e) { 362 throw new IllegalStateException( 363 "Previous bytes formatted incorrectly; aborting quota read."); 364 } 365 366 eventType = parser.next(); 367 do { 368 if (eventType == XmlPullParser.START_TAG) { 369 tagName = parser.getName(); 370 if (TAG_QUOTA.equals(tagName)) { 371 CacheQuotaHint request = getRequestFromXml(parser); 372 if (request == null) { 373 continue; 374 } 375 quotas.add(request); 376 } 377 } 378 eventType = parser.next(); 379 } while (eventType != XmlPullParser.END_DOCUMENT); 380 return new Pair<>(previousBytes, quotas); 381 } 382 383 @VisibleForTesting getRequestFromXml(TypedXmlPullParser parser)384 static CacheQuotaHint getRequestFromXml(TypedXmlPullParser parser) { 385 try { 386 String uuid = parser.getAttributeValue(null, ATTR_UUID); 387 int uid = parser.getAttributeInt(null, ATTR_UID); 388 long bytes = parser.getAttributeLong(null, ATTR_QUOTA_IN_BYTES); 389 return new CacheQuotaHint.Builder() 390 .setVolumeUuid(uuid).setUid(uid).setQuota(bytes).build(); 391 } catch (XmlPullParserException e) { 392 Slog.e(TAG, "Invalid cache quota request, skipping."); 393 return null; 394 } 395 } 396 } 397