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