Home | History | Annotate | Download | only in usage
      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 
     82         public Lifecycle(Context context) {
     83             super(context);
     84         }
     85 
     86         @Override
     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 
    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 
    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 
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    531         private void recalculateQuotas(CacheQuotaStrategy strategy) {
    532             if (DEBUG) {
    533                 Slog.v(TAG, ">>> recalculating quotas ");
    534             }
    535 
    536             strategy.recalculateQuotas();
    537         }
    538 
    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
    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      */
    556     void notifySignificantDelta() {
    557         mContext.getContentResolver().notifyChange(
    558                 Uri.parse("content://com.android.externalstorage.documents/"), null, false);
    559     }
    560 }
    561