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