StorageStatsService.java revision ddff807b762a8a455287abc97aea8f97b98fb104
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
17package com.android.server.usage;
18
19import static com.android.internal.util.ArrayUtils.defeatNullable;
20
21import android.app.AppOpsManager;
22import android.app.usage.ExternalStorageStats;
23import android.app.usage.IStorageStatsManager;
24import android.app.usage.StorageStats;
25import android.app.usage.UsageStatsManagerInternal;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.pm.ApplicationInfo;
29import android.content.pm.PackageManager;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.pm.PackageStats;
32import android.content.pm.UserInfo;
33import android.net.TrafficStats;
34import android.net.Uri;
35import android.os.Binder;
36import android.os.Environment;
37import android.os.FileUtils;
38import android.os.Handler;
39import android.os.Looper;
40import android.os.Message;
41import android.os.ParcelableException;
42import android.os.StatFs;
43import android.os.SystemProperties;
44import android.os.UserHandle;
45import android.os.UserManager;
46import android.os.storage.StorageEventListener;
47import android.os.storage.StorageManager;
48import android.os.storage.VolumeInfo;
49import android.provider.Settings;
50import android.text.format.DateUtils;
51import android.util.ArrayMap;
52import android.util.Slog;
53import android.util.SparseLongArray;
54
55import com.android.internal.annotations.VisibleForTesting;
56import com.android.internal.util.ArrayUtils;
57import com.android.internal.util.Preconditions;
58import com.android.server.IoThread;
59import com.android.server.LocalServices;
60import com.android.server.SystemService;
61import com.android.server.pm.Installer;
62import com.android.server.pm.Installer.InstallerException;
63import com.android.server.storage.CacheQuotaStrategy;
64
65import java.io.File;
66import java.io.FileNotFoundException;
67import java.io.IOException;
68
69public 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
81        public Lifecycle(Context context) {
82            super(context);
83        }
84
85        @Override
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
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
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
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
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
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
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);
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
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
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
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
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
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
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        return res;
396    }
397
398    private int[] getAppIds(int userId) {
399        int[] appIds = null;
400        for (ApplicationInfo app : mPackage.getInstalledApplicationsAsUser(
401                PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)) {
402            final int appId = UserHandle.getAppId(app.uid);
403            if (!ArrayUtils.contains(appIds, appId)) {
404                appIds = ArrayUtils.appendInt(appIds, appId);
405            }
406        }
407        return appIds;
408    }
409
410    private static int getDefaultFlags() {
411        if (SystemProperties.getBoolean(PROP_DISABLE_QUOTA, false)) {
412            return 0;
413        } else {
414            return Installer.FLAG_USE_QUOTA;
415        }
416    }
417
418    private static void checkEquals(String msg, long[] a, long[] b) {
419        for (int i = 0; i < a.length; i++) {
420            checkEquals(msg + "[" + i + "]", a[i], b[i]);
421        }
422    }
423
424    private static void checkEquals(String msg, PackageStats a, PackageStats b) {
425        checkEquals(msg + " codeSize", a.codeSize, b.codeSize);
426        checkEquals(msg + " dataSize", a.dataSize, b.dataSize);
427        checkEquals(msg + " cacheSize", a.cacheSize, b.cacheSize);
428        checkEquals(msg + " externalCodeSize", a.externalCodeSize, b.externalCodeSize);
429        checkEquals(msg + " externalDataSize", a.externalDataSize, b.externalDataSize);
430        checkEquals(msg + " externalCacheSize", a.externalCacheSize, b.externalCacheSize);
431    }
432
433    private static void checkEquals(String msg, long expected, long actual) {
434        if (expected != actual) {
435            Slog.e(TAG, msg + " expected " + expected + " actual " + actual);
436        }
437    }
438
439    private static StorageStats translate(PackageStats stats) {
440        final StorageStats res = new StorageStats();
441        res.codeBytes = stats.codeSize + stats.externalCodeSize;
442        res.dataBytes = stats.dataSize + stats.externalDataSize;
443        res.cacheBytes = stats.cacheSize + stats.externalCacheSize;
444        return res;
445    }
446
447    private class H extends Handler {
448        private static final int MSG_CHECK_STORAGE_DELTA = 100;
449        private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101;
450        /**
451         * By only triggering a re-calculation after the storage has changed sizes, we can avoid
452         * recalculating quotas too often. Minimum change delta defines the percentage of change
453         * we need to see before we recalculate.
454         */
455        private static final double MINIMUM_CHANGE_DELTA = 0.05;
456        private static final int UNSET = -1;
457        private static final boolean DEBUG = false;
458
459        private final StatFs mStats;
460        private long mPreviousBytes;
461        private double mMinimumThresholdBytes;
462
463        public H(Looper looper) {
464            super(looper);
465            // TODO: Handle all private volumes.
466            mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath());
467            mPreviousBytes = mStats.getAvailableBytes();
468            mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA;
469        }
470
471        public void handleMessage(Message msg) {
472            if (DEBUG) {
473                Slog.v(TAG, ">>> handling " + msg.what);
474            }
475
476            if (!isCacheQuotaCalculationsEnabled(mContext.getContentResolver())) {
477                return;
478            }
479
480            switch (msg.what) {
481                case MSG_CHECK_STORAGE_DELTA: {
482                    long bytesDelta = Math.abs(mPreviousBytes - mStats.getAvailableBytes());
483                    if (bytesDelta > mMinimumThresholdBytes) {
484                        mPreviousBytes = mStats.getAvailableBytes();
485                        recalculateQuotas(getInitializedStrategy());
486                        notifySignificantDelta();
487                    }
488                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
489                    break;
490                }
491                case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: {
492                    CacheQuotaStrategy strategy = getInitializedStrategy();
493                    mPreviousBytes = UNSET;
494                    try {
495                        mPreviousBytes = strategy.setupQuotasFromFile();
496                    } catch (IOException e) {
497                        Slog.e(TAG, "An error occurred while reading the cache quota file.", e);
498                    } catch (IllegalStateException e) {
499                        Slog.e(TAG, "Cache quota XML file is malformed?", e);
500                    }
501
502                    // If errors occurred getting the quotas from disk, let's re-calc them.
503                    if (mPreviousBytes < 0) {
504                        mPreviousBytes = mStats.getAvailableBytes();
505                        recalculateQuotas(strategy);
506                    }
507                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
508                    break;
509                }
510                default:
511                    if (DEBUG) {
512                        Slog.v(TAG, ">>> default message case ");
513                    }
514                    return;
515            }
516        }
517
518        private void recalculateQuotas(CacheQuotaStrategy strategy) {
519            if (DEBUG) {
520                Slog.v(TAG, ">>> recalculating quotas ");
521            }
522
523            strategy.recalculateQuotas();
524        }
525
526        private CacheQuotaStrategy getInitializedStrategy() {
527            UsageStatsManagerInternal usageStatsManager =
528                    LocalServices.getService(UsageStatsManagerInternal.class);
529            return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller, mCacheQuotas);
530        }
531    }
532
533    @VisibleForTesting
534    static boolean isCacheQuotaCalculationsEnabled(ContentResolver resolver) {
535        return Settings.Global.getInt(
536                resolver, Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION, 1) != 0;
537    }
538
539    /**
540     * Hacky way of notifying that disk space has changed significantly; we do
541     * this to cause "available space" values to be requeried.
542     */
543    void notifySignificantDelta() {
544        mContext.getContentResolver().notifyChange(
545                Uri.parse("content://com.android.externalstorage.documents/"), null, false);
546    }
547}
548