OSDN Git Service

Storage stats on external SD card in Settings.
[android-x86/packages-apps-Settings.git] / src / com / android / settings / deviceinfo / StorageMeasurement.java
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.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.content.pm.ApplicationInfo;
24 import android.content.pm.IPackageStatsObserver;
25 import android.content.pm.PackageManager;
26 import android.content.pm.PackageStats;
27 import android.os.Bundle;
28 import android.os.Environment;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.os.IBinder;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.StatFs;
35 import android.os.storage.StorageVolume;
36 import android.util.Log;
37
38 import com.android.internal.app.IMediaContainerService;
39
40 import java.io.File;
41 import java.lang.ref.WeakReference;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.concurrent.ConcurrentHashMap;
47
48 /**
49  * Measure the memory for various systems.
50  *
51  * TODO: This class should ideally have less knowledge about what the context
52  * it's measuring is. In the future, reduce the amount of stuff it needs to
53  * know about by just keeping an array of measurement types of the following
54  * properties:
55  *
56  *   Filesystem stats (using StatFs)
57  *   Directory measurements (using DefaultContainerService.measureDir)
58  *   Application measurements (using PackageManager)
59  *
60  * Then the calling application would just specify the type and an argument.
61  * This class would keep track of it while the calling application would
62  * decide on how to use it.
63  */
64 public class StorageMeasurement {
65     private static final String TAG = "StorageMeasurement";
66
67     private static final boolean LOCAL_LOGV = true;
68     static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE);
69
70     public static final String TOTAL_SIZE = "total_size";
71
72     public static final String AVAIL_SIZE = "avail_size";
73
74     public static final String APPS_USED = "apps_used";
75
76     public static final String DOWNLOADS_SIZE = "downloads_size";
77
78     public static final String MISC_SIZE = "misc_size";
79
80     public static final String MEDIA_SIZES = "media_sizes";
81
82     private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer";
83
84     private static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName(
85             DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService");
86
87     private final MeasurementHandler mHandler;
88
89     private static Map<StorageVolume, StorageMeasurement> sInstances =
90         new ConcurrentHashMap<StorageVolume, StorageMeasurement>();
91
92     private volatile WeakReference<MeasurementReceiver> mReceiver;
93
94     private long mTotalSize;
95     private long mAvailSize;
96     private long mAppsSize;
97     private long mDownloadsSize;
98     private long mMiscSize;
99     private long[] mMediaSizes = new long[StorageVolumePreferenceCategory.sMediaCategories.length];
100
101     final private StorageVolume mStorageVolume;
102     final private boolean mIsPrimary;
103
104     List<FileInfo> mFileInfoForMisc;
105
106     public interface MeasurementReceiver {
107         public void updateApproximate(Bundle bundle);
108         public void updateExact(Bundle bundle);
109     }
110
111     private StorageMeasurement(Context context, StorageVolume storageVolume, boolean isPrimary) {
112         mStorageVolume = storageVolume;
113         mIsPrimary = isPrimary;
114
115         // Start the thread that will measure the disk usage.
116         final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
117         handlerThread.start();
118         mHandler = new MeasurementHandler(context, handlerThread.getLooper());
119     }
120
121     /**
122      * Get the singleton of the StorageMeasurement class. The application
123      * context is used to avoid leaking activities.
124      * @param storageVolume The {@link StorageVolume} that will be measured
125      * @param isPrimary true when this storage volume is the primary volume
126      */
127     public static StorageMeasurement getInstance(Context context, StorageVolume storageVolume,
128             boolean isPrimary) {
129         if (sInstances.containsKey(storageVolume)) {
130             return sInstances.get(storageVolume);
131         } else {
132             StorageMeasurement storageMeasurement =
133                 new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary);
134             sInstances.put(storageVolume, storageMeasurement);
135             return storageMeasurement;
136         }
137     }
138
139     public void setReceiver(MeasurementReceiver receiver) {
140         if (mReceiver == null || mReceiver.get() == null) {
141             mReceiver = new WeakReference<MeasurementReceiver>(receiver);
142         }
143     }
144
145     public void measure() {
146         if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
147             mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
148         }
149     }
150
151     public void cleanUp() {
152         mReceiver = null;
153         mHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
154         mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
155     }
156
157     public void invalidate() {
158         mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
159     }
160
161     private void sendInternalApproximateUpdate() {
162         MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
163         if (receiver == null) {
164             return;
165         }
166
167         Bundle bundle = new Bundle();
168         bundle.putLong(TOTAL_SIZE, mTotalSize);
169         bundle.putLong(AVAIL_SIZE, mAvailSize);
170
171         receiver.updateApproximate(bundle);
172     }
173
174     private void sendExactUpdate() {
175         MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
176         if (receiver == null) {
177             if (LOGV) {
178                 Log.i(TAG, "measurements dropped because receiver is null! wasted effort");
179             }
180             return;
181         }
182
183         Bundle bundle = new Bundle();
184         bundle.putLong(TOTAL_SIZE, mTotalSize);
185         bundle.putLong(AVAIL_SIZE, mAvailSize);
186         bundle.putLong(APPS_USED, mAppsSize);
187         bundle.putLong(DOWNLOADS_SIZE, mDownloadsSize);
188         bundle.putLong(MISC_SIZE, mMiscSize);
189         bundle.putLongArray(MEDIA_SIZES, mMediaSizes);
190
191         receiver.updateExact(bundle);
192     }
193
194     private class MeasurementHandler extends Handler {
195         public static final int MSG_MEASURE = 1;
196
197         public static final int MSG_CONNECTED = 2;
198
199         public static final int MSG_DISCONNECT = 3;
200
201         public static final int MSG_COMPLETED = 4;
202
203         public static final int MSG_INVALIDATE = 5;
204
205         private Object mLock = new Object();
206
207         private IMediaContainerService mDefaultContainer;
208
209         private volatile boolean mBound = false;
210
211         private volatile boolean mMeasured = false;
212
213         private StatsObserver mStatsObserver;
214
215         private final WeakReference<Context> mContext;
216
217         final private ServiceConnection mDefContainerConn = new ServiceConnection() {
218             public void onServiceConnected(ComponentName name, IBinder service) {
219                 final IMediaContainerService imcs = IMediaContainerService.Stub
220                 .asInterface(service);
221                 mDefaultContainer = imcs;
222                 mBound = true;
223                 sendMessage(obtainMessage(MSG_CONNECTED, imcs));
224             }
225
226             public void onServiceDisconnected(ComponentName name) {
227                 mBound = false;
228                 removeMessages(MSG_CONNECTED);
229             }
230         };
231
232         public MeasurementHandler(Context context, Looper looper) {
233             super(looper);
234             mContext = new WeakReference<Context>(context);
235         }
236
237         @Override
238         public void handleMessage(Message msg) {
239             switch (msg.what) {
240                 case MSG_MEASURE: {
241                     if (mMeasured) {
242                         sendExactUpdate();
243                         break;
244                     }
245
246                     final Context context = (mContext != null) ? mContext.get() : null;
247                     if (context == null) {
248                         return;
249                     }
250
251                     measureApproximateStorage();
252
253                     synchronized (mLock) {
254                         if (mBound) {
255                             removeMessages(MSG_DISCONNECT);
256                             sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer));
257                         } else {
258                             Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
259                             context.bindService(service, mDefContainerConn,
260                                     Context.BIND_AUTO_CREATE);
261                         }
262                     }
263                     break;
264                 }
265                 case MSG_CONNECTED: {
266                     IMediaContainerService imcs = (IMediaContainerService) msg.obj;
267                     measureExactStorage(imcs);
268                     break;
269                 }
270                 case MSG_DISCONNECT: {
271                     synchronized (mLock) {
272                         if (mBound) {
273                             final Context context = (mContext != null) ? mContext.get() : null;
274                             if (context == null) {
275                                 return;
276                             }
277
278                             mBound = false;
279                             context.unbindService(mDefContainerConn);
280                         }
281                     }
282                     break;
283                 }
284                 case MSG_COMPLETED: {
285                     mMeasured = true;
286                     sendExactUpdate();
287                     break;
288                 }
289                 case MSG_INVALIDATE: {
290                     mMeasured = false;
291                     break;
292                 }
293             }
294         }
295
296         /**
297          * Request measurement of each package.
298          *
299          * @param pm PackageManager instance to query
300          */
301         public void requestQueuedMeasurementsLocked(PackageManager pm) {
302             final String[] appsList = mStatsObserver.getAppsList();
303             final int N = appsList.length;
304             for (int i = 0; i < N; i++) {
305                 pm.getPackageSizeInfo(appsList[i], mStatsObserver);
306             }
307         }
308
309         private class StatsObserver extends IPackageStatsObserver.Stub {
310             private long mAppsSizeForThisStatsObserver = 0;
311             private final List<String> mAppsList = new ArrayList<String>();
312
313             public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
314                 if (!mStatsObserver.equals(this)) {
315                     // this callback's class object is no longer in use. ignore this callback.
316                     return;
317                 }
318
319                 if (succeeded) {
320                     mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize +
321                     stats.externalCacheSize + stats.externalDataSize +
322                     stats.externalMediaSize + stats.externalObbSize;
323                 }
324
325                 synchronized (mAppsList) {
326                     mAppsList.remove(stats.packageName);
327                     if (mAppsList.size() > 0) return;
328                 }
329
330                 mAppsSize = mAppsSizeForThisStatsObserver;
331                 onInternalMeasurementComplete();
332             }
333
334             public void queuePackageMeasurementLocked(String packageName) {
335                 synchronized (mAppsList) {
336                     mAppsList.add(packageName);
337                 }
338             }
339
340             public String[] getAppsList() {
341                 synchronized (mAppsList) {
342                     return mAppsList.toArray(new String[mAppsList.size()]);
343                 }
344             }
345         }
346
347         private void onInternalMeasurementComplete() {
348             sendEmptyMessage(MSG_COMPLETED);
349         }
350
351         private void measureApproximateStorage() {
352             final StatFs stat = new StatFs(mStorageVolume.getPath());
353             final long blockSize = stat.getBlockSize();
354             final long totalBlocks = stat.getBlockCount();
355             final long availableBlocks = stat.getAvailableBlocks();
356
357             mTotalSize = totalBlocks * blockSize;
358             mAvailSize = availableBlocks * blockSize;
359
360             sendInternalApproximateUpdate();
361         }
362
363         private void measureExactStorage(IMediaContainerService imcs) {
364             Context context = mContext != null ? mContext.get() : null;
365             if (context == null) {
366                 return;
367             }
368
369             // Media
370             for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) {
371                 if (mIsPrimary) {
372                     String[] dirs = StorageVolumePreferenceCategory.sMediaCategories[i].mDirPaths;
373                     final int length = dirs.length;
374                     mMediaSizes[i] = 0;
375                     for (int d = 0; d < length; d++) {
376                         final String path = dirs[d];
377                         mMediaSizes[i] += getDirectorySize(imcs, path);
378                     }
379                 } else {
380                     // TODO Compute sizes using the MediaStore
381                     mMediaSizes[i] = 0;
382                 }
383             }
384
385             /* Compute sizes using the media provider
386             // Media sizes are measured by the MediaStore. Query database.
387             ContentResolver contentResolver = context.getContentResolver();
388             // TODO "external" as a static String from MediaStore?
389             Uri audioUri = MediaStore.Files.getContentUri("external");
390             final String[] projection =
391                 new String[] { "sum(" + MediaStore.Files.FileColumns.SIZE + ")" };
392             final String selection =
393                 MediaStore.Files.FileColumns.STORAGE_ID + "=" +
394                 Integer.toString(mStorageVolume.getStorageId()) + " AND " +
395                 MediaStore.Files.FileColumns.MEDIA_TYPE + "=?";
396
397             for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) {
398                 mMediaSizes[i] = 0;
399                 int mediaType = StorageVolumePreferenceCategory.sMediaCategories[i].mediaType;
400                 Cursor c = null;
401                 try {
402                     c = contentResolver.query(audioUri, projection, selection,
403                             new String[] { Integer.toString(mediaType) } , null);
404
405                     if (c != null && c.moveToNext()) {
406                         long size = c.getLong(0);
407                         mMediaSizes[i] = size;
408                     }
409                 } finally {
410                     if (c != null) c.close();
411                 }
412             }
413              */
414
415             // Downloads (primary volume only)
416             if (mIsPrimary) {
417                 final String downloadsPath = Environment.getExternalStoragePublicDirectory(
418                         Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
419                 mDownloadsSize = getDirectorySize(imcs, downloadsPath);
420             } else {
421                 mDownloadsSize = 0;
422             }
423
424             // Misc
425             mMiscSize = 0;
426             if (mIsPrimary) {
427                 measureSizesOfMisc(imcs);
428             }
429
430             // Apps
431             // We have to get installd to measure the package sizes.
432             PackageManager pm = context.getPackageManager();
433             if (pm == null) {
434                 return;
435             }
436             final List<ApplicationInfo> apps;
437             if (mIsPrimary) {
438                 apps = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES |
439                         PackageManager.GET_DISABLED_COMPONENTS);
440             } else {
441                 // TODO also measure apps installed on the SD card
442                 apps = Collections.emptyList();
443             }
444
445             if (apps != null && apps.size() > 0) {
446                 // initiate measurement of all package sizes. need new StatsObserver object.
447                 mStatsObserver = new StatsObserver();
448                 synchronized (mStatsObserver.mAppsList) {
449                     for (int i = 0; i < apps.size(); i++) {
450                         final ApplicationInfo info = apps.get(i);
451                         mStatsObserver.queuePackageMeasurementLocked(info.packageName);
452                     }
453                 }
454
455                 requestQueuedMeasurementsLocked(pm);
456                 // Sending of the message back to the MeasurementReceiver is
457                 // completed in the PackageObserver
458             } else {
459                 onInternalMeasurementComplete();
460             }
461         }
462     }
463
464     private long getDirectorySize(IMediaContainerService imcs, String dir) {
465         try {
466             return imcs.calculateDirectorySize(dir);
467         } catch (Exception e) {
468             Log.w(TAG, "Could not read memory from default container service for " + dir, e);
469             return 0;
470         }
471     }
472
473     long getMiscSize() {
474         return mMiscSize;
475     }
476
477     private void measureSizesOfMisc(IMediaContainerService imcs) {
478         File top = new File(mStorageVolume.getPath());
479         mFileInfoForMisc = new ArrayList<FileInfo>();
480         File[] files = top.listFiles();
481         final int len = files.length;
482         // Get sizes of all top level nodes except the ones already computed...
483         long counter = 0;
484         for (int i = 0; i < len; i++) {
485             String path = files[i].getAbsolutePath();
486             if (StorageVolumePreferenceCategory.sPathsExcludedForMisc.contains(path)) {
487                 continue;
488             }
489             if (files[i].isFile()) {
490                 final long fileSize = files[i].length();
491                 mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++));
492                 mMiscSize += fileSize;
493             } else if (files[i].isDirectory()) {
494                 final long dirSize = getDirectorySize(imcs, path);
495                 mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++));
496                 mMiscSize += dirSize;
497             } else {
498                 // Non directory, non file: not listed
499             }
500         }
501         // sort the list of FileInfo objects collected above in descending order of their sizes
502         Collections.sort(mFileInfoForMisc);
503     }
504
505     static class FileInfo implements Comparable<FileInfo> {
506         final String mFileName;
507         final long mSize;
508         final long mId;
509
510         FileInfo(String fileName, long size, long id) {
511             mFileName = fileName;
512             mSize = size;
513             mId = id;
514         }
515
516         @Override
517         public int compareTo(FileInfo that) {
518             if (this == that || mSize == that.mSize) return 0;
519             else return (mSize < that.mSize) ? 1 : -1; // for descending sort
520         }
521
522         @Override
523         public String toString() {
524             return mFileName  + " : " + mSize + ", id:" + mId;
525         }
526     }
527 }