1 /*
2  * Copyright (C) 2011 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.settings.deviceinfo;
18 
19 import android.app.ActivityManager;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.ServiceConnection;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.IPackageStatsObserver;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PackageStats;
28 import android.content.pm.UserInfo;
29 import android.os.Environment;
30 import android.os.Environment.UserEnvironment;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 import android.os.IBinder;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.os.storage.StorageVolume;
39 import android.util.Log;
40 import android.util.SparseLongArray;
41 
42 import com.android.internal.app.IMediaContainerService;
43 import com.google.android.collect.Maps;
44 import com.google.android.collect.Sets;
45 
46 import java.io.File;
47 import java.lang.ref.WeakReference;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Set;
53 
54 import javax.annotation.concurrent.GuardedBy;
55 
56 /**
57  * Utility for measuring the disk usage of internal storage or a physical
58  * {@link StorageVolume}. Connects with a remote {@link IMediaContainerService}
59  * and delivers results to {@link MeasurementReceiver}.
60  */
61 public class StorageMeasurement {
62     private static final String TAG = "StorageMeasurement";
63 
64     private static final boolean LOCAL_LOGV = true;
65     static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE);
66 
67     private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer";
68 
69     public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName(
70             DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService");
71 
72     /** Media types to measure on external storage. */
73     private static final Set<String> sMeasureMediaTypes = Sets.newHashSet(
74             Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
75             Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC,
76             Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
77             Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS,
78             Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID);
79 
80     @GuardedBy("sInstances")
81     private static HashMap<StorageVolume, StorageMeasurement> sInstances = Maps.newHashMap();
82 
83     /**
84      * Obtain shared instance of {@link StorageMeasurement} for given physical
85      * {@link StorageVolume}, or internal storage if {@code null}.
86      */
getInstance(Context context, StorageVolume volume)87     public static StorageMeasurement getInstance(Context context, StorageVolume volume) {
88         synchronized (sInstances) {
89             StorageMeasurement value = sInstances.get(volume);
90             if (value == null) {
91                 value = new StorageMeasurement(context.getApplicationContext(), volume);
92                 sInstances.put(volume, value);
93             }
94             return value;
95         }
96     }
97 
98     public static class MeasurementDetails {
99         public long totalSize;
100         public long availSize;
101 
102         /**
103          * Total apps disk usage.
104          * <p>
105          * When measuring internal storage, this value includes the code size of
106          * all apps (regardless of install status for current user), and
107          * internal disk used by the current user's apps. When the device
108          * emulates external storage, this value also includes emulated storage
109          * used by the current user's apps.
110          * <p>
111          * When measuring a physical {@link StorageVolume}, this value includes
112          * usage by all apps on that volume.
113          */
114         public long appsSize;
115 
116         /**
117          * Total cache disk usage by apps.
118          */
119         public long cacheSize;
120 
121         /**
122          * Total media disk usage, categorized by types such as
123          * {@link Environment#DIRECTORY_MUSIC}.
124          * <p>
125          * When measuring internal storage, this reflects media on emulated
126          * storage for the current user.
127          * <p>
128          * When measuring a physical {@link StorageVolume}, this reflects media
129          * on that volume.
130          */
131         public HashMap<String, Long> mediaSize = Maps.newHashMap();
132 
133         /**
134          * Misc external disk usage for the current user, unaccounted in
135          * {@link #mediaSize}.
136          */
137         public long miscSize;
138 
139         /**
140          * Total disk usage for users, which is only meaningful for emulated
141          * internal storage. Key is {@link UserHandle}.
142          */
143         public SparseLongArray usersSize = new SparseLongArray();
144     }
145 
146     public interface MeasurementReceiver {
updateApproximate(StorageMeasurement meas, long totalSize, long availSize)147         public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize);
updateDetails(StorageMeasurement meas, MeasurementDetails details)148         public void updateDetails(StorageMeasurement meas, MeasurementDetails details);
149     }
150 
151     private volatile WeakReference<MeasurementReceiver> mReceiver;
152 
153     /** Physical volume being measured, or {@code null} for internal. */
154     private final StorageVolume mVolume;
155 
156     private final boolean mIsInternal;
157     private final boolean mIsPrimary;
158 
159     private final MeasurementHandler mHandler;
160 
161     private long mTotalSize;
162     private long mAvailSize;
163 
164     List<FileInfo> mFileInfoForMisc;
165 
StorageMeasurement(Context context, StorageVolume volume)166     private StorageMeasurement(Context context, StorageVolume volume) {
167         mVolume = volume;
168         mIsInternal = volume == null;
169         mIsPrimary = volume != null ? volume.isPrimary() : false;
170 
171         // Start the thread that will measure the disk usage.
172         final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
173         handlerThread.start();
174         mHandler = new MeasurementHandler(context, handlerThread.getLooper());
175     }
176 
setReceiver(MeasurementReceiver receiver)177     public void setReceiver(MeasurementReceiver receiver) {
178         if (mReceiver == null || mReceiver.get() == null) {
179             mReceiver = new WeakReference<MeasurementReceiver>(receiver);
180         }
181     }
182 
measure()183     public void measure() {
184         if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
185             mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
186         }
187     }
188 
cleanUp()189     public void cleanUp() {
190         mReceiver = null;
191         mHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
192         mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
193     }
194 
invalidate()195     public void invalidate() {
196         mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
197     }
198 
sendInternalApproximateUpdate()199     private void sendInternalApproximateUpdate() {
200         MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
201         if (receiver == null) {
202             return;
203         }
204         receiver.updateApproximate(this, mTotalSize, mAvailSize);
205     }
206 
sendExactUpdate(MeasurementDetails details)207     private void sendExactUpdate(MeasurementDetails details) {
208         MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
209         if (receiver == null) {
210             if (LOGV) {
211                 Log.i(TAG, "measurements dropped because receiver is null! wasted effort");
212             }
213             return;
214         }
215         receiver.updateDetails(this, details);
216     }
217 
218     private static class StatsObserver extends IPackageStatsObserver.Stub {
219         private final boolean mIsInternal;
220         private final MeasurementDetails mDetails;
221         private final int mCurrentUser;
222         private final Message mFinished;
223 
224         private int mRemaining;
225 
StatsObserver(boolean isInternal, MeasurementDetails details, int currentUser, Message finished, int remaining)226         public StatsObserver(boolean isInternal, MeasurementDetails details, int currentUser,
227                 Message finished, int remaining) {
228             mIsInternal = isInternal;
229             mDetails = details;
230             mCurrentUser = currentUser;
231             mFinished = finished;
232             mRemaining = remaining;
233         }
234 
235         @Override
onGetStatsCompleted(PackageStats stats, boolean succeeded)236         public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
237             synchronized (mDetails) {
238                 if (succeeded) {
239                     addStatsLocked(stats);
240                 }
241                 if (--mRemaining == 0) {
242                     mFinished.sendToTarget();
243                 }
244             }
245         }
246 
addStatsLocked(PackageStats stats)247         private void addStatsLocked(PackageStats stats) {
248             if (mIsInternal) {
249                 long codeSize = stats.codeSize;
250                 long dataSize = stats.dataSize;
251                 long cacheSize = stats.cacheSize;
252                 if (Environment.isExternalStorageEmulated()) {
253                     // Include emulated storage when measuring internal. OBB is
254                     // shared on emulated storage, so treat as code.
255                     codeSize += stats.externalCodeSize + stats.externalObbSize;
256                     dataSize += stats.externalDataSize + stats.externalMediaSize;
257                     cacheSize += stats.externalCacheSize;
258                 }
259 
260                 // Count code and data for current user
261                 if (stats.userHandle == mCurrentUser) {
262                     mDetails.appsSize += codeSize;
263                     mDetails.appsSize += dataSize;
264                 }
265 
266                 // User summary only includes data (code is only counted once
267                 // for the current user)
268                 addValue(mDetails.usersSize, stats.userHandle, dataSize);
269 
270                 // Include cache for all users
271                 mDetails.cacheSize += cacheSize;
272 
273             } else {
274                 // Physical storage; only count external sizes
275                 mDetails.appsSize += stats.externalCodeSize + stats.externalDataSize
276                         + stats.externalMediaSize + stats.externalObbSize;
277                 mDetails.cacheSize += stats.externalCacheSize;
278             }
279         }
280     }
281 
282     private class MeasurementHandler extends Handler {
283         public static final int MSG_MEASURE = 1;
284         public static final int MSG_CONNECTED = 2;
285         public static final int MSG_DISCONNECT = 3;
286         public static final int MSG_COMPLETED = 4;
287         public static final int MSG_INVALIDATE = 5;
288 
289         private Object mLock = new Object();
290 
291         private IMediaContainerService mDefaultContainer;
292 
293         private volatile boolean mBound = false;
294 
295         private MeasurementDetails mCached;
296 
297         private final WeakReference<Context> mContext;
298 
299         private final ServiceConnection mDefContainerConn = new ServiceConnection() {
300             @Override
301             public void onServiceConnected(ComponentName name, IBinder service) {
302                 final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(
303                         service);
304                 mDefaultContainer = imcs;
305                 mBound = true;
306                 sendMessage(obtainMessage(MSG_CONNECTED, imcs));
307             }
308 
309             @Override
310             public void onServiceDisconnected(ComponentName name) {
311                 mBound = false;
312                 removeMessages(MSG_CONNECTED);
313             }
314         };
315 
MeasurementHandler(Context context, Looper looper)316         public MeasurementHandler(Context context, Looper looper) {
317             super(looper);
318             mContext = new WeakReference<Context>(context);
319         }
320 
321         @Override
handleMessage(Message msg)322         public void handleMessage(Message msg) {
323             switch (msg.what) {
324                 case MSG_MEASURE: {
325                     if (mCached != null) {
326                         sendExactUpdate(mCached);
327                         break;
328                     }
329 
330                     final Context context = (mContext != null) ? mContext.get() : null;
331                     if (context == null) {
332                         return;
333                     }
334 
335                     synchronized (mLock) {
336                         if (mBound) {
337                             removeMessages(MSG_DISCONNECT);
338                             sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer));
339                         } else {
340                             Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
341                             context.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE,
342                                     UserHandle.OWNER);
343                         }
344                     }
345                     break;
346                 }
347                 case MSG_CONNECTED: {
348                     IMediaContainerService imcs = (IMediaContainerService) msg.obj;
349                     measureApproximateStorage(imcs);
350                     measureExactStorage(imcs);
351                     break;
352                 }
353                 case MSG_DISCONNECT: {
354                     synchronized (mLock) {
355                         if (mBound) {
356                             final Context context = (mContext != null) ? mContext.get() : null;
357                             if (context == null) {
358                                 return;
359                             }
360 
361                             mBound = false;
362                             context.unbindService(mDefContainerConn);
363                         }
364                     }
365                     break;
366                 }
367                 case MSG_COMPLETED: {
368                     mCached = (MeasurementDetails) msg.obj;
369                     sendExactUpdate(mCached);
370                     break;
371                 }
372                 case MSG_INVALIDATE: {
373                     mCached = null;
374                     break;
375                 }
376             }
377         }
378 
measureApproximateStorage(IMediaContainerService imcs)379         private void measureApproximateStorage(IMediaContainerService imcs) {
380             final String path = mVolume != null ? mVolume.getPath()
381                     : Environment.getDataDirectory().getPath();
382             try {
383                 final long[] stats = imcs.getFileSystemStats(path);
384                 mTotalSize = stats[0];
385                 mAvailSize = stats[1];
386             } catch (Exception e) {
387                 Log.w(TAG, "Problem in container service", e);
388             }
389 
390             sendInternalApproximateUpdate();
391         }
392 
measureExactStorage(IMediaContainerService imcs)393         private void measureExactStorage(IMediaContainerService imcs) {
394             final Context context = mContext != null ? mContext.get() : null;
395             if (context == null) {
396                 return;
397             }
398 
399             final MeasurementDetails details = new MeasurementDetails();
400             final Message finished = obtainMessage(MSG_COMPLETED, details);
401 
402             details.totalSize = mTotalSize;
403             details.availSize = mAvailSize;
404 
405             final UserManager userManager = (UserManager) context.getSystemService(
406                     Context.USER_SERVICE);
407             final List<UserInfo> users = userManager.getUsers();
408 
409             final int currentUser = ActivityManager.getCurrentUser();
410             final UserEnvironment currentEnv = new UserEnvironment(currentUser);
411 
412             // Measure media types for emulated storage, or for primary physical
413             // external volume
414             final boolean measureMedia = (mIsInternal && Environment.isExternalStorageEmulated())
415                     || mIsPrimary;
416             if (measureMedia) {
417                 for (String type : sMeasureMediaTypes) {
418                     final File path = currentEnv.getExternalStoragePublicDirectory(type);
419                     final long size = getDirectorySize(imcs, path);
420                     details.mediaSize.put(type, size);
421                 }
422             }
423 
424             // Measure misc files not counted under media
425             if (measureMedia) {
426                 final File path = mIsInternal ? currentEnv.getExternalStorageDirectory()
427                         : mVolume.getPathFile();
428                 details.miscSize = measureMisc(imcs, path);
429             }
430 
431             // Measure total emulated storage of all users; internal apps data
432             // will be spliced in later
433             for (UserInfo user : users) {
434                 final UserEnvironment userEnv = new UserEnvironment(user.id);
435                 final long size = getDirectorySize(imcs, userEnv.getExternalStorageDirectory());
436                 addValue(details.usersSize, user.id, size);
437             }
438 
439             // Measure all apps for all users
440             final PackageManager pm = context.getPackageManager();
441             if (mIsInternal || mIsPrimary) {
442                 final List<ApplicationInfo> apps = pm.getInstalledApplications(
443                         PackageManager.GET_UNINSTALLED_PACKAGES
444                         | PackageManager.GET_DISABLED_COMPONENTS);
445 
446                 final int count = users.size() * apps.size();
447                 final StatsObserver observer = new StatsObserver(
448                         mIsInternal, details, currentUser, finished, count);
449 
450                 for (UserInfo user : users) {
451                     for (ApplicationInfo app : apps) {
452                         pm.getPackageSizeInfo(app.packageName, user.id, observer);
453                     }
454                 }
455 
456             } else {
457                 finished.sendToTarget();
458             }
459         }
460     }
461 
getDirectorySize(IMediaContainerService imcs, File path)462     private static long getDirectorySize(IMediaContainerService imcs, File path) {
463         try {
464             final long size = imcs.calculateDirectorySize(path.toString());
465             Log.d(TAG, "getDirectorySize(" + path + ") returned " + size);
466             return size;
467         } catch (Exception e) {
468             Log.w(TAG, "Could not read memory from default container service for " + path, e);
469             return 0;
470         }
471     }
472 
measureMisc(IMediaContainerService imcs, File dir)473     private long measureMisc(IMediaContainerService imcs, File dir) {
474         mFileInfoForMisc = new ArrayList<FileInfo>();
475 
476         final File[] files = dir.listFiles();
477         if (files == null) return 0;
478 
479         // Get sizes of all top level nodes except the ones already computed
480         long counter = 0;
481         long miscSize = 0;
482 
483         for (File file : files) {
484             final String path = file.getAbsolutePath();
485             final String name = file.getName();
486             if (sMeasureMediaTypes.contains(name)) {
487                 continue;
488             }
489 
490             if (file.isFile()) {
491                 final long fileSize = file.length();
492                 mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++));
493                 miscSize += fileSize;
494             } else if (file.isDirectory()) {
495                 final long dirSize = getDirectorySize(imcs, file);
496                 mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++));
497                 miscSize += dirSize;
498             } else {
499                 // Non directory, non file: not listed
500             }
501         }
502 
503         // sort the list of FileInfo objects collected above in descending order of their sizes
504         Collections.sort(mFileInfoForMisc);
505 
506         return miscSize;
507     }
508 
509     static class FileInfo implements Comparable<FileInfo> {
510         final String mFileName;
511         final long mSize;
512         final long mId;
513 
FileInfo(String fileName, long size, long id)514         FileInfo(String fileName, long size, long id) {
515             mFileName = fileName;
516             mSize = size;
517             mId = id;
518         }
519 
520         @Override
compareTo(FileInfo that)521         public int compareTo(FileInfo that) {
522             if (this == that || mSize == that.mSize) return 0;
523             else return (mSize < that.mSize) ? 1 : -1; // for descending sort
524         }
525 
526         @Override
toString()527         public String toString() {
528             return mFileName  + " : " + mSize + ", id:" + mId;
529         }
530     }
531 
addValue(SparseLongArray array, int key, long value)532     private static void addValue(SparseLongArray array, int key, long value) {
533         array.put(key, array.get(key) + value);
534     }
535 }
536