Merge "Move StorageMeasurement to SettingsLib"
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/StorageMeasurement.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/StorageMeasurement.java
new file mode 100644
index 0000000..e0af29d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/StorageMeasurement.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageStats;
+import android.content.pm.UserInfo;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+import android.util.Log;
+import android.util.SparseLongArray;
+
+import com.android.internal.app.IMediaContainerService;
+import com.android.internal.util.ArrayUtils;
+import com.google.android.collect.Sets;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility for measuring the disk usage of internal storage or a physical
+ * {@link StorageVolume}. Connects with a remote {@link IMediaContainerService}
+ * and delivers results to {@link MeasurementReceiver}.
+ */
+public class StorageMeasurement {
+    private static final String TAG = "StorageMeasurement";
+
+    private static final boolean LOCAL_LOGV = true;
+    static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE);
+
+    private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer";
+
+    public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName(
+            DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService");
+
+    /** Media types to measure on external storage. */
+    private static final Set<String> sMeasureMediaTypes = Sets.newHashSet(
+            Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
+            Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC,
+            Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
+            Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS,
+            Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID);
+
+    public static class MeasurementDetails {
+        /**
+         * Total apps disk usage.
+         * <p>
+         * When measuring internal storage, this value includes the code size of
+         * all apps (regardless of install status for current user), and
+         * internal disk used by the current user's apps. When the device
+         * emulates external storage, this value also includes emulated storage
+         * used by the current user's apps.
+         * <p>
+         * When measuring a physical {@link StorageVolume}, this value includes
+         * usage by all apps on that volume.
+         */
+        public long appsSize;
+
+        /**
+         * Total cache disk usage by apps.
+         */
+        public long cacheSize;
+
+        /**
+         * Total media disk usage, categorized by types such as
+         * {@link Environment#DIRECTORY_MUSIC}.
+         * <p>
+         * When measuring internal storage, this reflects media on emulated
+         * storage for the current user.
+         * <p>
+         * When measuring a physical {@link StorageVolume}, this reflects media
+         * on that volume.
+         */
+        public HashMap<String, Long> mediaSize = new HashMap<>();
+
+        /**
+         * Misc external disk usage for the current user, unaccounted in
+         * {@link #mediaSize}.
+         */
+        public long miscSize;
+
+        /**
+         * Total disk usage for users, which is only meaningful for emulated
+         * internal storage. Key is {@link UserHandle}.
+         */
+        public SparseLongArray usersSize = new SparseLongArray();
+    }
+
+    public interface MeasurementReceiver {
+        public void onDetailsChanged(MeasurementDetails details);
+    }
+
+    private WeakReference<MeasurementReceiver> mReceiver;
+
+    private final Context mContext;
+
+    private final VolumeInfo mVolume;
+    private final VolumeInfo mSharedVolume;
+
+    private final MainHandler mMainHandler;
+    private final MeasurementHandler mMeasurementHandler;
+
+    public StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume) {
+        mContext = context.getApplicationContext();
+
+        mVolume = volume;
+        mSharedVolume = sharedVolume;
+
+        // Start the thread that will measure the disk usage.
+        final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement");
+        handlerThread.start();
+
+        mMainHandler = new MainHandler();
+        mMeasurementHandler = new MeasurementHandler(handlerThread.getLooper());
+    }
+
+    public void setReceiver(MeasurementReceiver receiver) {
+        if (mReceiver == null || mReceiver.get() == null) {
+            mReceiver = new WeakReference<MeasurementReceiver>(receiver);
+        }
+    }
+
+    public void forceMeasure() {
+        invalidate();
+        measure();
+    }
+
+    public void measure() {
+        if (!mMeasurementHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) {
+            mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE);
+        }
+    }
+
+    public void onDestroy() {
+        mReceiver = null;
+        mMeasurementHandler.removeMessages(MeasurementHandler.MSG_MEASURE);
+        mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT);
+    }
+
+    private void invalidate() {
+        mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE);
+    }
+
+    private static class StatsObserver extends IPackageStatsObserver.Stub {
+        private final boolean mIsPrivate;
+        private final MeasurementDetails mDetails;
+        private final int mCurrentUser;
+        private final Message mFinished;
+
+        private int mRemaining;
+
+        public StatsObserver(boolean isPrivate, MeasurementDetails details, int currentUser,
+                Message finished, int remaining) {
+            mIsPrivate = isPrivate;
+            mDetails = details;
+            mCurrentUser = currentUser;
+            mFinished = finished;
+            mRemaining = remaining;
+        }
+
+        @Override
+        public void onGetStatsCompleted(PackageStats stats, boolean succeeded) {
+            synchronized (mDetails) {
+                if (succeeded) {
+                    addStatsLocked(stats);
+                }
+                if (--mRemaining == 0) {
+                    mFinished.sendToTarget();
+                }
+            }
+        }
+
+        private void addStatsLocked(PackageStats stats) {
+            if (mIsPrivate) {
+                long codeSize = stats.codeSize;
+                long dataSize = stats.dataSize;
+                long cacheSize = stats.cacheSize;
+                if (Environment.isExternalStorageEmulated()) {
+                    // Include emulated storage when measuring internal. OBB is
+                    // shared on emulated storage, so treat as code.
+                    codeSize += stats.externalCodeSize + stats.externalObbSize;
+                    dataSize += stats.externalDataSize + stats.externalMediaSize;
+                    cacheSize += stats.externalCacheSize;
+                }
+
+                // Count code and data for current user
+                if (stats.userHandle == mCurrentUser) {
+                    mDetails.appsSize += codeSize;
+                    mDetails.appsSize += dataSize;
+                }
+
+                // User summary only includes data (code is only counted once
+                // for the current user)
+                addValue(mDetails.usersSize, stats.userHandle, dataSize);
+
+                // Include cache for all users
+                mDetails.cacheSize += cacheSize;
+
+            } else {
+                // Physical storage; only count external sizes
+                mDetails.appsSize += stats.externalCodeSize + stats.externalDataSize
+                        + stats.externalMediaSize + stats.externalObbSize;
+                mDetails.cacheSize += stats.externalCacheSize;
+            }
+        }
+    }
+
+    private class MainHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            final MeasurementDetails details = (MeasurementDetails) msg.obj;
+            final MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
+            if (receiver != null) {
+                receiver.onDetailsChanged(details);
+            }
+        }
+    }
+
+    private class MeasurementHandler extends Handler {
+        public static final int MSG_MEASURE = 1;
+        public static final int MSG_CONNECTED = 2;
+        public static final int MSG_DISCONNECT = 3;
+        public static final int MSG_COMPLETED = 4;
+        public static final int MSG_INVALIDATE = 5;
+
+        private Object mLock = new Object();
+
+        private IMediaContainerService mDefaultContainer;
+
+        private volatile boolean mBound = false;
+
+        private MeasurementDetails mCached;
+
+        private final ServiceConnection mDefContainerConn = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(
+                        service);
+                mDefaultContainer = imcs;
+                mBound = true;
+                sendMessage(obtainMessage(MSG_CONNECTED, imcs));
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mBound = false;
+                removeMessages(MSG_CONNECTED);
+            }
+        };
+
+        public MeasurementHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_MEASURE: {
+                    if (mCached != null) {
+                        mMainHandler.obtainMessage(0, mCached).sendToTarget();
+                        break;
+                    }
+
+                    synchronized (mLock) {
+                        if (mBound) {
+                            removeMessages(MSG_DISCONNECT);
+                            sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer));
+                        } else {
+                            Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
+                            mContext.bindServiceAsUser(service, mDefContainerConn,
+                                    Context.BIND_AUTO_CREATE, UserHandle.OWNER);
+                        }
+                    }
+                    break;
+                }
+                case MSG_CONNECTED: {
+                    final IMediaContainerService imcs = (IMediaContainerService) msg.obj;
+                    measureExactStorage(imcs);
+                    break;
+                }
+                case MSG_DISCONNECT: {
+                    synchronized (mLock) {
+                        if (mBound) {
+                            mBound = false;
+                            mContext.unbindService(mDefContainerConn);
+                        }
+                    }
+                    break;
+                }
+                case MSG_COMPLETED: {
+                    mCached = (MeasurementDetails) msg.obj;
+                    mMainHandler.obtainMessage(0, mCached).sendToTarget();
+                    break;
+                }
+                case MSG_INVALIDATE: {
+                    mCached = null;
+                    break;
+                }
+            }
+        }
+    }
+
+    private void measureExactStorage(IMediaContainerService imcs) {
+        final UserManager userManager = mContext.getSystemService(UserManager.class);
+        final PackageManager packageManager = mContext.getPackageManager();
+
+        final List<UserInfo> users = userManager.getUsers();
+        final int currentUser = ActivityManager.getCurrentUser();
+
+        final MeasurementDetails details = new MeasurementDetails();
+        final Message finished = mMeasurementHandler.obtainMessage(MeasurementHandler.MSG_COMPLETED,
+                details);
+
+        if (mSharedVolume != null && mSharedVolume.isMountedReadable()) {
+            final File basePath = mSharedVolume.getPathForUser(currentUser);
+
+            // Measure media types for emulated storage, or for primary physical
+            // external volume
+            for (String type : sMeasureMediaTypes) {
+                final File path = new File(basePath, type);
+                final long size = getDirectorySize(imcs, path);
+                details.mediaSize.put(type, size);
+            }
+
+            // Measure misc files not counted under media
+            details.miscSize = measureMisc(imcs, basePath);
+
+            if (mSharedVolume.getType() == VolumeInfo.TYPE_EMULATED) {
+                // Measure total emulated storage of all users; internal apps data
+                // will be spliced in later
+                for (UserInfo user : users) {
+                    final File userPath = mSharedVolume.getPathForUser(user.id);
+                    final long size = getDirectorySize(imcs, userPath);
+                    addValue(details.usersSize, user.id, size);
+                }
+            }
+        }
+
+        // Measure all apps hosted on this volume for all users
+        if (mVolume.getType() == VolumeInfo.TYPE_PRIVATE) {
+            final List<ApplicationInfo> apps = packageManager.getInstalledApplications(
+                    PackageManager.GET_UNINSTALLED_PACKAGES
+                    | PackageManager.GET_DISABLED_COMPONENTS);
+
+            final List<ApplicationInfo> volumeApps = new ArrayList<>();
+            for (ApplicationInfo app : apps) {
+                if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) {
+                    volumeApps.add(app);
+                }
+            }
+
+            final int count = users.size() * volumeApps.size();
+            if (count == 0) {
+                finished.sendToTarget();
+                return;
+            }
+
+            final StatsObserver observer = new StatsObserver(
+                    true, details, currentUser, finished, count);
+            for (UserInfo user : users) {
+                for (ApplicationInfo app : volumeApps) {
+                    packageManager.getPackageSizeInfo(app.packageName, user.id, observer);
+                }
+            }
+
+        } else {
+            finished.sendToTarget();
+            return;
+        }
+    }
+
+    private static long getDirectorySize(IMediaContainerService imcs, File path) {
+        try {
+            final long size = imcs.calculateDirectorySize(path.toString());
+            Log.d(TAG, "getDirectorySize(" + path + ") returned " + size);
+            return size;
+        } catch (Exception e) {
+            Log.w(TAG, "Could not read memory from default container service for " + path, e);
+            return 0;
+        }
+    }
+
+    private long measureMisc(IMediaContainerService imcs, File dir) {
+        final File[] files = dir.listFiles();
+        if (ArrayUtils.isEmpty(files)) return 0;
+
+        // Get sizes of all top level nodes except the ones already computed
+        long miscSize = 0;
+        for (File file : files) {
+            final String name = file.getName();
+            if (sMeasureMediaTypes.contains(name)) {
+                continue;
+            }
+
+            if (file.isFile()) {
+                miscSize += file.length();
+            } else if (file.isDirectory()) {
+                miscSize += getDirectorySize(imcs, file);
+            }
+        }
+        return miscSize;
+    }
+
+    private static void addValue(SparseLongArray array, int key, long value) {
+        array.put(key, array.get(key) + value);
+    }
+}