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
17package com.android.settings.deviceinfo;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.IPackageStatsObserver;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageStats;
27import android.os.Bundle;
28import android.os.Environment;
29import android.os.Handler;
30import android.os.HandlerThread;
31import android.os.IBinder;
32import android.os.Looper;
33import android.os.Message;
34import android.os.StatFs;
35import android.os.storage.StorageVolume;
36import android.util.Log;
37
38import com.android.internal.app.IMediaContainerService;
39
40import java.io.File;
41import java.lang.ref.WeakReference;
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.List;
45import java.util.Map;
46import 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 */
64public 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    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                    measureApproximateStorage();
262
263                    synchronized (mLock) {
264                        if (mBound) {
265                            removeMessages(MSG_DISCONNECT);
266                            sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer));
267                        } else {
268                            Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
269                            context.bindService(service, mDefContainerConn,
270                                    Context.BIND_AUTO_CREATE);
271                        }
272                    }
273                    break;
274                }
275                case MSG_CONNECTED: {
276                    IMediaContainerService imcs = (IMediaContainerService) msg.obj;
277                    measureExactStorage(imcs);
278                    break;
279                }
280                case MSG_DISCONNECT: {
281                    synchronized (mLock) {
282                        if (mBound) {
283                            final Context context = (mContext != null) ? mContext.get() : null;
284                            if (context == null) {
285                                return;
286                            }
287
288                            mBound = false;
289                            context.unbindService(mDefContainerConn);
290                        }
291                    }
292                    break;
293                }
294                case MSG_COMPLETED: {
295                    mMeasured = true;
296                    sendExactUpdate();
297                    break;
298                }
299                case MSG_INVALIDATE: {
300                    mMeasured = false;
301                    break;
302                }
303            }
304        }
305
306        /**
307         * Request measurement of each package.
308         *
309         * @param pm PackageManager instance to query
310         */
311        public void requestQueuedMeasurementsLocked(PackageManager pm) {
312            final String[] appsList = mStatsObserver.getAppsList();
313            final int N = appsList.length;
314            for (int i = 0; i < N; i++) {
315                pm.getPackageSizeInfo(appsList[i], mStatsObserver);
316            }
317        }
318
319        private class StatsObserver extends IPackageStatsObserver.Stub {
320            private long mAppsSizeForThisStatsObserver = 0;
321            private final List<String> mAppsList = new ArrayList<String>();
322
323            public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
324                if (!mStatsObserver.equals(this)) {
325                    // this callback's class object is no longer in use. ignore this callback.
326                    return;
327                }
328
329                if (succeeded) {
330                    if (mIsInternal) {
331                        mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize;
332                    } else if (!Environment.isExternalStorageEmulated()) {
333                        mAppsSizeForThisStatsObserver += stats.externalObbSize +
334                                stats.externalCodeSize + stats.externalDataSize +
335                                stats.externalCacheSize + stats.externalMediaSize;
336                    } else {
337                        mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize +
338                                stats.externalCodeSize + stats.externalDataSize +
339                                stats.externalCacheSize + stats.externalMediaSize +
340                                stats.externalObbSize;
341                    }
342                }
343
344                synchronized (mAppsList) {
345                    mAppsList.remove(stats.packageName);
346                    if (mAppsList.size() > 0) return;
347                }
348
349                mAppsSize = mAppsSizeForThisStatsObserver;
350                onInternalMeasurementComplete();
351            }
352
353            public void queuePackageMeasurementLocked(String packageName) {
354                synchronized (mAppsList) {
355                    mAppsList.add(packageName);
356                }
357            }
358
359            public String[] getAppsList() {
360                synchronized (mAppsList) {
361                    return mAppsList.toArray(new String[mAppsList.size()]);
362                }
363            }
364        }
365
366        private void onInternalMeasurementComplete() {
367            sendEmptyMessage(MSG_COMPLETED);
368        }
369
370        private void measureApproximateStorage() {
371            final StatFs stat = new StatFs(mStorageVolume != null
372                    ? mStorageVolume.getPath() : Environment.getDataDirectory().getPath());
373            final long blockSize = stat.getBlockSize();
374            final long totalBlocks = stat.getBlockCount();
375            final long availableBlocks = stat.getAvailableBlocks();
376
377            mTotalSize = totalBlocks * blockSize;
378            mAvailSize = availableBlocks * blockSize;
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