/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.usage; import static com.android.internal.util.ArrayUtils.defeatNullable; import android.app.AppOpsManager; import android.app.usage.ExternalStorageStats; import android.app.usage.IStorageStatsManager; import android.app.usage.StorageStats; import android.app.usage.UsageStatsManagerInternal; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageStats; import android.content.pm.UserInfo; import android.net.TrafficStats; import android.net.Uri; import android.os.Binder; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelableException; import android.os.StatFs; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.Settings; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseLongArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.Installer; import com.android.server.pm.Installer.InstallerException; import com.android.server.storage.CacheQuotaStrategy; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; public class StorageStatsService extends IStorageStatsManager.Stub { private static final String TAG = "StorageStatsService"; private static final String PROP_DISABLE_QUOTA = "fw.disable_quota"; private static final String PROP_VERIFY_STORAGE = "fw.verify_storage"; private static final long DELAY_IN_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_QUOTA = 64 * TrafficStats.MB_IN_BYTES; public static class Lifecycle extends SystemService { private StorageStatsService mService; public Lifecycle(Context context) { super(context); } @Override public void onStart() { mService = new StorageStatsService(getContext()); publishBinderService(Context.STORAGE_STATS_SERVICE, mService); } } private final Context mContext; private final AppOpsManager mAppOps; private final UserManager mUser; private final PackageManager mPackage; private final StorageManager mStorage; private final ArrayMap mCacheQuotas; private final Installer mInstaller; private final H mHandler; public StorageStatsService(Context context) { mContext = Preconditions.checkNotNull(context); mAppOps = Preconditions.checkNotNull(context.getSystemService(AppOpsManager.class)); mUser = Preconditions.checkNotNull(context.getSystemService(UserManager.class)); mPackage = Preconditions.checkNotNull(context.getPackageManager()); mStorage = Preconditions.checkNotNull(context.getSystemService(StorageManager.class)); mCacheQuotas = new ArrayMap<>(); mInstaller = new Installer(context); mInstaller.onStart(); invalidateMounts(); mHandler = new H(IoThread.get().getLooper()); mHandler.sendEmptyMessage(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE); mStorage.registerListener(new StorageEventListener() { @Override public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { switch (vol.type) { case VolumeInfo.TYPE_PRIVATE: case VolumeInfo.TYPE_EMULATED: if (newState == VolumeInfo.STATE_MOUNTED) { invalidateMounts(); } } } }); } private void invalidateMounts() { try { mInstaller.invalidateMounts(); } catch (InstallerException e) { Slog.wtf(TAG, "Failed to invalidate mounts", e); } } private void enforcePermission(int callingUid, String callingPackage) { final int mode = mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, callingUid, callingPackage); switch (mode) { case AppOpsManager.MODE_ALLOWED: return; case AppOpsManager.MODE_DEFAULT: mContext.enforceCallingOrSelfPermission( android.Manifest.permission.PACKAGE_USAGE_STATS, TAG); return; default: throw new SecurityException("Package " + callingPackage + " from UID " + callingUid + " blocked by mode " + mode); } } @Override public boolean isQuotaSupported(String volumeUuid, String callingPackage) { enforcePermission(Binder.getCallingUid(), callingPackage); try { return mInstaller.isQuotaSupported(volumeUuid); } catch (InstallerException e) { throw new ParcelableException(new IOException(e.getMessage())); } } @Override public long getTotalBytes(String volumeUuid, String callingPackage) { // NOTE: No permissions required if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) { return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize()); } else { final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid); if (vol == null) { throw new ParcelableException( new IOException("Failed to find storage device for UUID " + volumeUuid)); } return FileUtils.roundStorageSize(vol.disk.size); } } @Override public long getFreeBytes(String volumeUuid, String callingPackage) { // NOTE: No permissions required final long token = Binder.clearCallingIdentity(); try { final File path; try { path = mStorage.findPathForUuid(volumeUuid); } catch (FileNotFoundException e) { throw new ParcelableException(e); } // Free space is usable bytes plus any cached data that we're // willing to automatically clear. To avoid user confusion, this // logic should be kept in sync with getAllocatableBytes(). if (isQuotaSupported(volumeUuid, callingPackage)) { final long cacheTotal = getCacheBytes(volumeUuid, callingPackage); final long cacheReserved = mStorage.getStorageCacheBytes(path); final long cacheClearable = Math.max(0, cacheTotal - cacheReserved); return path.getUsableSpace() + cacheClearable; } else { return path.getUsableSpace(); } } finally { Binder.restoreCallingIdentity(token); } } @Override public long getCacheBytes(String volumeUuid, String callingPackage) { enforcePermission(Binder.getCallingUid(), callingPackage); long cacheBytes = 0; for (UserInfo user : mUser.getUsers()) { final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null); cacheBytes += stats.cacheBytes; } return cacheBytes; } @Override public long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage) { enforcePermission(Binder.getCallingUid(), callingPackage); if (mCacheQuotas.containsKey(volumeUuid)) { final SparseLongArray uidMap = mCacheQuotas.get(volumeUuid); return uidMap.get(uid, DEFAULT_QUOTA); } return DEFAULT_QUOTA; } @Override public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, String callingPackage) { if (userId != UserHandle.getCallingUserId()) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); } final ApplicationInfo appInfo; try { appInfo = mPackage.getApplicationInfoAsUser(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); } catch (NameNotFoundException e) { throw new ParcelableException(e); } if (Binder.getCallingUid() == appInfo.uid) { // No permissions required when asking about themselves } else { enforcePermission(Binder.getCallingUid(), callingPackage); } if (defeatNullable(mPackage.getPackagesForUid(appInfo.uid)).length == 1) { // Only one package inside UID means we can fast-path return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage); } else { // Multiple packages means we need to go manual final int appId = UserHandle.getUserId(appInfo.uid); final String[] packageNames = new String[] { packageName }; final long[] ceDataInodes = new long[1]; String[] codePaths = new String[0]; if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { // We don't count code baked into system image } else { codePaths = ArrayUtils.appendElement(String.class, codePaths, appInfo.getCodePath()); } final PackageStats stats = new PackageStats(TAG); try { mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, appId, ceDataInodes, codePaths, stats); } catch (InstallerException e) { throw new ParcelableException(new IOException(e.getMessage())); } return translate(stats); } } @Override public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) { final int userId = UserHandle.getUserId(uid); final int appId = UserHandle.getAppId(uid); if (userId != UserHandle.getCallingUserId()) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); } if (Binder.getCallingUid() == uid) { // No permissions required when asking about themselves } else { enforcePermission(Binder.getCallingUid(), callingPackage); } final String[] packageNames = defeatNullable(mPackage.getPackagesForUid(uid)); final long[] ceDataInodes = new long[packageNames.length]; String[] codePaths = new String[0]; for (int i = 0; i < packageNames.length; i++) { try { final ApplicationInfo appInfo = mPackage.getApplicationInfoAsUser(packageNames[i], PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) { // We don't count code baked into system image } else { codePaths = ArrayUtils.appendElement(String.class, codePaths, appInfo.getCodePath()); } } catch (NameNotFoundException e) { throw new ParcelableException(e); } } final PackageStats stats = new PackageStats(TAG); try { mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(), appId, ceDataInodes, codePaths, stats); if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { final PackageStats manualStats = new PackageStats(TAG); mInstaller.getAppSize(volumeUuid, packageNames, userId, 0, appId, ceDataInodes, codePaths, manualStats); checkEquals("UID " + uid, manualStats, stats); } } catch (InstallerException e) { throw new ParcelableException(new IOException(e.getMessage())); } return translate(stats); } @Override public StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage) { if (userId != UserHandle.getCallingUserId()) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); } // Always require permission to see user-level stats enforcePermission(Binder.getCallingUid(), callingPackage); final int[] appIds = getAppIds(userId); final PackageStats stats = new PackageStats(TAG); try { mInstaller.getUserSize(volumeUuid, userId, getDefaultFlags(), appIds, stats); if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { final PackageStats manualStats = new PackageStats(TAG); mInstaller.getUserSize(volumeUuid, userId, 0, appIds, manualStats); checkEquals("User " + userId, manualStats, stats); } } catch (InstallerException e) { throw new ParcelableException(new IOException(e.getMessage())); } return translate(stats); } @Override public ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId, String callingPackage) { if (userId != UserHandle.getCallingUserId()) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); } // Always require permission to see user-level stats enforcePermission(Binder.getCallingUid(), callingPackage); final int[] appIds = getAppIds(userId); final long[] stats; try { stats = mInstaller.getExternalSize(volumeUuid, userId, getDefaultFlags(), appIds); if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) { final long[] manualStats = mInstaller.getExternalSize(volumeUuid, userId, 0, appIds); checkEquals("External " + userId, manualStats, stats); } } catch (InstallerException e) { throw new ParcelableException(new IOException(e.getMessage())); } final ExternalStorageStats res = new ExternalStorageStats(); res.totalBytes = stats[0]; res.audioBytes = stats[1]; res.videoBytes = stats[2]; res.imageBytes = stats[3]; res.appBytes = stats[4]; return res; } private int[] getAppIds(int userId) { int[] appIds = null; for (ApplicationInfo app : mPackage.getInstalledApplicationsAsUser( PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)) { final int appId = UserHandle.getAppId(app.uid); if (!ArrayUtils.contains(appIds, appId)) { appIds = ArrayUtils.appendInt(appIds, appId); } } return appIds; } private static int getDefaultFlags() { if (SystemProperties.getBoolean(PROP_DISABLE_QUOTA, false)) { return 0; } else { return Installer.FLAG_USE_QUOTA; } } private static void checkEquals(String msg, long[] a, long[] b) { for (int i = 0; i < a.length; i++) { checkEquals(msg + "[" + i + "]", a[i], b[i]); } } private static void checkEquals(String msg, PackageStats a, PackageStats b) { checkEquals(msg + " codeSize", a.codeSize, b.codeSize); checkEquals(msg + " dataSize", a.dataSize, b.dataSize); checkEquals(msg + " cacheSize", a.cacheSize, b.cacheSize); checkEquals(msg + " externalCodeSize", a.externalCodeSize, b.externalCodeSize); checkEquals(msg + " externalDataSize", a.externalDataSize, b.externalDataSize); checkEquals(msg + " externalCacheSize", a.externalCacheSize, b.externalCacheSize); } private static void checkEquals(String msg, long expected, long actual) { if (expected != actual) { Slog.e(TAG, msg + " expected " + expected + " actual " + actual); } } private static StorageStats translate(PackageStats stats) { final StorageStats res = new StorageStats(); res.codeBytes = stats.codeSize + stats.externalCodeSize; res.dataBytes = stats.dataSize + stats.externalDataSize; res.cacheBytes = stats.cacheSize + stats.externalCacheSize; return res; } private class H extends Handler { private static final int MSG_CHECK_STORAGE_DELTA = 100; private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101; /** * By only triggering a re-calculation after the storage has changed sizes, we can avoid * recalculating quotas too often. Minimum change delta defines the percentage of change * we need to see before we recalculate. */ private static final double MINIMUM_CHANGE_DELTA = 0.05; private static final int UNSET = -1; private static final boolean DEBUG = false; private final StatFs mStats; private long mPreviousBytes; private double mMinimumThresholdBytes; public H(Looper looper) { super(looper); // TODO: Handle all private volumes. mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath()); mPreviousBytes = mStats.getAvailableBytes(); mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA; } public void handleMessage(Message msg) { if (DEBUG) { Slog.v(TAG, ">>> handling " + msg.what); } if (!isCacheQuotaCalculationsEnabled(mContext.getContentResolver())) { return; } switch (msg.what) { case MSG_CHECK_STORAGE_DELTA: { long bytesDelta = Math.abs(mPreviousBytes - mStats.getAvailableBytes()); if (bytesDelta > mMinimumThresholdBytes) { mPreviousBytes = mStats.getAvailableBytes(); recalculateQuotas(getInitializedStrategy()); notifySignificantDelta(); } sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); break; } case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: { CacheQuotaStrategy strategy = getInitializedStrategy(); mPreviousBytes = UNSET; try { mPreviousBytes = strategy.setupQuotasFromFile(); } catch (IOException e) { Slog.e(TAG, "An error occurred while reading the cache quota file.", e); } catch (IllegalStateException e) { Slog.e(TAG, "Cache quota XML file is malformed?", e); } // If errors occurred getting the quotas from disk, let's re-calc them. if (mPreviousBytes < 0) { mPreviousBytes = mStats.getAvailableBytes(); recalculateQuotas(strategy); } sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS); break; } default: if (DEBUG) { Slog.v(TAG, ">>> default message case "); } return; } } private void recalculateQuotas(CacheQuotaStrategy strategy) { if (DEBUG) { Slog.v(TAG, ">>> recalculating quotas "); } strategy.recalculateQuotas(); } private CacheQuotaStrategy getInitializedStrategy() { UsageStatsManagerInternal usageStatsManager = LocalServices.getService(UsageStatsManagerInternal.class); return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller, mCacheQuotas); } } @VisibleForTesting static boolean isCacheQuotaCalculationsEnabled(ContentResolver resolver) { return Settings.Global.getInt( resolver, Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION, 1) != 0; } /** * Hacky way of notifying that disk space has changed significantly; we do * this to cause "available space" values to be requeried. */ void notifySignificantDelta() { mContext.getContentResolver().notifyChange( Uri.parse("content://com.android.externalstorage.documents/"), null, false); } }