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