/*
 * 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.SparseArray;
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 {
        public long totalSize;
        public long availSize;

        /**
         * Total apps disk usage per profiles of the current user.
         * <p>
         * When measuring internal storage, this value includes the code size of
         * all apps (regardless of install status for the given profile), and
         * internal disk used by the profile's apps. When the device
         * emulates external storage, this value also includes emulated storage
         * used by the profile's apps.
         * <p>
         * When measuring a physical {@link StorageVolume}, this value includes
         * usage by all apps on that volume and only for the primary profile.
         * <p>
         * Key is {@link UserHandle}.
         */
        public SparseLongArray appsSize = new SparseLongArray();

        /**
         * Total cache disk usage by apps (over all users and profiles).
         */
        public long cacheSize;

        /**
         * Total media disk usage, categorized by types such as
         * {@link Environment#DIRECTORY_MUSIC} for every user profile of the current user.
         * <p>
         * When measuring internal storage, this reflects media on emulated
         * storage for the respective profile.
         * <p>
         * When measuring a physical {@link StorageVolume}, this reflects media
         * on that volume.
         * <p>
         * Key of the {@link SparseArray} is {@link UserHandle}.
         */
        public SparseArray<HashMap<String, Long>> mediaSize = new SparseArray<>();

        /**
         * Misc external disk usage for the current user's profiles, unaccounted in
         * {@link #mediaSize}. Key is {@link UserHandle}.
         */
        public SparseLongArray miscSize = new SparseLongArray();

        /**
         * 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 {
        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,
                List<UserInfo> profiles, Message finished, int remaining) {
            mIsPrivate = isPrivate;
            mDetails = details;
            mCurrentUser = currentUser;
            if (isPrivate) {
                // Add the profile ids as keys to detail's app sizes.
                for (UserInfo userInfo : profiles) {
                    mDetails.appsSize.put(userInfo.id, 0);
                }
            }
            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's profiles (keys prepared in constructor)
                addValueIfKeyExists(mDetails.appsSize, stats.userHandle, codeSize + 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
                addValue(mDetails.appsSize, mCurrentUser,
                        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 List<UserInfo> currentProfiles = userManager.getEnabledProfiles(
                ActivityManager.getCurrentUser());

        final MeasurementDetails details = new MeasurementDetails();
        final Message finished = mMeasurementHandler.obtainMessage(MeasurementHandler.MSG_COMPLETED,
                details);

        if (mVolume == null || !mVolume.isMountedReadable()) {
            finished.sendToTarget();
            return;
        }

        if (mSharedVolume != null && mSharedVolume.isMountedReadable()) {
            for (UserInfo currentUserInfo : currentProfiles) {
                final int userId = currentUserInfo.id;
                final File basePath = mSharedVolume.getPathForUser(userId);
                HashMap<String, Long> mediaMap = new HashMap<>(sMeasureMediaTypes.size());
                details.mediaSize.put(userId, mediaMap);

                // 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);
                    mediaMap.put(type, size);
                }

                // Measure misc files not counted under media
                addValue(details.miscSize, userId, 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);
                }
            }
        }

        final File file = mVolume.getPath();
        if (file != null) {
            details.totalSize = file.getTotalSpace();
            details.availSize = file.getFreeSpace();
        }

        // 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,
                    ActivityManager.getCurrentUser(), currentProfiles, 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);
    }

    private static void addValueIfKeyExists(SparseLongArray array, int key, long value) {
        final int index = array.indexOfKey(key);
        if (index >= 0) {
            array.put(key, array.valueAt(index) + value);
        }
    }
}
