StorageStatsService.java revision fd65813157e4dd7fa9f0b7c5dd4c8f536cc6316a
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 switch (vol.type) { 116 case VolumeInfo.TYPE_PRIVATE: 117 case VolumeInfo.TYPE_EMULATED: 118 if (newState == VolumeInfo.STATE_MOUNTED) { 119 invalidateMounts(); 120 } 121 } 122 } 123 }); 124 } 125 126 private void invalidateMounts() { 127 try { 128 mInstaller.invalidateMounts(); 129 } catch (InstallerException e) { 130 Slog.wtf(TAG, "Failed to invalidate mounts", e); 131 } 132 } 133 134 private void enforcePermission(int callingUid, String callingPackage) { 135 final int mode = mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, 136 callingUid, callingPackage); 137 switch (mode) { 138 case AppOpsManager.MODE_ALLOWED: 139 return; 140 case AppOpsManager.MODE_DEFAULT: 141 mContext.enforceCallingOrSelfPermission( 142 android.Manifest.permission.PACKAGE_USAGE_STATS, TAG); 143 return; 144 default: 145 throw new SecurityException("Package " + callingPackage + " from UID " + callingUid 146 + " blocked by mode " + mode); 147 } 148 } 149 150 @Override 151 public boolean isQuotaSupported(String volumeUuid, String callingPackage) { 152 enforcePermission(Binder.getCallingUid(), callingPackage); 153 154 try { 155 return mInstaller.isQuotaSupported(volumeUuid); 156 } catch (InstallerException e) { 157 throw new ParcelableException(new IOException(e.getMessage())); 158 } 159 } 160 161 @Override 162 public long getTotalBytes(String volumeUuid, String callingPackage) { 163 // NOTE: No permissions required 164 165 if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { 166 return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize()); 167 } else { 168 final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); 169 if (vol == null) { 170 throw new ParcelableException( 171 new IOException("Failed to find storage device for UUID " + volumeUuid)); 172 } 173 return FileUtils.roundStorageSize(vol.disk.size); 174 } 175 } 176 177 @Override 178 public long getFreeBytes(String volumeUuid, String callingPackage) { 179 // NOTE: No permissions required 180 181 long cacheBytes = 0; 182 final long token = Binder.clearCallingIdentity(); 183 try { 184 if (isQuotaSupported(volumeUuid, callingPackage)) { 185 for (UserInfo user : mUser.getUsers()) { 186 final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null); 187 cacheBytes += stats.cacheBytes; 188 } 189 } 190 } finally { 191 Binder.restoreCallingIdentity(token); 192 } 193 194 if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { 195 return Environment.getDataDirectory().getFreeSpace() + cacheBytes; 196 } else { 197 final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); 198 if (vol == null) { 199 throw new ParcelableException( 200 new IOException("Failed to find storage device for UUID " + volumeUuid)); 201 } 202 return vol.getPath().getFreeSpace() + cacheBytes; 203 } 204 } 205 206 @Override 207 public long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage) { 208 enforcePermission(Binder.getCallingUid(), callingPackage); 209 210 if (mCacheQuotas.containsKey(volumeUuid)) { 211 final SparseLongArray uidMap = mCacheQuotas.get(volumeUuid); 212 return uidMap.get(uid, DEFAULT_QUOTA); 213 } 214 215 return DEFAULT_QUOTA; 216 } 217 218 @Override 219 public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, 220 String callingPackage) { 221 if (userId != UserHandle.getCallingUserId()) { 222 mContext.enforceCallingOrSelfPermission( 223 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 224 } 225 226 final ApplicationInfo appInfo; 227 try { 228 appInfo = mPackage.getApplicationInfoAsUser(packageName, 229 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 230 } catch (NameNotFoundException e) { 231 throw new ParcelableException(e); 232 } 233 234 if (Binder.getCallingUid() == appInfo.uid) { 235 // No permissions required when asking about themselves 236 } else { 237 enforcePermission(Binder.getCallingUid(), callingPackage); 238 } 239 240 if (mPackage.getPackagesForUid(appInfo.uid).length == 1) { 241 // Only one package inside UID means we can fast-path 242 return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage); 243 } else { 244 // Multiple packages means we need to go manual 245 final int appId = UserHandle.getUserId(appInfo.uid); 246 final String[] packageNames = new String[] { packageName }; 247 final long[] ceDataInodes = new long[1]; 248 String[] codePaths = new String[0]; 249 250 if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { 251 // We don't count code baked into system image 252 } else { 253 codePaths = ArrayUtils.appendElement(String.class, codePaths, 254 appInfo.getCodePath()); 255 } 256 257 final PackageStats stats = new PackageStats(TAG); 258 try { 259 mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, 260 appId, ceDataInodes, codePaths, stats); 261 } catch (InstallerException e) { 262 throw new ParcelableException(new IOException(e.getMessage())); 263 } 264 return translate(stats); 265 } 266 } 267 268 @Override 269 public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) { 270 final int userId = UserHandle.getUserId(uid); 271 final int appId = UserHandle.getAppId(uid); 272 273 if (userId != UserHandle.getCallingUserId()) { 274 mContext.enforceCallingOrSelfPermission( 275 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 276 } 277 278 if (Binder.getCallingUid() == uid) { 279 // No permissions required when asking about themselves 280 } else { 281 enforcePermission(Binder.getCallingUid(), callingPackage); 282 } 283 284 final String[] packageNames = mPackage.getPackagesForUid(uid); 285 final long[] ceDataInodes = new long[packageNames.length]; 286 String[] codePaths = new String[0]; 287 288 for (int i = 0; i < packageNames.length; i++) { 289 try { 290 final ApplicationInfo appInfo = mPackage.getApplicationInfoAsUser(packageNames[i], 291 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 292 if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { 293 // We don't count code baked into system image 294 } else { 295 codePaths = ArrayUtils.appendElement(String.class, codePaths, 296 appInfo.getCodePath()); 297 } 298 } catch (NameNotFoundException e) { 299 throw new ParcelableException(e); 300 } 301 } 302 303 final PackageStats stats = new PackageStats(TAG); 304 try { 305 mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(), 306 appId, ceDataInodes, codePaths, stats); 307 308 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 309 final PackageStats manualStats = new PackageStats(TAG); 310 mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, 311 appId, ceDataInodes, codePaths, manualStats); 312 checkEquals("UID " + uid, manualStats, stats); 313 } 314 } catch (InstallerException e) { 315 throw new ParcelableException(new IOException(e.getMessage())); 316 } 317 return translate(stats); 318 } 319 320 @Override 321 public StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage) { 322 if (userId != UserHandle.getCallingUserId()) { 323 mContext.enforceCallingOrSelfPermission( 324 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 325 } 326 327 // Always require permission to see user-level stats 328 enforcePermission(Binder.getCallingUid(), callingPackage); 329 330 final int[] appIds = getAppIds(userId); 331 final PackageStats stats = new PackageStats(TAG); 332 try { 333 mInstaller.getUserSize(volumeUuid, userId, getDefaultFlags(), appIds, stats); 334 335 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 336 final PackageStats manualStats = new PackageStats(TAG); 337 mInstaller.getUserSize(volumeUuid, userId, 0, appIds, manualStats); 338 checkEquals("User " + userId, manualStats, stats); 339 } 340 } catch (InstallerException e) { 341 throw new ParcelableException(new IOException(e.getMessage())); 342 } 343 return translate(stats); 344 } 345 346 @Override 347 public ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId, 348 String callingPackage) { 349 if (userId != UserHandle.getCallingUserId()) { 350 mContext.enforceCallingOrSelfPermission( 351 android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); 352 } 353 354 // Always require permission to see user-level stats 355 enforcePermission(Binder.getCallingUid(), callingPackage); 356 357 final int[] appIds = getAppIds(userId); 358 final long[] stats; 359 try { 360 stats = mInstaller.getExternalSize(volumeUuid, userId, getDefaultFlags(), appIds); 361 362 if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { 363 final long[] manualStats = mInstaller.getExternalSize(volumeUuid, userId, 0, 364 appIds); 365 checkEquals("External " + userId, manualStats, stats); 366 } 367 } catch (InstallerException e) { 368 throw new ParcelableException(new IOException(e.getMessage())); 369 } 370 371 final ExternalStorageStats res = new ExternalStorageStats(); 372 res.totalBytes = stats[0]; 373 res.audioBytes = stats[1]; 374 res.videoBytes = stats[2]; 375 res.imageBytes = stats[3]; 376 res.appBytes = stats[4]; 377 return res; 378 } 379 380 private int[] getAppIds(int userId) { 381 int[] appIds = null; 382 for (ApplicationInfo app : mPackage.getInstalledApplicationsAsUser( 383 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)) { 384 final int appId = UserHandle.getAppId(app.uid); 385 if (!ArrayUtils.contains(appIds, appId)) { 386 appIds = ArrayUtils.appendInt(appIds, appId); 387 } 388 } 389 return appIds; 390 } 391 392 private static int getDefaultFlags() { 393 if (SystemProperties.getBoolean(PROP_DISABLE_QUOTA, false)) { 394 return 0; 395 } else { 396 return Installer.FLAG_USE_QUOTA; 397 } 398 } 399 400 private static void checkEquals(String msg, long[] a, long[] b) { 401 for (int i = 0; i < a.length; i++) { 402 checkEquals(msg + "[" + i + "]", a[i], b[i]); 403 } 404 } 405 406 private static void checkEquals(String msg, PackageStats a, PackageStats b) { 407 checkEquals(msg + " codeSize", a.codeSize, b.codeSize); 408 checkEquals(msg + " dataSize", a.dataSize, b.dataSize); 409 checkEquals(msg + " cacheSize", a.cacheSize, b.cacheSize); 410 checkEquals(msg + " externalCodeSize", a.externalCodeSize, b.externalCodeSize); 411 checkEquals(msg + " externalDataSize", a.externalDataSize, b.externalDataSize); 412 checkEquals(msg + " externalCacheSize", a.externalCacheSize, b.externalCacheSize); 413 } 414 415 private static void checkEquals(String msg, long expected, long actual) { 416 if (expected != actual) { 417 Slog.e(TAG, msg + " expected " + expected + " actual " + actual); 418 } 419 } 420 421 private static StorageStats translate(PackageStats stats) { 422 final StorageStats res = new StorageStats(); 423 res.codeBytes = stats.codeSize + stats.externalCodeSize; 424 res.dataBytes = stats.dataSize + stats.externalDataSize; 425 res.cacheBytes = stats.cacheSize + stats.externalCacheSize; 426 return res; 427 } 428 429 private class H extends Handler { 430 private static final int MSG_CHECK_STORAGE_DELTA = 100; 431 private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101; 432 /** 433 * By only triggering a re-calculation after the storage has changed sizes, we can avoid 434 * recalculating quotas too often. Minimum change delta defines the percentage of change 435 * we need to see before we recalculate. 436 */ 437 private static final double MINIMUM_CHANGE_DELTA = 0.05; 438 private static final int UNSET = -1; 439 private static final boolean DEBUG = false; 440 441 private final StatFs mStats; 442 private long mPreviousBytes; 443 private double mMinimumThresholdBytes; 444 445 public H(Looper looper) { 446 super(looper); 447 // TODO: Handle all private volumes. 448 mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); 449 mPreviousBytes = mStats.getAvailableBytes(); 450 mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA; 451 } 452 453 public void handleMessage(Message msg) { 454 if (DEBUG) { 455 Slog.v(TAG, ">>> handling " + msg.what); 456 } 457 458 if (!isCacheQuotaCalculationsEnabled(mContext.getContentResolver())) { 459 return; 460 } 461 462 switch (msg.what) { 463 case MSG_CHECK_STORAGE_DELTA: { 464 long bytesDelta = Math.abs(mPreviousBytes - mStats.getAvailableBytes()); 465 if (bytesDelta > mMinimumThresholdBytes) { 466 mPreviousBytes = mStats.getAvailableBytes(); 467 recalculateQuotas(getInitializedStrategy()); 468 } 469 sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); 470 break; 471 } 472 case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: { 473 CacheQuotaStrategy strategy = getInitializedStrategy(); 474 mPreviousBytes = UNSET; 475 try { 476 mPreviousBytes = strategy.setupQuotasFromFile(); 477 } catch (IOException e) { 478 Slog.e(TAG, "An error occurred while reading the cache quota file.", e); 479 } catch (IllegalStateException e) { 480 Slog.e(TAG, "Cache quota XML file is malformed?", e); 481 } 482 483 // If errors occurred getting the quotas from disk, let's re-calc them. 484 if (mPreviousBytes < 0) { 485 mPreviousBytes = mStats.getAvailableBytes(); 486 recalculateQuotas(strategy); 487 } 488 sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); 489 break; 490 } 491 default: 492 if (DEBUG) { 493 Slog.v(TAG, ">>> default message case "); 494 } 495 return; 496 } 497 } 498 499 private void recalculateQuotas(CacheQuotaStrategy strategy) { 500 if (DEBUG) { 501 Slog.v(TAG, ">>> recalculating quotas "); 502 } 503 504 strategy.recalculateQuotas(); 505 } 506 507 private CacheQuotaStrategy getInitializedStrategy() { 508 UsageStatsManagerInternal usageStatsManager = 509 LocalServices.getService(UsageStatsManagerInternal.class); 510 return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller, mCacheQuotas); 511 } 512 } 513 514 @VisibleForTesting 515 static boolean isCacheQuotaCalculationsEnabled(ContentResolver resolver) { 516 return Settings.Global.getInt( 517 resolver, Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION, 1) != 0; 518 } 519} 520