StorageStatsService.java revision 789a8fc792725e4988ff43d554b3c8c037c41921
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 17package com.android.server.usage; 18 19import android.app.AppOpsManager; 20import android.app.usage.ExternalStorageStats; 21import android.app.usage.IStorageStatsManager; 22import android.app.usage.StorageStats; 23import android.app.usage.UsageStatsManagerInternal; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.pm.ApplicationInfo; 27import android.content.pm.PackageManager; 28import android.content.pm.PackageManager.NameNotFoundException; 29import android.content.pm.PackageStats; 30import android.content.pm.UserInfo; 31import android.net.TrafficStats; 32import android.os.Binder; 33import android.os.Environment; 34import android.os.FileUtils; 35import android.os.Handler; 36import android.os.Looper; 37import android.os.Message; 38import android.os.ParcelableException; 39import android.os.StatFs; 40import android.os.SystemProperties; 41import android.os.UserHandle; 42import android.os.UserManager; 43import android.os.storage.StorageEventListener; 44import android.os.storage.StorageManager; 45import android.os.storage.VolumeInfo; 46import android.provider.Settings; 47import android.text.format.DateUtils; 48import android.util.ArrayMap; 49import android.util.Slog; 50import android.util.SparseLongArray; 51 52import com.android.internal.annotations.VisibleForTesting; 53import com.android.internal.util.ArrayUtils; 54import com.android.internal.util.Preconditions; 55import com.android.server.IoThread; 56import com.android.server.LocalServices; 57import com.android.server.SystemService; 58import com.android.server.pm.Installer; 59import com.android.server.pm.Installer.InstallerException; 60import com.android.server.storage.CacheQuotaStrategy; 61 62import java.io.IOException; 63 64public class StorageStatsService extends IStorageStatsManager.Stub { 65 private static final String TAG = "StorageStatsService"; 66 67 private static final String PROP_DISABLE_QUOTA = "fw.disable_quota"; 68 private static final String PROP_VERIFY_STORAGE = "fw.verify_storage"; 69 70 private static final long DELAY_IN_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS; 71 private static final long DEFAULT_QUOTA = 64 * TrafficStats.MB_IN_BYTES; 72 73 public static class Lifecycle extends SystemService { 74 private StorageStatsService mService; 75 76 public Lifecycle(Context context) { 77 super(context); 78 } 79 80 @Override 81 public void onStart() { 82 mService = new StorageStatsService(getContext()); 83 publishBinderService(Context.STORAGE_STATS_SERVICE, mService); 84 } 85 } 86 87 private final Context mContext; 88 private final AppOpsManager mAppOps; 89 private final UserManager mUser; 90 private final PackageManager mPackage; 91 private final StorageManager mStorage; 92 private final ArrayMap<String, SparseLongArray> mCacheQuotas; 93 94 private final Installer mInstaller; 95 private final H mHandler; 96 97 public StorageStatsService(Context context) { 98 mContext = Preconditions.checkNotNull(context); 99 mAppOps = Preconditions.checkNotNull(context.getSystemService(AppOpsManager.class)); 100 mUser = Preconditions.checkNotNull(context.getSystemService(UserManager.class)); 101 mPackage = Preconditions.checkNotNull(context.getPackageManager()); 102 mStorage = Preconditions.checkNotNull(context.getSystemService(StorageManager.class)); 103 mCacheQuotas = new ArrayMap<>(); 104 105 mInstaller = new Installer(context); 106 mInstaller.onStart(); 107 invalidateMounts(); 108 109 mHandler = new H(IoThread.get().getLooper()); 110 mHandler.sendEmptyMessage(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE); 111 112 mStorage.registerListener(new StorageEventListener() { 113 @Override 114 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 115 if ((vol.type == VolumeInfo.TYPE_PRIVATE) 116 && (newState == VolumeInfo.STATE_MOUNTED)) { 117 invalidateMounts(); 118 } 119 } 120 }); 121 } 122 123 private void invalidateMounts() { 124 try { 125 mInstaller.invalidateMounts(); 126 } catch (InstallerException e) { 127 Slog.wtf(TAG, "Failed to invalidate mounts", e); 128 } 129 } 130 131 private void enforcePermission(int callingUid, String callingPackage) { 132 final int mode = mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, 133 callingUid, callingPackage); 134 switch (mode) { 135 case AppOpsManager.MODE_ALLOWED: 136 return; 137 case AppOpsManager.MODE_DEFAULT: 138 mContext.enforceCallingOrSelfPermission( 139 android.Manifest.permission.PACKAGE_USAGE_STATS, TAG); 140 return; 141 default: 142 throw new SecurityException("Package " + callingPackage + " from UID " + callingUid 143 + " blocked by mode " + mode); 144 } 145 } 146 147 @Override 148 public boolean isQuotaSupported(String volumeUuid, String callingPackage) { 149 enforcePermission(Binder.getCallingUid(), callingPackage); 150 151 try { 152 return mInstaller.isQuotaSupported(volumeUuid); 153 } catch (InstallerException e) { 154 throw new ParcelableException(new IOException(e.getMessage())); 155 } 156 } 157 158 @Override 159 public long getTotalBytes(String volumeUuid, String callingPackage) { 160 enforcePermission(Binder.getCallingUid(), callingPackage); 161 162 if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { 163 return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize()); 164 } else { 165 final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); 166 if (vol == null) { 167 throw new ParcelableException( 168 new IOException("Failed to find storage device for UUID " + volumeUuid)); 169 } 170 return FileUtils.roundStorageSize(vol.disk.size); 171 } 172 } 173 174 @Override 175 public long getFreeBytes(String volumeUuid, String callingPackage) { 176 enforcePermission(Binder.getCallingUid(), callingPackage); 177 178 long cacheBytes = 0; 179 final long token = Binder.clearCallingIdentity(); 180 try { 181 for (UserInfo user : mUser.getUsers()) { 182 final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null); 183 cacheBytes += stats.cacheBytes; 184 } 185 } finally { 186 Binder.restoreCallingIdentity(token); 187 } 188 189 if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { 190 return Environment.getDataDirectory().getUsableSpace() + cacheBytes; 191 } else { 192 final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); 193 if (vol == null) { 194 throw new ParcelableException( 195 new IOException("Failed to find storage device for UUID " + volumeUuid)); 196 } 197 return vol.getPath().getUsableSpace() + cacheBytes; 198 } 199 } 200 201 @Override 202 public long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage) { 203 enforcePermission(Binder.getCallingUid(), callingPackage); 204 205 if (mCacheQuotas.containsKey(volumeUuid)) { 206 final SparseLongArray uidMap = mCacheQuotas.get(volumeUuid); 207 return uidMap.get(uid, DEFAULT_QUOTA); 208 } 209 210 return DEFAULT_QUOTA; 211 } 212 213 @Override 214 public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, 215 String callingPackage) { 216 enforcePermission(Binder.getCallingUid(), callingPackage); 217 if (userId != UserHandle.getCallingUserId()) { 218 mContext.enforceCallingOrSelfPermission( 219 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 220 } 221 222 final ApplicationInfo appInfo; 223 try { 224 appInfo = mPackage.getApplicationInfoAsUser(packageName, 225 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 226 } catch (NameNotFoundException e) { 227 throw new ParcelableException(e); 228 } 229 230 if (mPackage.getPackagesForUid(appInfo.uid).length == 1) { 231 // Only one package inside UID means we can fast-path 232 return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage); 233 } else { 234 // Multiple packages means we need to go manual 235 final int appId = UserHandle.getUserId(appInfo.uid); 236 final String[] packageNames = new String[] { packageName }; 237 final long[] ceDataInodes = new long[1]; 238 final String[] codePaths = new String[] { appInfo.getCodePath() }; 239 240 final PackageStats stats = new PackageStats(TAG); 241 try { 242 mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, 243 appId, ceDataInodes, codePaths, stats); 244 } catch (InstallerException e) { 245 throw new ParcelableException(new IOException(e.getMessage())); 246 } 247 return translate(stats); 248 } 249 } 250 251 @Override 252 public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) { 253 enforcePermission(Binder.getCallingUid(), callingPackage); 254 if (UserHandle.getUserId(uid) != UserHandle.getCallingUserId()) { 255 mContext.enforceCallingOrSelfPermission( 256 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 257 } 258 259 final int userId = UserHandle.getUserId(uid); 260 final int appId = UserHandle.getAppId(uid); 261 262 final String[] packageNames = mPackage.getPackagesForUid(uid); 263 final long[] ceDataInodes = new long[packageNames.length]; 264 final String[] codePaths = new String[packageNames.length]; 265 266 for (int i = 0; i < packageNames.length; i++) { 267 try { 268 codePaths[i] = mPackage.getApplicationInfoAsUser(packageNames[i], 269 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId).getCodePath(); 270 } catch (NameNotFoundException e) { 271 throw new ParcelableException(e); 272 } 273 } 274 275 final PackageStats stats = new PackageStats(TAG); 276 try { 277 mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(), 278 appId, ceDataInodes, codePaths, stats); 279 280 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 281 final PackageStats manualStats = new PackageStats(TAG); 282 mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, 283 appId, ceDataInodes, codePaths, manualStats); 284 checkEquals("UID " + uid, manualStats, stats); 285 } 286 } catch (InstallerException e) { 287 throw new ParcelableException(new IOException(e.getMessage())); 288 } 289 return translate(stats); 290 } 291 292 @Override 293 public StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage) { 294 enforcePermission(Binder.getCallingUid(), callingPackage); 295 if (userId != UserHandle.getCallingUserId()) { 296 mContext.enforceCallingOrSelfPermission( 297 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 298 } 299 300 int[] appIds = null; 301 for (ApplicationInfo app : mPackage.getInstalledApplicationsAsUser( 302 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)) { 303 final int appId = UserHandle.getAppId(app.uid); 304 if (!ArrayUtils.contains(appIds, appId)) { 305 appIds = ArrayUtils.appendInt(appIds, appId); 306 } 307 } 308 309 final PackageStats stats = new PackageStats(TAG); 310 try { 311 mInstaller.getUserSize(volumeUuid, userId, getDefaultFlags(), appIds, stats); 312 313 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 314 final PackageStats manualStats = new PackageStats(TAG); 315 mInstaller.getUserSize(volumeUuid, userId, 0, appIds, manualStats); 316 checkEquals("User " + userId, manualStats, stats); 317 } 318 } catch (InstallerException e) { 319 throw new ParcelableException(new IOException(e.getMessage())); 320 } 321 return translate(stats); 322 } 323 324 @Override 325 public ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId, 326 String callingPackage) { 327 enforcePermission(Binder.getCallingUid(), callingPackage); 328 if (userId != UserHandle.getCallingUserId()) { 329 mContext.enforceCallingOrSelfPermission( 330 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 331 } 332 333 final long[] stats; 334 try { 335 stats = mInstaller.getExternalSize(volumeUuid, userId, getDefaultFlags()); 336 337 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 338 final long[] manualStats = mInstaller.getExternalSize(volumeUuid, userId, 0); 339 checkEquals("External " + userId, manualStats, stats); 340 } 341 } catch (InstallerException e) { 342 throw new ParcelableException(new IOException(e.getMessage())); 343 } 344 345 final ExternalStorageStats res = new ExternalStorageStats(); 346 res.totalBytes = stats[0]; 347 res.audioBytes = stats[1]; 348 res.videoBytes = stats[2]; 349 res.imageBytes = stats[3]; 350 return res; 351 } 352 353 private static int getDefaultFlags() { 354 if (SystemProperties.getBoolean(PROP_DISABLE_QUOTA, false)) { 355 return 0; 356 } else { 357 return Installer.FLAG_USE_QUOTA; 358 } 359 } 360 361 private static void checkEquals(String msg, long[] a, long[] b) { 362 for (int i = 0; i < a.length; i++) { 363 checkEquals(msg + "[" + i + "]", a[i], b[i]); 364 } 365 } 366 367 private static void checkEquals(String msg, PackageStats a, PackageStats b) { 368 checkEquals(msg + " codeSize", a.codeSize, b.codeSize); 369 checkEquals(msg + " dataSize", a.dataSize, b.dataSize); 370 checkEquals(msg + " cacheSize", a.cacheSize, b.cacheSize); 371 checkEquals(msg + " externalCodeSize", a.externalCodeSize, b.externalCodeSize); 372 checkEquals(msg + " externalDataSize", a.externalDataSize, b.externalDataSize); 373 checkEquals(msg + " externalCacheSize", a.externalCacheSize, b.externalCacheSize); 374 } 375 376 private static void checkEquals(String msg, long expected, long actual) { 377 if (expected != actual) { 378 Slog.e(TAG, msg + " expected " + expected + " actual " + actual); 379 } 380 } 381 382 private static StorageStats translate(PackageStats stats) { 383 final StorageStats res = new StorageStats(); 384 res.codeBytes = stats.codeSize + stats.externalCodeSize; 385 res.dataBytes = stats.dataSize + stats.externalDataSize; 386 res.cacheBytes = stats.cacheSize + stats.externalCacheSize; 387 return res; 388 } 389 390 private class H extends Handler { 391 private static final int MSG_CHECK_STORAGE_DELTA = 100; 392 private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101; 393 /** 394 * By only triggering a re-calculation after the storage has changed sizes, we can avoid 395 * recalculating quotas too often. Minimum change delta defines the percentage of change 396 * we need to see before we recalculate. 397 */ 398 private static final double MINIMUM_CHANGE_DELTA = 0.05; 399 private static final int UNSET = -1; 400 private static final boolean DEBUG = false; 401 402 private final StatFs mStats; 403 private long mPreviousBytes; 404 private double mMinimumThresholdBytes; 405 406 public H(Looper looper) { 407 super(looper); 408 // TODO: Handle all private volumes. 409 mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); 410 mPreviousBytes = mStats.getAvailableBytes(); 411 mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA; 412 } 413 414 public void handleMessage(Message msg) { 415 if (DEBUG) { 416 Slog.v(TAG, ">>> handling " + msg.what); 417 } 418 419 if (!isCacheQuotaCalculationsEnabled(mContext.getContentResolver())) { 420 return; 421 } 422 423 switch (msg.what) { 424 case MSG_CHECK_STORAGE_DELTA: { 425 long bytesDelta = Math.abs(mPreviousBytes - mStats.getAvailableBytes()); 426 if (bytesDelta > mMinimumThresholdBytes) { 427 mPreviousBytes = mStats.getAvailableBytes(); 428 recalculateQuotas(getInitializedStrategy()); 429 } 430 sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); 431 break; 432 } 433 case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: { 434 CacheQuotaStrategy strategy = getInitializedStrategy(); 435 mPreviousBytes = UNSET; 436 try { 437 mPreviousBytes = strategy.setupQuotasFromFile(); 438 } catch (IOException e) { 439 Slog.e(TAG, "An error occurred while reading the cache quota file.", e); 440 } catch (IllegalStateException e) { 441 Slog.e(TAG, "Cache quota XML file is malformed?", e); 442 } 443 444 // If errors occurred getting the quotas from disk, let's re-calc them. 445 if (mPreviousBytes < 0) { 446 mPreviousBytes = mStats.getAvailableBytes(); 447 recalculateQuotas(strategy); 448 } 449 sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); 450 break; 451 } 452 default: 453 if (DEBUG) { 454 Slog.v(TAG, ">>> default message case "); 455 } 456 return; 457 } 458 } 459 460 private void recalculateQuotas(CacheQuotaStrategy strategy) { 461 if (DEBUG) { 462 Slog.v(TAG, ">>> recalculating quotas "); 463 } 464 465 strategy.recalculateQuotas(); 466 } 467 468 private CacheQuotaStrategy getInitializedStrategy() { 469 UsageStatsManagerInternal usageStatsManager = 470 LocalServices.getService(UsageStatsManagerInternal.class); 471 return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller, mCacheQuotas); 472 } 473 } 474 475 @VisibleForTesting 476 static boolean isCacheQuotaCalculationsEnabled(ContentResolver resolver) { 477 return Settings.Global.getInt( 478 resolver, Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION, 1) != 0; 479 } 480} 481