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.app.ActivityManager;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.ServiceConnection;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.IPackageStatsObserver;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageStats;
28import android.content.pm.UserInfo;
29import android.os.Environment;
30import android.os.Environment.UserEnvironment;
31import android.os.Handler;
32import android.os.HandlerThread;
33import android.os.IBinder;
34import android.os.Looper;
35import android.os.Message;
36import android.os.UserHandle;
37import android.os.UserManager;
38import android.os.storage.StorageVolume;
39import android.util.Log;
40import android.util.SparseLongArray;
41
42import com.android.internal.app.IMediaContainerService;
43import com.google.android.collect.Maps;
44import com.google.common.collect.Sets;
45
46import java.io.File;
47import java.lang.ref.WeakReference;
48import java.util.ArrayList;
49import java.util.Collections;
50import java.util.HashMap;
51import java.util.List;
52import java.util.Set;
53
54import 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 */
61public 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.bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE,
342                                    UserHandle.USER_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 (mIsInternal || mIsPrimary) {
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