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.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      */
     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 {
    147         public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize);
    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 
    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 
    177     public void setReceiver(MeasurementReceiver receiver) {
    178         if (mReceiver == null || mReceiver.get() == null) {
    179             mReceiver = new WeakReference<MeasurementReceiver>(receiver);
    180         }
    181     }
    182 
    183     public void measure() {
    184         if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
    185             mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
    186         }
    187     }
    188 
    189     public void cleanUp() {
    190         mReceiver = null;
    191         mHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
    192         mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
    193     }
    194 
    195     public void invalidate() {
    196         mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
    197     }
    198 
    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 
    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 
    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
    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 
    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 
    316         public MeasurementHandler(Context context, Looper looper) {
    317             super(looper);
    318             mContext = new WeakReference<Context>(context);
    319         }
    320 
    321         @Override
    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 
    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 
    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 
    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 
    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 
    514         FileInfo(String fileName, long size, long id) {
    515             mFileName = fileName;
    516             mSize = size;
    517             mId = id;
    518         }
    519 
    520         @Override
    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
    527         public String toString() {
    528             return mFileName  + " : " + mSize + ", id:" + mId;
    529         }
    530     }
    531 
    532     private static void addValue(SparseLongArray array, int key, long value) {
    533         array.put(key, array.get(key) + value);
    534     }
    535 }
    536