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