/**
 * Copyright (C) 2014 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.server.usage;

import android.app.usage.TimeSparseArray;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManager;
import android.os.Build;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;

import libcore.io.IoUtils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * Provides an interface to query for UsageStat data from a Protocol Buffer database.
 *
 * Prior to version 4, UsageStatsDatabase used XML to store Usage Stats data to disk.
 * When the UsageStatsDatabase version is upgraded, the files on disk are migrated to the new
 * version on init. The steps of migration are as follows:
 * 1) Check if version upgrade breadcrumb exists on disk, if so skip to step 4.
 * 2) Move current files to a timestamped backup directory.
 * 3) Write a temporary breadcrumb file with some info about the backup directory.
 * 4) Deserialize the backup files in the timestamped backup folder referenced by the breadcrumb.
 * 5) Reserialize the data read from the file with the new version format and replace the old files
 * 6) Repeat Step 3 and 4 for each file in the backup folder.
 * 7) Update the version file with the new version and build fingerprint.
 * 8) Delete the time stamped backup folder (unless flagged to be kept).
 * 9) Delete the breadcrumb file.
 *
 * Performing the upgrade steps in this order, protects against unexpected shutdowns mid upgrade
 *
 * The backup directory will contain directories with timestamp names. If the upgrade breadcrumb
 * exists on disk, it will contain a timestamp which will match one of the backup directories. The
 * breadcrumb will also contain a version number which will denote how the files in the backup
 * directory should be deserialized.
 */
public class UsageStatsDatabase {
    private static final int DEFAULT_CURRENT_VERSION = 5;
    /**
     * Current version of the backup schema
     *
     * @hide
     */
    @VisibleForTesting
    public static final int BACKUP_VERSION = 4;

    @VisibleForTesting
    static final int[] MAX_FILES_PER_INTERVAL_TYPE = new int[]{100, 50, 12, 10};

    // Key under which the payload blob is stored
    // same as UsageStatsBackupHelper.KEY_USAGE_STATS
    static final String KEY_USAGE_STATS = "usage_stats";

    // Persist versioned backup files.
    // Should be false, except when testing new versions
    static final boolean KEEP_BACKUP_DIR = false;

    private static final String TAG = "UsageStatsDatabase";
    private static final boolean DEBUG = UsageStatsService.DEBUG;
    private static final String BAK_SUFFIX = ".bak";
    private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX;
    private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention";
    private static final int SELECTION_LOG_RETENTION_LEN =
            SystemProperties.getInt(RETENTION_LEN_KEY, 14);

    private final Object mLock = new Object();
    private final File[] mIntervalDirs;
    @VisibleForTesting
    final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
    private final UnixCalendar mCal;
    private final File mVersionFile;
    private final File mBackupsDir;
    // If this file exists on disk, UsageStatsDatabase is in the middle of migrating files to a new
    // version. If this file exists on boot, the upgrade was interrupted and needs to be picked up
    // where it left off.
    private final File mUpdateBreadcrumb;
    // Current version of the database files schema
    private int mCurrentVersion;
    private boolean mFirstUpdate;
    private boolean mNewUpdate;
    private boolean mUpgradePerformed;

    // The obfuscated packages to tokens mappings file
    private final File mPackageMappingsFile;
    // Holds all of the data related to the obfuscated packages and their token mappings.
    final PackagesTokenData mPackagesTokenData = new PackagesTokenData();

    /**
     * UsageStatsDatabase constructor that allows setting the version number.
     * This should only be used for testing.
     *
     * @hide
     */
    @VisibleForTesting
    public UsageStatsDatabase(File dir, int version) {
        mIntervalDirs = new File[]{
            new File(dir, "daily"),
            new File(dir, "weekly"),
            new File(dir, "monthly"),
            new File(dir, "yearly"),
        };
        mCurrentVersion = version;
        mVersionFile = new File(dir, "version");
        mBackupsDir = new File(dir, "backups");
        mUpdateBreadcrumb = new File(dir, "breadcrumb");
        mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
        mPackageMappingsFile = new File(dir, "mappings");
        mCal = new UnixCalendar(0);
    }

    public UsageStatsDatabase(File dir) {
        this(dir, DEFAULT_CURRENT_VERSION);
    }

    /**
     * Initialize any directories required and index what stats are available.
     */
    public void init(long currentTimeMillis) {
        synchronized (mLock) {
            for (File f : mIntervalDirs) {
                f.mkdirs();
                if (!f.exists()) {
                    throw new IllegalStateException("Failed to create directory "
                            + f.getAbsolutePath());
                }
            }

            checkVersionAndBuildLocked();
            indexFilesLocked();

            // Delete files that are in the future.
            for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
                final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis);
                if (startIndex < 0) {
                    continue;
                }

                final int fileCount = files.size();
                for (int i = startIndex; i < fileCount; i++) {
                    files.valueAt(i).delete();
                }

                // Remove in a separate loop because any accesses (valueAt)
                // will cause a gc in the SparseArray and mess up the order.
                for (int i = startIndex; i < fileCount; i++) {
                    files.removeAt(i);
                }
            }
        }
    }

    public interface CheckinAction {
        boolean checkin(IntervalStats stats);
    }

    /**
     * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction}
     * for all {@link IntervalStats} that haven't been checked-in.
     * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws
     * an exception, the check-in will be aborted.
     *
     * @param checkinAction The callback to run when checking-in {@link IntervalStats}.
     * @return true if the check-in succeeded.
     */
    public boolean checkinDailyFiles(CheckinAction checkinAction) {
        synchronized (mLock) {
            final TimeSparseArray<AtomicFile> files =
                    mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY];
            final int fileCount = files.size();

            // We may have holes in the checkin (if there was an error)
            // so find the last checked-in file and go from there.
            int lastCheckin = -1;
            for (int i = 0; i < fileCount - 1; i++) {
                if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) {
                    lastCheckin = i;
                }
            }

            final int start = lastCheckin + 1;
            if (start == fileCount - 1) {
                return true;
            }

            try {
                for (int i = start; i < fileCount - 1; i++) {
                    final IntervalStats stats = new IntervalStats();
                    readLocked(files.valueAt(i), stats);
                    if (!checkinAction.checkin(stats)) {
                        return false;
                    }
                }
            } catch (Exception e) {
                Slog.e(TAG, "Failed to check-in", e);
                return false;
            }

            // We have successfully checked-in the stats, so rename the files so that they
            // are marked as checked-in.
            for (int i = start; i < fileCount - 1; i++) {
                final AtomicFile file = files.valueAt(i);
                final File checkedInFile = new File(
                        file.getBaseFile().getPath() + CHECKED_IN_SUFFIX);
                if (!file.getBaseFile().renameTo(checkedInFile)) {
                    // We must return success, as we've already marked some files as checked-in.
                    // It's better to repeat ourselves than to lose data.
                    Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath()
                            + " as checked-in");
                    return true;
                }

                // AtomicFile needs to set a new backup path with the same -c extension, so
                // we replace the old AtomicFile with the updated one.
                files.setValueAt(i, new AtomicFile(checkedInFile));
            }
        }
        return true;
    }

    /** @hide */
    @VisibleForTesting
    void forceIndexFiles() {
        synchronized (mLock) {
            indexFilesLocked();
        }
    }

    private void indexFilesLocked() {
        final FilenameFilter backupFileFilter = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return !name.endsWith(BAK_SUFFIX);
            }
        };
        // Index the available usage stat files on disk.
        for (int i = 0; i < mSortedStatFiles.length; i++) {
            if (mSortedStatFiles[i] == null) {
                mSortedStatFiles[i] = new TimeSparseArray<>();
            } else {
                mSortedStatFiles[i].clear();
            }
            File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
            if (files != null) {
                if (DEBUG) {
                    Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
                }
                final int len = files.length;
                for (int j = 0; j < len; j++) {
                    final File f = files[j];
                    final AtomicFile af = new AtomicFile(f);
                    try {
                        mSortedStatFiles[i].put(parseBeginTime(af), af);
                    } catch (IOException e) {
                        Slog.e(TAG, "failed to index file: " + f, e);
                    }
                }

                // only keep the max allowed number of files for each interval type.
                final int toDelete = mSortedStatFiles[i].size() - MAX_FILES_PER_INTERVAL_TYPE[i];
                if (toDelete > 0) {
                    for (int j = 0; j < toDelete; j++) {
                        mSortedStatFiles[i].valueAt(0).delete();
                        mSortedStatFiles[i].removeAt(0);
                    }
                    Slog.d(TAG, "Deleted " + toDelete + " stat files for interval " + i);
                }
            }
        }
    }

    /**
     * Is this the first update to the system from L to M?
     */
    boolean isFirstUpdate() {
        return mFirstUpdate;
    }

    /**
     * Is this a system update since we started tracking build fingerprint in the version file?
     */
    boolean isNewUpdate() {
        return mNewUpdate;
    }

    /**
     * Was an upgrade performed when this database was initialized?
     */
    boolean wasUpgradePerformed() {
        return mUpgradePerformed;
    }

    private void checkVersionAndBuildLocked() {
        int version;
        String buildFingerprint;
        String currentFingerprint = getBuildFingerprint();
        mFirstUpdate = true;
        mNewUpdate = true;
        try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
            version = Integer.parseInt(reader.readLine());
            buildFingerprint = reader.readLine();
            if (buildFingerprint != null) {
                mFirstUpdate = false;
            }
            if (currentFingerprint.equals(buildFingerprint)) {
                mNewUpdate = false;
            }
        } catch (NumberFormatException | IOException e) {
            version = 0;
        }

        if (version != mCurrentVersion) {
            Slog.i(TAG, "Upgrading from version " + version + " to " + mCurrentVersion);
            if (!mUpdateBreadcrumb.exists()) {
                try {
                    doUpgradeLocked(version);
                } catch (Exception e) {
                    Slog.e(TAG,
                            "Failed to upgrade from version " + version + " to " + mCurrentVersion,
                            e);
                    // Fallback to previous version.
                    mCurrentVersion = version;
                    return;
                }
            } else {
                Slog.i(TAG, "Version upgrade breadcrumb found on disk! Continuing version upgrade");
            }
        }

        if (mUpdateBreadcrumb.exists()) {
            int previousVersion;
            long token;
            try (BufferedReader reader = new BufferedReader(
                    new FileReader(mUpdateBreadcrumb))) {
                token = Long.parseLong(reader.readLine());
                previousVersion = Integer.parseInt(reader.readLine());
            } catch (NumberFormatException | IOException e) {
                Slog.e(TAG, "Failed read version upgrade breadcrumb");
                throw new RuntimeException(e);
            }
            if (mCurrentVersion >= 4) {
                continueUpgradeLocked(previousVersion, token);
            } else {
                Slog.wtf(TAG, "Attempting to upgrade to an unsupported version: "
                        + mCurrentVersion);
            }
        }

        if (version != mCurrentVersion || mNewUpdate) {
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
                writer.write(Integer.toString(mCurrentVersion));
                writer.write("\n");
                writer.write(currentFingerprint);
                writer.write("\n");
                writer.flush();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to write new version");
                throw new RuntimeException(e);
            }
        }

        if (mUpdateBreadcrumb.exists()) {
            // Files should be up to date with current version. Clear the version update breadcrumb
            mUpdateBreadcrumb.delete();
            // update mUpgradePerformed after breadcrumb is deleted to indicate a successful upgrade
            mUpgradePerformed = true;
        }

        if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) {
            mUpgradePerformed = true; // updated here to ensure that data is cleaned up
            deleteDirectory(mBackupsDir);
        }
    }

    private String getBuildFingerprint() {
        return Build.VERSION.RELEASE + ";"
                + Build.VERSION.CODENAME + ";"
                + Build.VERSION.INCREMENTAL;
    }

    private void doUpgradeLocked(int thisVersion) {
        if (thisVersion < 2) {
            // Delete all files if we are version 0. This is a pre-release version,
            // so this is fine.
            Slog.i(TAG, "Deleting all usage stats files");
            for (int i = 0; i < mIntervalDirs.length; i++) {
                File[] files = mIntervalDirs[i].listFiles();
                if (files != null) {
                    for (File f : files) {
                        f.delete();
                    }
                }
            }
        } else {
            // Create a dir in backups based on current timestamp
            final long token = System.currentTimeMillis();
            final File backupDir = new File(mBackupsDir, Long.toString(token));
            backupDir.mkdirs();
            if (!backupDir.exists()) {
                throw new IllegalStateException(
                        "Failed to create backup directory " + backupDir.getAbsolutePath());
            }
            try {
                Files.copy(mVersionFile.toPath(),
                        new File(backupDir, mVersionFile.getName()).toPath(),
                        StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                Slog.e(TAG, "Failed to back up version file : " + mVersionFile.toString());
                throw new RuntimeException(e);
            }

            for (int i = 0; i < mIntervalDirs.length; i++) {
                final File backupIntervalDir = new File(backupDir, mIntervalDirs[i].getName());
                backupIntervalDir.mkdir();

                if (!backupIntervalDir.exists()) {
                    throw new IllegalStateException(
                            "Failed to create interval backup directory "
                                    + backupIntervalDir.getAbsolutePath());
                }
                File[] files = mIntervalDirs[i].listFiles();
                if (files != null) {
                    for (int j = 0; j < files.length; j++) {
                        final File backupFile = new File(backupIntervalDir, files[j].getName());
                        if (DEBUG) {
                            Slog.d(TAG, "Creating versioned (" + Integer.toString(thisVersion)
                                    + ") backup of " + files[j].toString()
                                    + " stat files for interval "
                                    + i + " to " + backupFile.toString());
                        }

                        try {
                            // Backup file should not already exist, but make sure it doesn't
                            Files.move(files[j].toPath(), backupFile.toPath(),
                                    StandardCopyOption.REPLACE_EXISTING);
                        } catch (IOException e) {
                            Slog.e(TAG, "Failed to back up file : " + files[j].toString());
                            throw new RuntimeException(e);
                        }
                    }
                }
            }

            // Leave a breadcrumb behind noting that all the usage stats have been moved to a backup
            BufferedWriter writer = null;
            try {
                writer = new BufferedWriter(new FileWriter(mUpdateBreadcrumb));
                writer.write(Long.toString(token));
                writer.write("\n");
                writer.write(Integer.toString(thisVersion));
                writer.write("\n");
                writer.flush();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to write new version upgrade breadcrumb");
                throw new RuntimeException(e);
            } finally {
                IoUtils.closeQuietly(writer);
            }
        }
    }

    private void continueUpgradeLocked(int version, long token) {
        if (version <= 3) {
            Slog.w(TAG, "Reading UsageStats as XML; current database version: " + mCurrentVersion);
        }
        final File backupDir = new File(mBackupsDir, Long.toString(token));

        // Upgrade step logic for the entire usage stats directory, not individual interval dirs.
        if (version >= 5) {
            readMappingsLocked();
        }

        // Read each file in the backup according to the version and write to the interval
        // directories in the current versions format
        for (int i = 0; i < mIntervalDirs.length; i++) {
            final File backedUpInterval = new File(backupDir, mIntervalDirs[i].getName());
            File[] files = backedUpInterval.listFiles();
            if (files != null) {
                for (int j = 0; j < files.length; j++) {
                    if (DEBUG) {
                        Slog.d(TAG,
                                "Upgrading " + files[j].toString() + " to version ("
                                        + Integer.toString(
                                        mCurrentVersion) + ") for interval " + i);
                    }
                    try {
                        IntervalStats stats = new IntervalStats();
                        readLocked(new AtomicFile(files[j]), stats, version, mPackagesTokenData);
                        // Upgrade to version 5+.
                        // Future version upgrades should add additional logic here to upgrade.
                        if (mCurrentVersion >= 5) {
                            // Create the initial obfuscated packages map.
                            stats.obfuscateData(mPackagesTokenData);
                        }
                        writeLocked(new AtomicFile(new File(mIntervalDirs[i],
                                Long.toString(stats.beginTime))), stats, mCurrentVersion,
                                mPackagesTokenData);
                    } catch (Exception e) {
                        // This method is called on boot, log the exception and move on
                        Slog.e(TAG, "Failed to upgrade backup file : " + files[j].toString());
                    }
                }
            }
        }

        // Upgrade step logic for the entire usage stats directory, not individual interval dirs.
        if (mCurrentVersion >= 5) {
            try {
                writeMappingsLocked();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to write the tokens mappings file.");
            }
        }
    }

    /**
     * Returns the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN}
     * if not mapped.
     */
    int onPackageRemoved(String packageName, long timeRemoved) {
        synchronized (mLock) {
            final int tokenRemoved = mPackagesTokenData.removePackage(packageName, timeRemoved);
            try {
                writeMappingsLocked();
            } catch (Exception e) {
                Slog.w(TAG, "Unable to update package mappings on disk after removing token "
                        + tokenRemoved);
            }
            return tokenRemoved;
        }
    }

    /**
     * Reads all the usage stats data on disk and rewrites it with any data related to uninstalled
     * packages omitted. Returns {@code true} on success, {@code false} otherwise.
     */
    boolean pruneUninstalledPackagesData() {
        synchronized (mLock) {
            for (int i = 0; i < mIntervalDirs.length; i++) {
                final File[] files = mIntervalDirs[i].listFiles();
                if (files == null) {
                    continue;
                }
                for (int j = 0; j < files.length; j++) {
                    try {
                        final IntervalStats stats = new IntervalStats();
                        final AtomicFile atomicFile = new AtomicFile(files[j]);
                        if (!readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData)) {
                            continue; // no data was omitted when read so no need to rewrite
                        }
                        // Any data related to packages that have been removed would have failed
                        // the deobfuscation step on read so the IntervalStats object here only
                        // contains data for packages that are currently installed - all we need
                        // to do here is write the data back to disk.
                        writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
                    } catch (Exception e) {
                        Slog.e(TAG, "Failed to prune data from: " + files[j].toString());
                        return false;
                    }
                }
            }

            try {
                writeMappingsLocked();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to write package mappings after pruning data.");
                return false;
            }
            return true;
        }
    }

    /**
     * Iterates through all the files on disk and prunes any data that belongs to packages that have
     * been uninstalled (packages that are not in the given list).
     * Note: this should only be called once, when there has been a database upgrade.
     *
     * @param installedPackages map of installed packages (package_name:package_install_time)
     */
    void prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages) {
        if (ArrayUtils.isEmpty(installedPackages)) {
            return;
        }
        synchronized (mLock) {
            for (int i = 0; i < mIntervalDirs.length; i++) {
                final File[] files = mIntervalDirs[i].listFiles();
                if (files == null) {
                    continue;
                }
                for (int j = 0; j < files.length; j++) {
                    try {
                        final IntervalStats stats = new IntervalStats();
                        final AtomicFile atomicFile = new AtomicFile(files[j]);
                        readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
                        if (!pruneStats(installedPackages, stats)) {
                            continue; // no stats were pruned so no need to rewrite
                        }
                        writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
                    } catch (Exception e) {
                        Slog.e(TAG, "Failed to prune data from: " + files[j].toString());
                    }
                }
            }
        }
    }

    private boolean pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats) {
        boolean dataPruned = false;

        // prune old package usage stats
        for (int i = stats.packageStats.size() - 1; i >= 0; i--) {
            final UsageStats usageStats = stats.packageStats.valueAt(i);
            final Long timeInstalled = installedPackages.get(usageStats.mPackageName);
            if (timeInstalled == null || timeInstalled > usageStats.mEndTimeStamp) {
                stats.packageStats.removeAt(i);
                dataPruned = true;
            }
        }
        if (dataPruned) {
            // ensure old stats don't linger around during the obfuscation step on write
            stats.packageStatsObfuscated.clear();
        }

        // prune old events
        for (int i = stats.events.size() - 1; i >= 0; i--) {
            final UsageEvents.Event event = stats.events.get(i);
            final Long timeInstalled = installedPackages.get(event.mPackage);
            if (timeInstalled == null || timeInstalled > event.mTimeStamp) {
                stats.events.remove(i);
                dataPruned = true;
            }
        }

        return dataPruned;
    }

    public void onTimeChanged(long timeDiffMillis) {
        synchronized (mLock) {
            StringBuilder logBuilder = new StringBuilder();
            logBuilder.append("Time changed by ");
            TimeUtils.formatDuration(timeDiffMillis, logBuilder);
            logBuilder.append(".");

            int filesDeleted = 0;
            int filesMoved = 0;

            for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
                final int fileCount = files.size();
                for (int i = 0; i < fileCount; i++) {
                    final AtomicFile file = files.valueAt(i);
                    final long newTime = files.keyAt(i) + timeDiffMillis;
                    if (newTime < 0) {
                        filesDeleted++;
                        file.delete();
                    } else {
                        try {
                            file.openRead().close();
                        } catch (IOException e) {
                            // Ignore, this is just to make sure there are no backups.
                        }

                        String newName = Long.toString(newTime);
                        if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) {
                            newName = newName + CHECKED_IN_SUFFIX;
                        }

                        final File newFile = new File(file.getBaseFile().getParentFile(), newName);
                        filesMoved++;
                        file.getBaseFile().renameTo(newFile);
                    }
                }
                files.clear();
            }

            logBuilder.append(" files deleted: ").append(filesDeleted);
            logBuilder.append(" files moved: ").append(filesMoved);
            Slog.i(TAG, logBuilder.toString());

            // Now re-index the new files.
            indexFilesLocked();
        }
    }

    /**
     * Get the latest stats that exist for this interval type.
     */
    public IntervalStats getLatestUsageStats(int intervalType) {
        synchronized (mLock) {
            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
                throw new IllegalArgumentException("Bad interval type " + intervalType);
            }

            final int fileCount = mSortedStatFiles[intervalType].size();
            if (fileCount == 0) {
                return null;
            }

            try {
                final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
                IntervalStats stats = new IntervalStats();
                readLocked(f, stats);
                return stats;
            } catch (Exception e) {
                Slog.e(TAG, "Failed to read usage stats file", e);
            }
        }
        return null;
    }

    /**
     * Filter out those stats from the given stats that belong to removed packages. Filtering out
     * all of the stats at once has an amortized cost for future calls.
     */
    void filterStats(IntervalStats stats) {
        if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
            return;
        }
        final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;

        // filter out package usage stats
        final int removedPackagesSize = removedPackagesMap.size();
        for (int i = 0; i < removedPackagesSize; i++) {
            final String removedPackage = removedPackagesMap.keyAt(i);
            final UsageStats usageStats = stats.packageStats.get(removedPackage);
            if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
                stats.packageStats.remove(removedPackage);
            }
        }

        // filter out events
        for (int i = stats.events.size() - 1; i >= 0; i--) {
            final UsageEvents.Event event = stats.events.get(i);
            final Long timeRemoved = removedPackagesMap.get(event.mPackage);
            if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
                stats.events.remove(i);
            }
        }
    }

    /**
     * Figures out what to extract from the given IntervalStats object.
     */
    public interface StatCombiner<T> {

        /**
         * Implementations should extract interesting from <code>stats</code> and add it
         * to the <code>accumulatedResult</code> list.
         *
         * If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
         * which means you should make a copy of the data before adding it to the
         * <code>accumulatedResult</code> list.
         *
         * @param stats             The {@link IntervalStats} object selected.
         * @param mutable           Whether or not the data inside the stats object is mutable.
         * @param accumulatedResult The list to which to add extracted data.
         */
        void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
    }

    /**
     * Find all {@link IntervalStats} for the given range and interval type.
     */
    public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
            StatCombiner<T> combiner) {
        synchronized (mLock) {
            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
                throw new IllegalArgumentException("Bad interval type " + intervalType);
            }

            final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];

            if (endTime <= beginTime) {
                if (DEBUG) {
                    Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
                }
                return null;
            }

            int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
            if (startIndex < 0) {
                // All the stats available have timestamps after beginTime, which means they all
                // match.
                startIndex = 0;
            }

            int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
            if (endIndex < 0) {
                // All the stats start after this range ends, so nothing matches.
                if (DEBUG) {
                    Slog.d(TAG, "No results for this range. All stats start after.");
                }
                return null;
            }

            if (intervalStats.keyAt(endIndex) == endTime) {
                // The endTime is exclusive, so if we matched exactly take the one before.
                endIndex--;
                if (endIndex < 0) {
                    // All the stats start after this range ends, so nothing matches.
                    if (DEBUG) {
                        Slog.d(TAG, "No results for this range. All stats start after.");
                    }
                    return null;
                }
            }

            final ArrayList<T> results = new ArrayList<>();
            for (int i = startIndex; i <= endIndex; i++) {
                final AtomicFile f = intervalStats.valueAt(i);
                final IntervalStats stats = new IntervalStats();

                if (DEBUG) {
                    Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
                }

                try {
                    readLocked(f, stats);
                    if (beginTime < stats.endTime) {
                        combiner.combine(stats, false, results);
                    }
                } catch (Exception e) {
                    Slog.e(TAG, "Failed to read usage stats file", e);
                    // We continue so that we return results that are not
                    // corrupt.
                }
            }
            return results;
        }
    }

    /**
     * Find the interval that best matches this range.
     *
     * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
     */
    public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
        synchronized (mLock) {
            int bestBucket = -1;
            long smallestDiff = Long.MAX_VALUE;
            for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
                final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
                int size = mSortedStatFiles[i].size();
                if (index >= 0 && index < size) {
                    // We have some results here, check if they are better than our current match.
                    long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
                    if (diff < smallestDiff) {
                        smallestDiff = diff;
                        bestBucket = i;
                    }
                }
            }
            return bestBucket;
        }
    }

    /**
     * Remove any usage stat files that are too old.
     */
    public void prune(final long currentTimeMillis) {
        synchronized (mLock) {
            mCal.setTimeInMillis(currentTimeMillis);
            mCal.addYears(-3);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(currentTimeMillis);
            mCal.addMonths(-6);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(currentTimeMillis);
            mCal.addWeeks(-4);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(currentTimeMillis);
            mCal.addDays(-10);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(currentTimeMillis);
            mCal.addDays(-SELECTION_LOG_RETENTION_LEN);
            for (int i = 0; i < mIntervalDirs.length; ++i) {
                pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis());
            }

            // We must re-index our file list or we will be trying to read
            // deleted files.
            indexFilesLocked();
        }
    }

    private static void pruneFilesOlderThan(File dir, long expiryTime) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                long beginTime;
                try {
                    beginTime = parseBeginTime(f);
                } catch (IOException e) {
                    beginTime = 0;
                }

                if (beginTime < expiryTime) {
                    new AtomicFile(f).delete();
                }
            }
        }
    }

    private void pruneChooserCountsOlderThan(File dir, long expiryTime) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                long beginTime;
                try {
                    beginTime = parseBeginTime(f);
                } catch (IOException e) {
                    beginTime = 0;
                }

                if (beginTime < expiryTime) {
                    try {
                        final AtomicFile af = new AtomicFile(f);
                        final IntervalStats stats = new IntervalStats();
                        readLocked(af, stats);
                        final int pkgCount = stats.packageStats.size();
                        for (int i = 0; i < pkgCount; i++) {
                            UsageStats pkgStats = stats.packageStats.valueAt(i);
                            if (pkgStats.mChooserCounts != null) {
                                pkgStats.mChooserCounts.clear();
                            }
                        }
                        writeLocked(af, stats);
                    } catch (Exception e) {
                        Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e);
                    }
                }
            }
        }
    }


    private static long parseBeginTime(AtomicFile file) throws IOException {
        return parseBeginTime(file.getBaseFile());
    }

    private static long parseBeginTime(File file) throws IOException {
        String name = file.getName();

        // Parse out the digits from the the front of the file name
        for (int i = 0; i < name.length(); i++) {
            final char c = name.charAt(i);
            if (c < '0' || c > '9') {
                // found first char that is not a digit.
                name = name.substring(0, i);
                break;
            }
        }

        try {
            return Long.parseLong(name);
        } catch (NumberFormatException e) {
            throw new IOException(e);
        }
    }

    private void writeLocked(AtomicFile file, IntervalStats stats)
            throws IOException, RuntimeException {
        if (mCurrentVersion <= 3) {
            Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + mCurrentVersion);
            return;
        }
        writeLocked(file, stats, mCurrentVersion, mPackagesTokenData);
    }

    private static void writeLocked(AtomicFile file, IntervalStats stats, int version,
            PackagesTokenData packagesTokenData) throws IOException, RuntimeException {
        FileOutputStream fos = file.startWrite();
        try {
            writeLocked(fos, stats, version, packagesTokenData);
            file.finishWrite(fos);
            fos = null;
        } catch (Exception e) {
            // Do nothing. Exception has already been handled.
        } finally {
            // When fos is null (successful write), this will no-op
            file.failWrite(fos);
        }
    }

    private static void writeLocked(OutputStream out, IntervalStats stats, int version,
            PackagesTokenData packagesTokenData) throws Exception {
        switch (version) {
            case 1:
            case 2:
            case 3:
                Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + version);
                break;
            case 4:
                try {
                    UsageStatsProto.write(out, stats);
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to write interval stats to proto.", e);
                    throw e;
                }
                break;
            case 5:
                stats.obfuscateData(packagesTokenData);
                try {
                    UsageStatsProtoV2.write(out, stats);
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to write interval stats to proto.", e);
                    throw e;
                }
                break;
            default:
                throw new RuntimeException(
                        "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
                                + " on write.");
        }
    }

    /**
     * Note: the data read from the given file will add to the IntervalStats object passed into this
     * method. It is up to the caller to ensure that this is the desired behavior - if not, the
     * caller should ensure that the data in the reused object is being cleared.
     */
    private void readLocked(AtomicFile file, IntervalStats statsOut)
            throws IOException, RuntimeException {
        if (mCurrentVersion <= 3) {
            Slog.wtf(TAG, "Reading UsageStats as XML; current database version: "
                    + mCurrentVersion);
        }
        readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData);
    }

    /**
     * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
     * <p/>
     * Note: the data read from the given file will add to the IntervalStats object passed into this
     * method. It is up to the caller to ensure that this is the desired behavior - if not, the
     * caller should ensure that the data in the reused object is being cleared.
     */
    private static boolean readLocked(AtomicFile file, IntervalStats statsOut, int version,
            PackagesTokenData packagesTokenData) throws IOException, RuntimeException {
        boolean dataOmitted = false;
        try {
            FileInputStream in = file.openRead();
            try {
                statsOut.beginTime = parseBeginTime(file);
                dataOmitted = readLocked(in, statsOut, version, packagesTokenData);
                statsOut.lastTimeSaved = file.getLastModifiedTime();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
                    // Empty
                }
            }
        } catch (FileNotFoundException e) {
            Slog.e(TAG, "UsageStatsDatabase", e);
            throw e;
        }
        return dataOmitted;
    }

    /**
     * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
     * <p/>
     * Note: the data read from the given file will add to the IntervalStats object passed into this
     * method. It is up to the caller to ensure that this is the desired behavior - if not, the
     * caller should ensure that the data in the reused object is being cleared.
     */
    private static boolean readLocked(InputStream in, IntervalStats statsOut, int version,
            PackagesTokenData packagesTokenData) throws RuntimeException {
        boolean dataOmitted = false;
        switch (version) {
            case 1:
            case 2:
            case 3:
                Slog.w(TAG, "Reading UsageStats as XML; database version: " + version);
                try {
                    UsageStatsXml.read(in, statsOut);
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to read interval stats from XML", e);
                }
                break;
            case 4:
                try {
                    UsageStatsProto.read(in, statsOut);
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to read interval stats from proto.", e);
                }
                break;
            case 5:
                try {
                    UsageStatsProtoV2.read(in, statsOut);
                } catch (Exception e) {
                    Slog.e(TAG, "Unable to read interval stats from proto.", e);
                }
                dataOmitted = statsOut.deobfuscateData(packagesTokenData);
                break;
            default:
                throw new RuntimeException(
                        "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
                                + " on read.");
        }
        return dataOmitted;
    }

    /**
     * Reads the obfuscated data file from disk containing the tokens to packages mappings and
     * rebuilds the packages to tokens mappings based on that data.
     */
    public void readMappingsLocked() {
        if (!mPackageMappingsFile.exists()) {
            return; // package mappings file is missing - recreate mappings on next write.
        }

        try (FileInputStream in = new AtomicFile(mPackageMappingsFile).openRead()) {
            UsageStatsProtoV2.readObfuscatedData(in, mPackagesTokenData);
        } catch (Exception e) {
            Slog.e(TAG, "Failed to read the obfuscated packages mapping file.", e);
            return;
        }

        final SparseArray<ArrayList<String>> tokensToPackagesMap =
                mPackagesTokenData.tokensToPackagesMap;
        final int tokensToPackagesMapSize = tokensToPackagesMap.size();
        for (int i = 0; i < tokensToPackagesMapSize; i++) {
            final int packageToken = tokensToPackagesMap.keyAt(i);
            final ArrayList<String> tokensMap = tokensToPackagesMap.valueAt(i);
            final ArrayMap<String, Integer> packageStringsMap = new ArrayMap<>();
            final int tokensMapSize = tokensMap.size();
            // package name will always be at index 0 but its token should not be 0
            packageStringsMap.put(tokensMap.get(0), packageToken);
            for (int j = 1; j < tokensMapSize; j++) {
                packageStringsMap.put(tokensMap.get(j), j);
            }
            mPackagesTokenData.packagesToTokensMap.put(tokensMap.get(0), packageStringsMap);
        }
    }

    void writeMappingsLocked() throws IOException {
        final AtomicFile file = new AtomicFile(mPackageMappingsFile);
        FileOutputStream fos = file.startWrite();
        try {
            UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData);
            file.finishWrite(fos);
            fos = null;
        } catch (Exception e) {
            Slog.e(TAG, "Unable to write obfuscated data to proto.", e);
        } finally {
            file.failWrite(fos);
        }
    }

    void obfuscateCurrentStats(IntervalStats[] currentStats) {
        if (mCurrentVersion < 5) {
            return;
        }
        for (int i = 0; i < currentStats.length; i++) {
            final IntervalStats stats = currentStats[i];
            stats.obfuscateData(mPackagesTokenData);
        }
    }

    /**
     * Update the stats in the database. They may not be written to disk immediately.
     */
    public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
        if (stats == null) return;
        synchronized (mLock) {
            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
                throw new IllegalArgumentException("Bad interval type " + intervalType);
            }

            AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
            if (f == null) {
                f = new AtomicFile(new File(mIntervalDirs[intervalType],
                        Long.toString(stats.beginTime)));
                mSortedStatFiles[intervalType].put(stats.beginTime, f);
            }

            writeLocked(f, stats);
            stats.lastTimeSaved = f.getLastModifiedTime();
        }
    }


    /* Backup/Restore Code */
    byte[] getBackupPayload(String key) {
        return getBackupPayload(key, BACKUP_VERSION);
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public byte[] getBackupPayload(String key, int version) {
        if (version >= 1 && version <= 3) {
            Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version);
            return null;
        }
        synchronized (mLock) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            if (KEY_USAGE_STATS.equals(key)) {
                prune(System.currentTimeMillis());
                DataOutputStream out = new DataOutputStream(baos);
                try {
                    out.writeInt(version);

                    out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());

                    for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size();
                            i++) {
                        writeIntervalStatsToStream(out,
                                mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i),
                                version);
                    }

                    out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
                    for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size();
                            i++) {
                        writeIntervalStatsToStream(out,
                                mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i),
                                version);
                    }

                    out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
                    for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size();
                            i++) {
                        writeIntervalStatsToStream(out,
                                mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i),
                                version);
                    }

                    out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
                    for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size();
                            i++) {
                        writeIntervalStatsToStream(out,
                                mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i),
                                version);
                    }
                    if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
                } catch (IOException ioe) {
                    Slog.d(TAG, "Failed to write data to output stream", ioe);
                    baos.reset();
                }
            }
            return baos.toByteArray();
        }

    }

    /**
     * @hide
     */
    @VisibleForTesting
    public void applyRestoredPayload(String key, byte[] payload) {
        synchronized (mLock) {
            if (KEY_USAGE_STATS.equals(key)) {
                // Read stats files for the current device configs
                IntervalStats dailyConfigSource =
                        getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
                IntervalStats weeklyConfigSource =
                        getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
                IntervalStats monthlyConfigSource =
                        getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
                IntervalStats yearlyConfigSource =
                        getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);

                try {
                    DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
                    int backupDataVersion = in.readInt();

                    // Can't handle this backup set
                    if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;

                    // Delete all stats files
                    // Do this after reading version and before actually restoring
                    for (int i = 0; i < mIntervalDirs.length; i++) {
                        deleteDirectoryContents(mIntervalDirs[i]);
                    }

                    int fileCount = in.readInt();
                    for (int i = 0; i < fileCount; i++) {
                        IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                backupDataVersion);
                        stats = mergeStats(stats, dailyConfigSource);
                        putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
                    }

                    fileCount = in.readInt();
                    for (int i = 0; i < fileCount; i++) {
                        IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                backupDataVersion);
                        stats = mergeStats(stats, weeklyConfigSource);
                        putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
                    }

                    fileCount = in.readInt();
                    for (int i = 0; i < fileCount; i++) {
                        IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                backupDataVersion);
                        stats = mergeStats(stats, monthlyConfigSource);
                        putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
                    }

                    fileCount = in.readInt();
                    for (int i = 0; i < fileCount; i++) {
                        IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                backupDataVersion);
                        stats = mergeStats(stats, yearlyConfigSource);
                        putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
                    }
                    if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
                } catch (IOException ioe) {
                    Slog.d(TAG, "Failed to read data from input stream", ioe);
                } finally {
                    indexFilesLocked();
                }
            }
        }
    }

    /**
     * Get the Configuration Statistics from the current device statistics and merge them
     * with the backed up usage statistics.
     */
    private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
        if (onDevice == null) return beingRestored;
        if (beingRestored == null) return null;
        beingRestored.activeConfiguration = onDevice.activeConfiguration;
        beingRestored.configurations.putAll(onDevice.configurations);
        beingRestored.events.clear();
        beingRestored.events.merge(onDevice.events);
        return beingRestored;
    }

    private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)
            throws IOException {
        IntervalStats stats = new IntervalStats();
        try {
            readLocked(statsFile, stats);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read usage stats file", e);
            out.writeInt(0);
            return;
        }
        sanitizeIntervalStatsForBackup(stats);
        byte[] data = serializeIntervalStats(stats, version);
        out.writeInt(data.length);
        out.write(data);
    }

    private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException {
        int length = in.readInt();
        byte[] buffer = new byte[length];
        in.read(buffer, 0, length);
        return buffer;
    }

    private static void sanitizeIntervalStatsForBackup(IntervalStats stats) {
        if (stats == null) return;
        stats.activeConfiguration = null;
        stats.configurations.clear();
        stats.events.clear();
    }

    private byte[] serializeIntervalStats(IntervalStats stats, int version) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(baos);
        try {
            out.writeLong(stats.beginTime);
            writeLocked(out, stats, version, mPackagesTokenData);
        } catch (Exception ioe) {
            Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
            baos.reset();
        }
        return baos.toByteArray();
    }

    private IntervalStats deserializeIntervalStats(byte[] data, int version) {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        DataInputStream in = new DataInputStream(bais);
        IntervalStats stats = new IntervalStats();
        try {
            stats.beginTime = in.readLong();
            readLocked(in, stats, version, mPackagesTokenData);
        } catch (Exception e) {
            Slog.d(TAG, "DeSerializing IntervalStats Failed", e);
            stats = null;
        }
        return stats;
    }

    private static void deleteDirectoryContents(File directory) {
        File[] files = directory.listFiles();
        for (File file : files) {
            deleteDirectory(file);
        }
    }

    private static void deleteDirectory(File directory) {
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                if (!file.isDirectory()) {
                    file.delete();
                } else {
                    deleteDirectory(file);
                }
            }
        }
        directory.delete();
    }

    /**
     * Prints the obfuscated package mappings and a summary of the database files.
     * @param pw the print writer to print to
     */
    public void dump(IndentingPrintWriter pw, boolean compact) {
        synchronized (mLock) {
            pw.println();
            pw.println("UsageStatsDatabase:");
            pw.increaseIndent();
            dumpMappings(pw);
            pw.decreaseIndent();
            pw.println("Database Summary:");
            pw.increaseIndent();
            for (int i = 0; i < mSortedStatFiles.length; i++) {
                final TimeSparseArray<AtomicFile> files = mSortedStatFiles[i];
                final int size = files.size();
                pw.print(UserUsageStatsService.intervalToString(i));
                pw.print(" stats files: ");
                pw.print(size);
                pw.println(", sorted list of files:");
                pw.increaseIndent();
                for (int f = 0; f < size; f++) {
                    final long fileName = files.keyAt(f);
                    if (compact) {
                        pw.print(UserUsageStatsService.formatDateTime(fileName, false));
                    } else {
                        pw.printPair(Long.toString(fileName),
                                UserUsageStatsService.formatDateTime(fileName, true));
                    }
                    pw.println();
                }
                pw.decreaseIndent();
            }
            pw.decreaseIndent();
        }
    }

    void dumpMappings(IndentingPrintWriter pw) {
        synchronized (mLock) {
            pw.println("Obfuscated Packages Mappings:");
            pw.increaseIndent();
            pw.println("Counter: " + mPackagesTokenData.counter);
            pw.println("Tokens Map Size: " + mPackagesTokenData.tokensToPackagesMap.size());
            if (!mPackagesTokenData.removedPackageTokens.isEmpty()) {
                pw.println("Removed Package Tokens: "
                        + Arrays.toString(mPackagesTokenData.removedPackageTokens.toArray()));
            }
            for (int i = 0; i < mPackagesTokenData.tokensToPackagesMap.size(); i++) {
                final int packageToken = mPackagesTokenData.tokensToPackagesMap.keyAt(i);
                final String packageStrings = String.join(", ",
                        mPackagesTokenData.tokensToPackagesMap.valueAt(i));
                pw.println("Token " + packageToken + ": [" + packageStrings + "]");
            }
            pw.println();
            pw.decreaseIndent();
        }
    }

    IntervalStats readIntervalStatsForFile(int interval, long fileName) {
        synchronized (mLock) {
            final IntervalStats stats = new IntervalStats();
            try {
                readLocked(mSortedStatFiles[interval].get(fileName, null), stats);
                return stats;
            } catch (Exception e) {
                return null;
            }
        }
    }
}
