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