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