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