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.settingslib.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.Handler;
31import android.os.HandlerThread;
32import android.os.IBinder;
33import android.os.Looper;
34import android.os.Message;
35import android.os.UserHandle;
36import android.os.UserManager;
37import android.os.storage.StorageVolume;
38import android.os.storage.VolumeInfo;
39import android.util.Log;
40import android.util.SparseArray;
41import android.util.SparseLongArray;
42
43import com.android.internal.app.IMediaContainerService;
44import com.android.internal.util.ArrayUtils;
45import com.google.android.collect.Sets;
46
47import java.io.File;
48import java.lang.ref.WeakReference;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.List;
52import java.util.Objects;
53import java.util.Set;
54
55/**
56 * Utility for measuring the disk usage of internal storage or a physical
57 * {@link StorageVolume}. Connects with a remote {@link IMediaContainerService}
58 * and delivers results to {@link MeasurementReceiver}.
59 */
60public class StorageMeasurement {
61    private static final String TAG = "StorageMeasurement";
62
63    private static final boolean LOCAL_LOGV = true;
64    static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE);
65
66    private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer";
67
68    public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName(
69            DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService");
70
71    /** Media types to measure on external storage. */
72    private static final Set<String> sMeasureMediaTypes = Sets.newHashSet(
73            Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
74            Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC,
75            Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
76            Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS,
77            Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID);
78
79    public static class MeasurementDetails {
80        public long totalSize;
81        public long availSize;
82
83        /**
84         * Total apps disk usage per profiles of the current user.
85         * <p>
86         * When measuring internal storage, this value includes the code size of
87         * all apps (regardless of install status for the given profile), and
88         * internal disk used by the profile's apps. When the device
89         * emulates external storage, this value also includes emulated storage
90         * used by the profile's apps.
91         * <p>
92         * When measuring a physical {@link StorageVolume}, this value includes
93         * usage by all apps on that volume and only for the primary profile.
94         * <p>
95         * Key is {@link UserHandle}.
96         */
97        public SparseLongArray appsSize = new SparseLongArray();
98
99        /**
100         * Total cache disk usage by apps (over all users and profiles).
101         */
102        public long cacheSize;
103
104        /**
105         * Total media disk usage, categorized by types such as
106         * {@link Environment#DIRECTORY_MUSIC} for every user profile of the current user.
107         * <p>
108         * When measuring internal storage, this reflects media on emulated
109         * storage for the respective profile.
110         * <p>
111         * When measuring a physical {@link StorageVolume}, this reflects media
112         * on that volume.
113         * <p>
114         * Key of the {@link SparseArray} is {@link UserHandle}.
115         */
116        public SparseArray<HashMap<String, Long>> mediaSize = new SparseArray<>();
117
118        /**
119         * Misc external disk usage for the current user's profiles, unaccounted in
120         * {@link #mediaSize}. Key is {@link UserHandle}.
121         */
122        public SparseLongArray miscSize = new SparseLongArray();
123
124        /**
125         * Total disk usage for users, which is only meaningful for emulated
126         * internal storage. Key is {@link UserHandle}.
127         */
128        public SparseLongArray usersSize = new SparseLongArray();
129    }
130
131    public interface MeasurementReceiver {
132        void onDetailsChanged(MeasurementDetails details);
133    }
134
135    private WeakReference<MeasurementReceiver> mReceiver;
136
137    private final Context mContext;
138
139    private final VolumeInfo mVolume;
140    private final VolumeInfo mSharedVolume;
141
142    private final MainHandler mMainHandler;
143    private final MeasurementHandler mMeasurementHandler;
144
145    public StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume) {
146        mContext = context.getApplicationContext();
147
148        mVolume = volume;
149        mSharedVolume = sharedVolume;
150
151        // Start the thread that will measure the disk usage.
152        final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
153        handlerThread.start();
154
155        mMainHandler = new MainHandler();
156        mMeasurementHandler = new MeasurementHandler(handlerThread.getLooper());
157    }
158
159    public void setReceiver(MeasurementReceiver receiver) {
160        if (mReceiver == null || mReceiver.get() == null) {
161            mReceiver = new WeakReference<MeasurementReceiver>(receiver);
162        }
163    }
164
165    public void forceMeasure() {
166        invalidate();
167        measure();
168    }
169
170    public void measure() {
171        if (!mMeasurementHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
172            mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
173        }
174    }
175
176    public void onDestroy() {
177        mReceiver = null;
178        mMeasurementHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
179        mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
180    }
181
182    private void invalidate() {
183        mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
184    }
185
186    private static class StatsObserver extends IPackageStatsObserver.Stub {
187        private final boolean mIsPrivate;
188        private final MeasurementDetails mDetails;
189        private final int mCurrentUser;
190        private final Message mFinished;
191
192        private int mRemaining;
193
194        public StatsObserver(boolean isPrivate, MeasurementDetails details, int currentUser,
195                List<UserInfo> profiles, Message finished, int remaining) {
196            mIsPrivate = isPrivate;
197            mDetails = details;
198            mCurrentUser = currentUser;
199            if (isPrivate) {
200                // Add the profile ids as keys to detail's app sizes.
201                for (UserInfo userInfo : profiles) {
202                    mDetails.appsSize.put(userInfo.id, 0);
203                }
204            }
205            mFinished = finished;
206            mRemaining = remaining;
207        }
208
209        @Override
210        public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
211            synchronized (mDetails) {
212                if (succeeded) {
213                    addStatsLocked(stats);
214                }
215                if (--mRemaining == 0) {
216                    mFinished.sendToTarget();
217                }
218            }
219        }
220
221        private void addStatsLocked(PackageStats stats) {
222            if (mIsPrivate) {
223                long codeSize = stats.codeSize;
224                long dataSize = stats.dataSize;
225                long cacheSize = stats.cacheSize;
226                if (Environment.isExternalStorageEmulated()) {
227                    // Include emulated storage when measuring internal. OBB is
228                    // shared on emulated storage, so treat as code.
229                    codeSize += stats.externalCodeSize + stats.externalObbSize;
230                    dataSize += stats.externalDataSize + stats.externalMediaSize;
231                    cacheSize += stats.externalCacheSize;
232                }
233
234                // Count code and data for current user's profiles (keys prepared in constructor)
235                addValueIfKeyExists(mDetails.appsSize, stats.userHandle, codeSize + dataSize);
236
237                // User summary only includes data (code is only counted once
238                // for the current user)
239                addValue(mDetails.usersSize, stats.userHandle, dataSize);
240
241                // Include cache for all users
242                mDetails.cacheSize += cacheSize;
243
244            } else {
245                // Physical storage; only count external sizes
246                addValue(mDetails.appsSize, mCurrentUser,
247                        stats.externalCodeSize + stats.externalDataSize
248                        + stats.externalMediaSize + stats.externalObbSize);
249                mDetails.cacheSize += stats.externalCacheSize;
250            }
251        }
252    }
253
254    private class MainHandler extends Handler {
255        @Override
256        public void handleMessage(Message msg) {
257            final MeasurementDetails details = (MeasurementDetails) msg.obj;
258            final MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
259            if (receiver != null) {
260                receiver.onDetailsChanged(details);
261            }
262        }
263    }
264
265    private class MeasurementHandler extends Handler {
266        public static final int MSG_MEASURE = 1;
267        public static final int MSG_CONNECTED = 2;
268        public static final int MSG_DISCONNECT = 3;
269        public static final int MSG_COMPLETED = 4;
270        public static final int MSG_INVALIDATE = 5;
271
272        private Object mLock = new Object();
273
274        private IMediaContainerService mDefaultContainer;
275
276        private volatile boolean mBound = false;
277
278        private MeasurementDetails mCached;
279
280        private final ServiceConnection mDefContainerConn = new ServiceConnection() {
281            @Override
282            public void onServiceConnected(ComponentName name, IBinder service) {
283                final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(
284                        service);
285                mDefaultContainer = imcs;
286                mBound = true;
287                sendMessage(obtainMessage(MSG_CONNECTED, imcs));
288            }
289
290            @Override
291            public void onServiceDisconnected(ComponentName name) {
292                mBound = false;
293                removeMessages(MSG_CONNECTED);
294            }
295        };
296
297        public MeasurementHandler(Looper looper) {
298            super(looper);
299        }
300
301        @Override
302        public void handleMessage(Message msg) {
303            switch (msg.what) {
304                case MSG_MEASURE: {
305                    if (mCached != null) {
306                        mMainHandler.obtainMessage(0, mCached).sendToTarget();
307                        break;
308                    }
309
310                    synchronized (mLock) {
311                        if (mBound) {
312                            removeMessages(MSG_DISCONNECT);
313                            sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer));
314                        } else {
315                            Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
316                            mContext.bindServiceAsUser(service, mDefContainerConn,
317                                    Context.BIND_AUTO_CREATE, UserHandle.OWNER);
318                        }
319                    }
320                    break;
321                }
322                case MSG_CONNECTED: {
323                    final IMediaContainerService imcs = (IMediaContainerService) msg.obj;
324                    measureExactStorage(imcs);
325                    break;
326                }
327                case MSG_DISCONNECT: {
328                    synchronized (mLock) {
329                        if (mBound) {
330                            mBound = false;
331                            mContext.unbindService(mDefContainerConn);
332                        }
333                    }
334                    break;
335                }
336                case MSG_COMPLETED: {
337                    mCached = (MeasurementDetails) msg.obj;
338                    mMainHandler.obtainMessage(0, mCached).sendToTarget();
339                    break;
340                }
341                case MSG_INVALIDATE: {
342                    mCached = null;
343                    break;
344                }
345            }
346        }
347    }
348
349    private void measureExactStorage(IMediaContainerService imcs) {
350        final UserManager userManager = mContext.getSystemService(UserManager.class);
351        final PackageManager packageManager = mContext.getPackageManager();
352
353        final List<UserInfo> users = userManager.getUsers();
354        final List<UserInfo> currentProfiles = userManager.getEnabledProfiles(
355                ActivityManager.getCurrentUser());
356
357        final MeasurementDetails details = new MeasurementDetails();
358        final Message finished = mMeasurementHandler.obtainMessage(MeasurementHandler.MSG_COMPLETED,
359                details);
360
361        if (mVolume == null || !mVolume.isMountedReadable()) {
362            finished.sendToTarget();
363            return;
364        }
365
366        if (mSharedVolume != null && mSharedVolume.isMountedReadable()) {
367            for (UserInfo currentUserInfo : currentProfiles) {
368                final int userId = currentUserInfo.id;
369                final File basePath = mSharedVolume.getPathForUser(userId);
370                HashMap<String, Long> mediaMap = new HashMap<>(sMeasureMediaTypes.size());
371                details.mediaSize.put(userId, mediaMap);
372
373                // Measure media types for emulated storage, or for primary physical
374                // external volume
375                for (String type : sMeasureMediaTypes) {
376                    final File path = new File(basePath, type);
377                    final long size = getDirectorySize(imcs, path);
378                    mediaMap.put(type, size);
379                }
380
381                // Measure misc files not counted under media
382                addValue(details.miscSize, userId, measureMisc(imcs, basePath));
383            }
384
385            if (mSharedVolume.getType() == VolumeInfo.TYPE_EMULATED) {
386                // Measure total emulated storage of all users; internal apps data
387                // will be spliced in later
388                for (UserInfo user : users) {
389                    final File userPath = mSharedVolume.getPathForUser(user.id);
390                    final long size = getDirectorySize(imcs, userPath);
391                    addValue(details.usersSize, user.id, size);
392                }
393            }
394        }
395
396        final File file = mVolume.getPath();
397        if (file != null) {
398            details.totalSize = file.getTotalSpace();
399            details.availSize = file.getFreeSpace();
400        }
401
402        // Measure all apps hosted on this volume for all users
403        if (mVolume.getType() == VolumeInfo.TYPE_PRIVATE) {
404            final List<ApplicationInfo> apps = packageManager.getInstalledApplications(
405                    PackageManager.GET_UNINSTALLED_PACKAGES
406                    | PackageManager.GET_DISABLED_COMPONENTS);
407
408            final List<ApplicationInfo> volumeApps = new ArrayList<>();
409            for (ApplicationInfo app : apps) {
410                if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) {
411                    volumeApps.add(app);
412                }
413            }
414
415            final int count = users.size() * volumeApps.size();
416            if (count == 0) {
417                finished.sendToTarget();
418                return;
419            }
420
421            final StatsObserver observer = new StatsObserver(true, details,
422                    ActivityManager.getCurrentUser(), currentProfiles, finished, count);
423            for (UserInfo user : users) {
424                for (ApplicationInfo app : volumeApps) {
425                    packageManager.getPackageSizeInfo(app.packageName, user.id, observer);
426                }
427            }
428
429        } else {
430            finished.sendToTarget();
431            return;
432        }
433    }
434
435    private static long getDirectorySize(IMediaContainerService imcs, File path) {
436        try {
437            final long size = imcs.calculateDirectorySize(path.toString());
438            Log.d(TAG, "getDirectorySize(" + path + ") returned " + size);
439            return size;
440        } catch (Exception e) {
441            Log.w(TAG, "Could not read memory from default container service for " + path, e);
442            return 0;
443        }
444    }
445
446    private long measureMisc(IMediaContainerService imcs, File dir) {
447        final File[] files = dir.listFiles();
448        if (ArrayUtils.isEmpty(files)) return 0;
449
450        // Get sizes of all top level nodes except the ones already computed
451        long miscSize = 0;
452        for (File file : files) {
453            final String name = file.getName();
454            if (sMeasureMediaTypes.contains(name)) {
455                continue;
456            }
457
458            if (file.isFile()) {
459                miscSize += file.length();
460            } else if (file.isDirectory()) {
461                miscSize += getDirectorySize(imcs, file);
462            }
463        }
464        return miscSize;
465    }
466
467    private static void addValue(SparseLongArray array, int key, long value) {
468        array.put(key, array.get(key) + value);
469    }
470
471    private static void addValueIfKeyExists(SparseLongArray array, int key, long value) {
472        final int index = array.indexOfKey(key);
473        if (index >= 0) {
474            array.put(key, array.valueAt(index) + value);
475        }
476    }
477}
478