StorageStatsService.java revision 789a8fc792725e4988ff43d554b3c8c037c41921
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                if ((vol.type == VolumeInfo.TYPE_PRIVATE)
116                        && (newState == VolumeInfo.STATE_MOUNTED)) {
117                    invalidateMounts();
118                }
119            }
120        });
121    }
122
123    private void invalidateMounts() {
124        try {
125            mInstaller.invalidateMounts();
126        } catch (InstallerException e) {
127            Slog.wtf(TAG, "Failed to invalidate mounts", e);
128        }
129    }
130
131    private void enforcePermission(int callingUid, String callingPackage) {
132        final int mode = mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS,
133                callingUid, callingPackage);
134        switch (mode) {
135            case AppOpsManager.MODE_ALLOWED:
136                return;
137            case AppOpsManager.MODE_DEFAULT:
138                mContext.enforceCallingOrSelfPermission(
139                        android.Manifest.permission.PACKAGE_USAGE_STATS, TAG);
140                return;
141            default:
142                throw new SecurityException("Package " + callingPackage + " from UID " + callingUid
143                        + " blocked by mode " + mode);
144        }
145    }
146
147    @Override
148    public boolean isQuotaSupported(String volumeUuid, String callingPackage) {
149        enforcePermission(Binder.getCallingUid(), callingPackage);
150
151        try {
152            return mInstaller.isQuotaSupported(volumeUuid);
153        } catch (InstallerException e) {
154            throw new ParcelableException(new IOException(e.getMessage()));
155        }
156    }
157
158    @Override
159    public long getTotalBytes(String volumeUuid, String callingPackage) {
160        enforcePermission(Binder.getCallingUid(), callingPackage);
161
162        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
163            return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
164        } else {
165            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
166            if (vol == null) {
167                throw new ParcelableException(
168                        new IOException("Failed to find storage device for UUID " + volumeUuid));
169            }
170            return FileUtils.roundStorageSize(vol.disk.size);
171        }
172    }
173
174    @Override
175    public long getFreeBytes(String volumeUuid, String callingPackage) {
176        enforcePermission(Binder.getCallingUid(), callingPackage);
177
178        long cacheBytes = 0;
179        final long token = Binder.clearCallingIdentity();
180        try {
181            for (UserInfo user : mUser.getUsers()) {
182                final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null);
183                cacheBytes += stats.cacheBytes;
184            }
185        } finally {
186            Binder.restoreCallingIdentity(token);
187        }
188
189        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
190            return Environment.getDataDirectory().getUsableSpace() + cacheBytes;
191        } else {
192            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
193            if (vol == null) {
194                throw new ParcelableException(
195                        new IOException("Failed to find storage device for UUID " + volumeUuid));
196            }
197            return vol.getPath().getUsableSpace() + cacheBytes;
198        }
199    }
200
201    @Override
202    public long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage) {
203        enforcePermission(Binder.getCallingUid(), callingPackage);
204
205        if (mCacheQuotas.containsKey(volumeUuid)) {
206            final SparseLongArray uidMap = mCacheQuotas.get(volumeUuid);
207            return uidMap.get(uid, DEFAULT_QUOTA);
208        }
209
210        return DEFAULT_QUOTA;
211    }
212
213    @Override
214    public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId,
215            String callingPackage) {
216        enforcePermission(Binder.getCallingUid(), callingPackage);
217        if (userId != UserHandle.getCallingUserId()) {
218            mContext.enforceCallingOrSelfPermission(
219                    android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
220        }
221
222        final ApplicationInfo appInfo;
223        try {
224            appInfo = mPackage.getApplicationInfoAsUser(packageName,
225                    PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
226        } catch (NameNotFoundException e) {
227            throw new ParcelableException(e);
228        }
229
230        if (mPackage.getPackagesForUid(appInfo.uid).length == 1) {
231            // Only one package inside UID means we can fast-path
232            return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage);
233        } else {
234            // Multiple packages means we need to go manual
235            final int appId = UserHandle.getUserId(appInfo.uid);
236            final String[] packageNames = new String[] { packageName };
237            final long[] ceDataInodes = new long[1];
238            final String[] codePaths = new String[] { appInfo.getCodePath() };
239
240            final PackageStats stats = new PackageStats(TAG);
241            try {
242                mInstaller.getAppSize(volumeUuid, packageNames, userId, 0,
243                        appId, ceDataInodes, codePaths, stats);
244            } catch (InstallerException e) {
245                throw new ParcelableException(new IOException(e.getMessage()));
246            }
247            return translate(stats);
248        }
249    }
250
251    @Override
252    public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) {
253        enforcePermission(Binder.getCallingUid(), callingPackage);
254        if (UserHandle.getUserId(uid) != UserHandle.getCallingUserId()) {
255            mContext.enforceCallingOrSelfPermission(
256                    android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
257        }
258
259        final int userId = UserHandle.getUserId(uid);
260        final int appId = UserHandle.getAppId(uid);
261
262        final String[] packageNames = mPackage.getPackagesForUid(uid);
263        final long[] ceDataInodes = new long[packageNames.length];
264        final String[] codePaths = new String[packageNames.length];
265
266        for (int i = 0; i < packageNames.length; i++) {
267            try {
268                codePaths[i] = mPackage.getApplicationInfoAsUser(packageNames[i],
269                        PackageManager.MATCH_UNINSTALLED_PACKAGES, userId).getCodePath();
270            } catch (NameNotFoundException e) {
271                throw new ParcelableException(e);
272            }
273        }
274
275        final PackageStats stats = new PackageStats(TAG);
276        try {
277            mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(),
278                    appId, ceDataInodes, codePaths, stats);
279
280            if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) {
281                final PackageStats manualStats = new PackageStats(TAG);
282                mInstaller.getAppSize(volumeUuid, packageNames, userId, 0,
283                        appId, ceDataInodes, codePaths, manualStats);
284                checkEquals("UID " + uid, manualStats, stats);
285            }
286        } catch (InstallerException e) {
287            throw new ParcelableException(new IOException(e.getMessage()));
288        }
289        return translate(stats);
290    }
291
292    @Override
293    public StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage) {
294        enforcePermission(Binder.getCallingUid(), callingPackage);
295        if (userId != UserHandle.getCallingUserId()) {
296            mContext.enforceCallingOrSelfPermission(
297                    android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
298        }
299
300        int[] appIds = null;
301        for (ApplicationInfo app : mPackage.getInstalledApplicationsAsUser(
302                PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)) {
303            final int appId = UserHandle.getAppId(app.uid);
304            if (!ArrayUtils.contains(appIds, appId)) {
305                appIds = ArrayUtils.appendInt(appIds, appId);
306            }
307        }
308
309        final PackageStats stats = new PackageStats(TAG);
310        try {
311            mInstaller.getUserSize(volumeUuid, userId, getDefaultFlags(), appIds, stats);
312
313            if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) {
314                final PackageStats manualStats = new PackageStats(TAG);
315                mInstaller.getUserSize(volumeUuid, userId, 0, appIds, manualStats);
316                checkEquals("User " + userId, manualStats, stats);
317            }
318        } catch (InstallerException e) {
319            throw new ParcelableException(new IOException(e.getMessage()));
320        }
321        return translate(stats);
322    }
323
324    @Override
325    public ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId,
326            String callingPackage) {
327        enforcePermission(Binder.getCallingUid(), callingPackage);
328        if (userId != UserHandle.getCallingUserId()) {
329            mContext.enforceCallingOrSelfPermission(
330                    android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
331        }
332
333        final long[] stats;
334        try {
335            stats = mInstaller.getExternalSize(volumeUuid, userId, getDefaultFlags());
336
337            if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) {
338                final long[] manualStats = mInstaller.getExternalSize(volumeUuid, userId, 0);
339                checkEquals("External " + userId, manualStats, stats);
340            }
341        } catch (InstallerException e) {
342            throw new ParcelableException(new IOException(e.getMessage()));
343        }
344
345        final ExternalStorageStats res = new ExternalStorageStats();
346        res.totalBytes = stats[0];
347        res.audioBytes = stats[1];
348        res.videoBytes = stats[2];
349        res.imageBytes = stats[3];
350        return res;
351    }
352
353    private static int getDefaultFlags() {
354        if (SystemProperties.getBoolean(PROP_DISABLE_QUOTA, false)) {
355            return 0;
356        } else {
357            return Installer.FLAG_USE_QUOTA;
358        }
359    }
360
361    private static void checkEquals(String msg, long[] a, long[] b) {
362        for (int i = 0; i < a.length; i++) {
363            checkEquals(msg + "[" + i + "]", a[i], b[i]);
364        }
365    }
366
367    private static void checkEquals(String msg, PackageStats a, PackageStats b) {
368        checkEquals(msg + " codeSize", a.codeSize, b.codeSize);
369        checkEquals(msg + " dataSize", a.dataSize, b.dataSize);
370        checkEquals(msg + " cacheSize", a.cacheSize, b.cacheSize);
371        checkEquals(msg + " externalCodeSize", a.externalCodeSize, b.externalCodeSize);
372        checkEquals(msg + " externalDataSize", a.externalDataSize, b.externalDataSize);
373        checkEquals(msg + " externalCacheSize", a.externalCacheSize, b.externalCacheSize);
374    }
375
376    private static void checkEquals(String msg, long expected, long actual) {
377        if (expected != actual) {
378            Slog.e(TAG, msg + " expected " + expected + " actual " + actual);
379        }
380    }
381
382    private static StorageStats translate(PackageStats stats) {
383        final StorageStats res = new StorageStats();
384        res.codeBytes = stats.codeSize + stats.externalCodeSize;
385        res.dataBytes = stats.dataSize + stats.externalDataSize;
386        res.cacheBytes = stats.cacheSize + stats.externalCacheSize;
387        return res;
388    }
389
390    private class H extends Handler {
391        private static final int MSG_CHECK_STORAGE_DELTA = 100;
392        private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101;
393        /**
394         * By only triggering a re-calculation after the storage has changed sizes, we can avoid
395         * recalculating quotas too often. Minimum change delta defines the percentage of change
396         * we need to see before we recalculate.
397         */
398        private static final double MINIMUM_CHANGE_DELTA = 0.05;
399        private static final int UNSET = -1;
400        private static final boolean DEBUG = false;
401
402        private final StatFs mStats;
403        private long mPreviousBytes;
404        private double mMinimumThresholdBytes;
405
406        public H(Looper looper) {
407            super(looper);
408            // TODO: Handle all private volumes.
409            mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath());
410            mPreviousBytes = mStats.getAvailableBytes();
411            mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA;
412        }
413
414        public void handleMessage(Message msg) {
415            if (DEBUG) {
416                Slog.v(TAG, ">>> handling " + msg.what);
417            }
418
419            if (!isCacheQuotaCalculationsEnabled(mContext.getContentResolver())) {
420                return;
421            }
422
423            switch (msg.what) {
424                case MSG_CHECK_STORAGE_DELTA: {
425                    long bytesDelta = Math.abs(mPreviousBytes - mStats.getAvailableBytes());
426                    if (bytesDelta > mMinimumThresholdBytes) {
427                        mPreviousBytes = mStats.getAvailableBytes();
428                        recalculateQuotas(getInitializedStrategy());
429                    }
430                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
431                    break;
432                }
433                case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: {
434                    CacheQuotaStrategy strategy = getInitializedStrategy();
435                    mPreviousBytes = UNSET;
436                    try {
437                        mPreviousBytes = strategy.setupQuotasFromFile();
438                    } catch (IOException e) {
439                        Slog.e(TAG, "An error occurred while reading the cache quota file.", e);
440                    } catch (IllegalStateException e) {
441                        Slog.e(TAG, "Cache quota XML file is malformed?", e);
442                    }
443
444                    // If errors occurred getting the quotas from disk, let's re-calc them.
445                    if (mPreviousBytes < 0) {
446                        mPreviousBytes = mStats.getAvailableBytes();
447                        recalculateQuotas(strategy);
448                    }
449                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
450                    break;
451                }
452                default:
453                    if (DEBUG) {
454                        Slog.v(TAG, ">>> default message case ");
455                    }
456                    return;
457            }
458        }
459
460        private void recalculateQuotas(CacheQuotaStrategy strategy) {
461            if (DEBUG) {
462                Slog.v(TAG, ">>> recalculating quotas ");
463            }
464
465            strategy.recalculateQuotas();
466        }
467
468        private CacheQuotaStrategy getInitializedStrategy() {
469            UsageStatsManagerInternal usageStatsManager =
470                    LocalServices.getService(UsageStatsManagerInternal.class);
471            return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller, mCacheQuotas);
472        }
473    }
474
475    @VisibleForTesting
476    static boolean isCacheQuotaCalculationsEnabled(ContentResolver resolver) {
477        return Settings.Global.getInt(
478                resolver, Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION, 1) != 0;
479    }
480}
481