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