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