/*
 * Copyright (C) 2019 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.ConfigurationStats;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStats;
import android.content.res.Configuration;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/**
 * UsageStats reader/writer V2 for Protocol Buffer format.
 */
final class UsageStatsProtoV2 {
    private static final String TAG = "UsageStatsProtoV2";

    private static final long ONE_HOUR_MS = TimeUnit.HOURS.toMillis(1);

    // Static-only utility class.
    private UsageStatsProtoV2() {}

    private static UsageStats parseUsageStats(ProtoInputStream proto, final long beginTime)
            throws IOException {
        UsageStats stats = new UsageStats();
        // Time attributes stored is an offset of the beginTime.
        while (true) {
            switch (proto.nextField()) {
                case (int) UsageStatsObfuscatedProto.PACKAGE_TOKEN:
                    stats.mPackageToken = proto.readInt(
                            UsageStatsObfuscatedProto.PACKAGE_TOKEN) - 1;
                    break;
                case (int) UsageStatsObfuscatedProto.LAST_TIME_ACTIVE_MS:
                    stats.mLastTimeUsed = beginTime + proto.readLong(
                            UsageStatsObfuscatedProto.LAST_TIME_ACTIVE_MS);
                    break;
                case (int) UsageStatsObfuscatedProto.TOTAL_TIME_ACTIVE_MS:
                    stats.mTotalTimeInForeground = proto.readLong(
                            UsageStatsObfuscatedProto.TOTAL_TIME_ACTIVE_MS);
                    break;
                case (int) UsageStatsObfuscatedProto.APP_LAUNCH_COUNT:
                    stats.mAppLaunchCount = proto.readInt(
                            UsageStatsObfuscatedProto.APP_LAUNCH_COUNT);
                    break;
                case (int) UsageStatsObfuscatedProto.CHOOSER_ACTIONS:
                    try {
                        final long token = proto.start(UsageStatsObfuscatedProto.CHOOSER_ACTIONS);
                        loadChooserCounts(proto, stats);
                        proto.end(token);
                    } catch (IOException e) {
                        Slog.e(TAG, "Unable to read chooser counts for " + stats.mPackageToken);
                    }
                    break;
                case (int) UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS:
                    stats.mLastTimeForegroundServiceUsed = beginTime + proto.readLong(
                            UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS);
                    break;
                case (int) UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS:
                    stats.mTotalTimeForegroundServiceUsed = proto.readLong(
                            UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS);
                    break;
                case (int) UsageStatsObfuscatedProto.LAST_TIME_VISIBLE_MS:
                    stats.mLastTimeVisible = beginTime + proto.readLong(
                            UsageStatsObfuscatedProto.LAST_TIME_VISIBLE_MS);
                    break;
                case (int) UsageStatsObfuscatedProto.TOTAL_TIME_VISIBLE_MS:
                    stats.mTotalTimeVisible = proto.readLong(
                            UsageStatsObfuscatedProto.TOTAL_TIME_VISIBLE_MS);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return stats;
            }
        }
    }

    private static void loadCountAndTime(ProtoInputStream proto, long fieldId,
            IntervalStats.EventTracker tracker) {
        try {
            final long token = proto.start(fieldId);
            while (true) {
                switch (proto.nextField()) {
                    case (int) IntervalStatsObfuscatedProto.CountAndTime.COUNT:
                        tracker.count = proto.readInt(
                                IntervalStatsObfuscatedProto.CountAndTime.COUNT);
                        break;
                    case (int) IntervalStatsObfuscatedProto.CountAndTime.TIME_MS:
                        tracker.duration = proto.readLong(
                                IntervalStatsObfuscatedProto.CountAndTime.TIME_MS);
                        break;
                    case ProtoInputStream.NO_MORE_FIELDS:
                        proto.end(token);
                        return;
                }
            }
        } catch (IOException e) {
            Slog.e(TAG, "Unable to read event tracker " + fieldId, e);
        }
    }

    private static void loadChooserCounts(ProtoInputStream proto, UsageStats usageStats)
            throws IOException {
        int actionToken;
        SparseIntArray counts;
        if (proto.nextField(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN)) {
            // Fast path; this should work for most cases since the action token is written first
            actionToken = proto.readInt(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN) - 1;
            counts = usageStats.mChooserCountsObfuscated.get(actionToken);
            if (counts == null) {
                counts = new SparseIntArray();
                usageStats.mChooserCountsObfuscated.put(actionToken, counts);
            }
        } else {
            counts = new SparseIntArray();
        }

        while (true) {
            switch (proto.nextField()) {
                case (int) UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN:
                    // Fast path failed for some reason, add the SparseIntArray object to usageStats
                    actionToken = proto.readInt(
                            UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN) - 1;
                    usageStats.mChooserCountsObfuscated.put(actionToken, counts);
                    break;
                case (int) UsageStatsObfuscatedProto.ChooserAction.COUNTS:
                    final long token = proto.start(UsageStatsObfuscatedProto.ChooserAction.COUNTS);
                    loadCountsForAction(proto, counts);
                    proto.end(token);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return; // if the action was never read, the loaded counts will be ignored.
            }
        }
    }

    private static void loadCountsForAction(ProtoInputStream proto, SparseIntArray counts)
            throws IOException {
        int categoryToken = PackagesTokenData.UNASSIGNED_TOKEN;
        int count = 0;
        while (true) {
            switch (proto.nextField()) {
                case (int) UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN:
                    categoryToken = proto.readInt(
                            UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN)
                            - 1;
                    break;
                case (int) UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT:
                    count = proto.readInt(
                            UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    if (categoryToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                        counts.put(categoryToken, count);
                    }
                    return;
            }
        }
    }

    private static void loadConfigStats(ProtoInputStream proto, IntervalStats stats)
            throws IOException {
        boolean configActive = false;
        final Configuration config = new Configuration();
        ConfigurationStats configStats = new ConfigurationStats();
        if (proto.nextField(IntervalStatsObfuscatedProto.Configuration.CONFIG)) {
            // Fast path; this should work since the configuration is written first
            config.readFromProto(proto, IntervalStatsObfuscatedProto.Configuration.CONFIG);
            configStats = stats.getOrCreateConfigurationStats(config);
        }

        while (true) {
            switch (proto.nextField()) {
                case (int) IntervalStatsObfuscatedProto.Configuration.CONFIG:
                    // Fast path failed from some reason, add ConfigStats object to statsOut now
                    config.readFromProto(proto, IntervalStatsObfuscatedProto.Configuration.CONFIG);
                    final ConfigurationStats temp = stats.getOrCreateConfigurationStats(config);
                    temp.mLastTimeActive = configStats.mLastTimeActive;
                    temp.mTotalTimeActive = configStats.mTotalTimeActive;
                    temp.mActivationCount = configStats.mActivationCount;
                    configStats = temp;
                    break;
                case (int) IntervalStatsObfuscatedProto.Configuration.LAST_TIME_ACTIVE_MS:
                    configStats.mLastTimeActive = stats.beginTime + proto.readLong(
                            IntervalStatsObfuscatedProto.Configuration.LAST_TIME_ACTIVE_MS);
                    break;
                case (int) IntervalStatsObfuscatedProto.Configuration.TOTAL_TIME_ACTIVE_MS:
                    configStats.mTotalTimeActive = proto.readLong(
                            IntervalStatsObfuscatedProto.Configuration.TOTAL_TIME_ACTIVE_MS);
                    break;
                case (int) IntervalStatsObfuscatedProto.Configuration.COUNT:
                    configStats.mActivationCount = proto.readInt(
                            IntervalStatsObfuscatedProto.Configuration.COUNT);
                    break;
                case (int) IntervalStatsObfuscatedProto.Configuration.ACTIVE:
                    configActive = proto.readBoolean(
                            IntervalStatsObfuscatedProto.Configuration.ACTIVE);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    if (configActive) {
                        stats.activeConfiguration = configStats.mConfiguration;
                    }
                    return;
            }
        }
    }

    private static UsageEvents.Event parseEvent(ProtoInputStream proto, long beginTime)
            throws IOException {
        final UsageEvents.Event event = new UsageEvents.Event();
        while (true) {
            switch (proto.nextField()) {
                case (int) EventObfuscatedProto.PACKAGE_TOKEN:
                    event.mPackageToken = proto.readInt(EventObfuscatedProto.PACKAGE_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.CLASS_TOKEN:
                    event.mClassToken = proto.readInt(EventObfuscatedProto.CLASS_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.TIME_MS:
                    event.mTimeStamp = beginTime + proto.readLong(EventObfuscatedProto.TIME_MS);
                    break;
                case (int) EventObfuscatedProto.FLAGS:
                    event.mFlags = proto.readInt(EventObfuscatedProto.FLAGS);
                    break;
                case (int) EventObfuscatedProto.TYPE:
                    event.mEventType = proto.readInt(EventObfuscatedProto.TYPE);
                    break;
                case (int) EventObfuscatedProto.CONFIG:
                    event.mConfiguration = new Configuration();
                    event.mConfiguration.readFromProto(proto, EventObfuscatedProto.CONFIG);
                    break;
                case (int) EventObfuscatedProto.SHORTCUT_ID_TOKEN:
                    event.mShortcutIdToken = proto.readInt(
                            EventObfuscatedProto.SHORTCUT_ID_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.STANDBY_BUCKET:
                    event.mBucketAndReason = proto.readInt(EventObfuscatedProto.STANDBY_BUCKET);
                    break;
                case (int) EventObfuscatedProto.NOTIFICATION_CHANNEL_ID_TOKEN:
                    event.mNotificationChannelIdToken = proto.readInt(
                            EventObfuscatedProto.NOTIFICATION_CHANNEL_ID_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.INSTANCE_ID:
                    event.mInstanceId = proto.readInt(EventObfuscatedProto.INSTANCE_ID);
                    break;
                case (int) EventObfuscatedProto.TASK_ROOT_PACKAGE_TOKEN:
                    event.mTaskRootPackageToken = proto.readInt(
                            EventObfuscatedProto.TASK_ROOT_PACKAGE_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.TASK_ROOT_CLASS_TOKEN:
                    event.mTaskRootClassToken = proto.readInt(
                            EventObfuscatedProto.TASK_ROOT_CLASS_TOKEN) - 1;
                    break;
                case (int) EventObfuscatedProto.LOCUS_ID_TOKEN:
                    event.mLocusIdToken = proto.readInt(
                            EventObfuscatedProto.LOCUS_ID_TOKEN) - 1;
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return event.mPackageToken == PackagesTokenData.UNASSIGNED_TOKEN ? null : event;
            }
        }
    }

    static void writeOffsetTimestamp(ProtoOutputStream proto, long fieldId,
            long timestamp, long beginTime) {
        // timestamps will only be written if they're after the begin time
        // a grace period of one hour before the begin time is allowed because of rollover logic
        final long rolloverGracePeriod = beginTime - ONE_HOUR_MS;
        if (timestamp > rolloverGracePeriod) {
            // time attributes are stored as an offset of the begin time (given offset)
            proto.write(fieldId, getOffsetTimestamp(timestamp, beginTime));
        }
    }

    static long getOffsetTimestamp(long timestamp, long offset) {
        final long offsetTimestamp = timestamp - offset;
        // add one ms to timestamp if 0 to ensure it's written to proto (default values are ignored)
        return offsetTimestamp == 0 ? offsetTimestamp + 1 : offsetTimestamp;
    }

    private static void writeUsageStats(ProtoOutputStream proto, final long beginTime,
            final UsageStats stats) throws IllegalArgumentException {
        proto.write(UsageStatsObfuscatedProto.PACKAGE_TOKEN, stats.mPackageToken + 1);
        writeOffsetTimestamp(proto, UsageStatsObfuscatedProto.LAST_TIME_ACTIVE_MS,
                stats.mLastTimeUsed, beginTime);
        proto.write(UsageStatsObfuscatedProto.TOTAL_TIME_ACTIVE_MS, stats.mTotalTimeInForeground);
        writeOffsetTimestamp(proto, UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS,
                stats.mLastTimeForegroundServiceUsed, beginTime);
        proto.write(UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS,
                stats.mTotalTimeForegroundServiceUsed);
        writeOffsetTimestamp(proto, UsageStatsObfuscatedProto.LAST_TIME_VISIBLE_MS,
                stats.mLastTimeVisible, beginTime);
        proto.write(UsageStatsObfuscatedProto.TOTAL_TIME_VISIBLE_MS, stats.mTotalTimeVisible);
        proto.write(UsageStatsObfuscatedProto.APP_LAUNCH_COUNT, stats.mAppLaunchCount);
        try {
            writeChooserCounts(proto, stats);
        } catch (IllegalArgumentException e) {
            Slog.e(TAG, "Unable to write chooser counts for " + stats.mPackageName, e);
        }
    }

    private static void writeCountAndTime(ProtoOutputStream proto, long fieldId, int count,
            long time) throws IllegalArgumentException {
        final long token = proto.start(fieldId);
        proto.write(IntervalStatsObfuscatedProto.CountAndTime.COUNT, count);
        proto.write(IntervalStatsObfuscatedProto.CountAndTime.TIME_MS, time);
        proto.end(token);
    }

    private static void writeChooserCounts(ProtoOutputStream proto, final UsageStats stats)
            throws IllegalArgumentException {
        if (stats == null || stats.mChooserCountsObfuscated.size() == 0) {
            return;
        }
        final int chooserCountSize = stats.mChooserCountsObfuscated.size();
        for (int i = 0; i < chooserCountSize; i++) {
            final int action = stats.mChooserCountsObfuscated.keyAt(i);
            final SparseIntArray counts = stats.mChooserCountsObfuscated.valueAt(i);
            if (counts == null || counts.size() == 0) {
                continue;
            }
            final long token = proto.start(UsageStatsObfuscatedProto.CHOOSER_ACTIONS);
            proto.write(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN, action + 1);
            writeCountsForAction(proto, counts);
            proto.end(token);
        }
    }

    private static void writeCountsForAction(ProtoOutputStream proto, SparseIntArray counts)
            throws IllegalArgumentException {
        final int countsSize = counts.size();
        for (int i = 0; i < countsSize; i++) {
            final int category = counts.keyAt(i);
            final int count = counts.valueAt(i);
            if (count <= 0) {
                continue;
            }
            final long token = proto.start(UsageStatsObfuscatedProto.ChooserAction.COUNTS);
            proto.write(UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN,
                    category + 1);
            proto.write(UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT, count);
            proto.end(token);
        }
    }

    private static void writeConfigStats(ProtoOutputStream proto, final long statsBeginTime,
            final ConfigurationStats configStats, boolean isActive)
            throws IllegalArgumentException {
        configStats.mConfiguration.dumpDebug(proto,
                IntervalStatsObfuscatedProto.Configuration.CONFIG);
        writeOffsetTimestamp(proto, IntervalStatsObfuscatedProto.Configuration.LAST_TIME_ACTIVE_MS,
                configStats.mLastTimeActive, statsBeginTime);
        proto.write(IntervalStatsObfuscatedProto.Configuration.TOTAL_TIME_ACTIVE_MS,
                configStats.mTotalTimeActive);
        proto.write(IntervalStatsObfuscatedProto.Configuration.COUNT, configStats.mActivationCount);
        proto.write(IntervalStatsObfuscatedProto.Configuration.ACTIVE, isActive);
    }

    private static void writeEvent(ProtoOutputStream proto, final long statsBeginTime,
            final UsageEvents.Event event) throws IllegalArgumentException {
        proto.write(EventObfuscatedProto.PACKAGE_TOKEN, event.mPackageToken + 1);
        if (event.mClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
            proto.write(EventObfuscatedProto.CLASS_TOKEN, event.mClassToken + 1);
        }
        writeOffsetTimestamp(proto, EventObfuscatedProto.TIME_MS, event.mTimeStamp, statsBeginTime);
        proto.write(EventObfuscatedProto.FLAGS, event.mFlags);
        proto.write(EventObfuscatedProto.TYPE, event.mEventType);
        proto.write(EventObfuscatedProto.INSTANCE_ID, event.mInstanceId);
        if (event.mTaskRootPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) {
            proto.write(EventObfuscatedProto.TASK_ROOT_PACKAGE_TOKEN,
                    event.mTaskRootPackageToken + 1);
        }
        if (event.mTaskRootClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
            proto.write(EventObfuscatedProto.TASK_ROOT_CLASS_TOKEN, event.mTaskRootClassToken + 1);
        }
        switch (event.mEventType) {
            case UsageEvents.Event.CONFIGURATION_CHANGE:
                if (event.mConfiguration != null) {
                    event.mConfiguration.dumpDebug(proto, EventObfuscatedProto.CONFIG);
                }
                break;
            case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
                if (event.mBucketAndReason != 0) {
                    proto.write(EventObfuscatedProto.STANDBY_BUCKET, event.mBucketAndReason);
                }
                break;
            case UsageEvents.Event.SHORTCUT_INVOCATION:
                if (event.mShortcutIdToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                    proto.write(EventObfuscatedProto.SHORTCUT_ID_TOKEN, event.mShortcutIdToken + 1);
                }
                break;
            case UsageEvents.Event.LOCUS_ID_SET:
                if (event.mLocusIdToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                    proto.write(EventObfuscatedProto.LOCUS_ID_TOKEN, event.mLocusIdToken + 1);
                }
                break;
            case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                if (event.mNotificationChannelIdToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                    proto.write(EventObfuscatedProto.NOTIFICATION_CHANNEL_ID_TOKEN,
                            event.mNotificationChannelIdToken + 1);
                }
                break;
        }
    }

    /**
     * Populates a tokenized version of interval stats from the input stream given.
     *
     * @param in the input stream from which to read events.
     * @param stats the interval stats object which will be populated.
     */
    public static void read(InputStream in, IntervalStats stats) throws IOException {
        final ProtoInputStream proto = new ProtoInputStream(in);
        while (true) {
            switch (proto.nextField()) {
                case (int) IntervalStatsObfuscatedProto.END_TIME_MS:
                    stats.endTime = stats.beginTime + proto.readLong(
                            IntervalStatsObfuscatedProto.END_TIME_MS);
                    break;
                case (int) IntervalStatsObfuscatedProto.MAJOR_VERSION:
                    stats.majorVersion = proto.readInt(IntervalStatsObfuscatedProto.MAJOR_VERSION);
                    break;
                case (int) IntervalStatsObfuscatedProto.MINOR_VERSION:
                    stats.minorVersion = proto.readInt(IntervalStatsObfuscatedProto.MINOR_VERSION);
                    break;
                case (int) IntervalStatsObfuscatedProto.INTERACTIVE:
                    loadCountAndTime(proto, IntervalStatsObfuscatedProto.INTERACTIVE,
                            stats.interactiveTracker);
                    break;
                case (int) IntervalStatsObfuscatedProto.NON_INTERACTIVE:
                    loadCountAndTime(proto, IntervalStatsObfuscatedProto.NON_INTERACTIVE,
                            stats.nonInteractiveTracker);
                    break;
                case (int) IntervalStatsObfuscatedProto.KEYGUARD_SHOWN:
                    loadCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_SHOWN,
                            stats.keyguardShownTracker);
                    break;
                case (int) IntervalStatsObfuscatedProto.KEYGUARD_HIDDEN:
                    loadCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_HIDDEN,
                            stats.keyguardHiddenTracker);
                    break;
                case (int) IntervalStatsObfuscatedProto.PACKAGES:
                    try {
                        final long packagesToken = proto.start(
                                IntervalStatsObfuscatedProto.PACKAGES);
                        UsageStats usageStats = parseUsageStats(proto, stats.beginTime);
                        proto.end(packagesToken);
                        if (usageStats.mPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) {
                            stats.packageStatsObfuscated.put(usageStats.mPackageToken, usageStats);
                        }
                    } catch (IOException e) {
                        Slog.e(TAG, "Unable to read some usage stats from proto.", e);
                    }
                    break;
                case (int) IntervalStatsObfuscatedProto.CONFIGURATIONS:
                    try {
                        final long configsToken = proto.start(
                                IntervalStatsObfuscatedProto.CONFIGURATIONS);
                        loadConfigStats(proto, stats);
                        proto.end(configsToken);
                    } catch (IOException e) {
                        Slog.e(TAG, "Unable to read some configuration stats from proto.", e);
                    }
                    break;
                case (int) IntervalStatsObfuscatedProto.EVENT_LOG:
                    try {
                        final long eventsToken = proto.start(
                                IntervalStatsObfuscatedProto.EVENT_LOG);
                        UsageEvents.Event event = parseEvent(proto, stats.beginTime);
                        proto.end(eventsToken);
                        if (event != null) {
                            stats.events.insert(event);
                        }
                    } catch (IOException e) {
                        Slog.e(TAG, "Unable to read some events from proto.", e);
                    }
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    // update the begin and end time stamps for all usage stats
                    final int usageStatsSize = stats.packageStatsObfuscated.size();
                    for (int i = 0; i < usageStatsSize; i++) {
                        final UsageStats usageStats = stats.packageStatsObfuscated.valueAt(i);
                        usageStats.mBeginTimeStamp = stats.beginTime;
                        usageStats.mEndTimeStamp = stats.endTime;
                    }
                    return;
            }
        }
    }

    /**
     * Writes the tokenized interval stats object to a ProtoBuf file.
     *
     * @param out the output stream to which to write the interval stats data.
     * @param stats the interval stats object to write to the proto file.
     */
    public static void write(OutputStream out, IntervalStats stats)
            throws IOException, IllegalArgumentException {
        final ProtoOutputStream proto = new ProtoOutputStream(out);
        proto.write(IntervalStatsObfuscatedProto.END_TIME_MS,
                getOffsetTimestamp(stats.endTime, stats.beginTime));
        proto.write(IntervalStatsObfuscatedProto.MAJOR_VERSION, stats.majorVersion);
        proto.write(IntervalStatsObfuscatedProto.MINOR_VERSION, stats.minorVersion);

        try {
            writeCountAndTime(proto, IntervalStatsObfuscatedProto.INTERACTIVE,
                    stats.interactiveTracker.count, stats.interactiveTracker.duration);
            writeCountAndTime(proto, IntervalStatsObfuscatedProto.NON_INTERACTIVE,
                    stats.nonInteractiveTracker.count, stats.nonInteractiveTracker.duration);
            writeCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_SHOWN,
                    stats.keyguardShownTracker.count, stats.keyguardShownTracker.duration);
            writeCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_HIDDEN,
                    stats.keyguardHiddenTracker.count, stats.keyguardHiddenTracker.duration);
        } catch (IllegalArgumentException e) {
            Slog.e(TAG, "Unable to write some interval stats trackers to proto.", e);
        }

        final int statsCount = stats.packageStatsObfuscated.size();
        for (int i = 0; i < statsCount; i++) {
            try {
                final long token = proto.start(IntervalStatsObfuscatedProto.PACKAGES);
                writeUsageStats(proto, stats.beginTime, stats.packageStatsObfuscated.valueAt(i));
                proto.end(token);
            } catch (IllegalArgumentException e) {
                Slog.e(TAG, "Unable to write some usage stats to proto.", e);
            }
        }
        final int configCount = stats.configurations.size();
        for (int i = 0; i < configCount; i++) {
            boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i));
            try {
                final long token = proto.start(IntervalStatsObfuscatedProto.CONFIGURATIONS);
                writeConfigStats(proto, stats.beginTime, stats.configurations.valueAt(i), active);
                proto.end(token);
            } catch (IllegalArgumentException e) {
                Slog.e(TAG, "Unable to write some configuration stats to proto.", e);
            }
        }
        final int eventCount = stats.events.size();
        for (int i = 0; i < eventCount; i++) {
            try {
                final long token = proto.start(IntervalStatsObfuscatedProto.EVENT_LOG);
                writeEvent(proto, stats.beginTime, stats.events.get(i));
                proto.end(token);
            } catch (IllegalArgumentException e) {
                Slog.e(TAG, "Unable to write some events to proto.", e);
            }
        }

        proto.flush();
    }

    /***** Read/Write obfuscated packages data logic. *****/

    private static void loadPackagesMap(ProtoInputStream proto,
            SparseArray<ArrayList<String>> tokensToPackagesMap) throws IOException {
        int key = PackagesTokenData.UNASSIGNED_TOKEN;
        final ArrayList<String> strings = new ArrayList<>();
        while (true) {
            switch (proto.nextField()) {
                case (int) ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN:
                    key = proto.readInt(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN) - 1;
                    break;
                case (int) ObfuscatedPackagesProto.PackagesMap.STRINGS:
                    strings.add(proto.readString(ObfuscatedPackagesProto.PackagesMap.STRINGS));
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    if (key != PackagesTokenData.UNASSIGNED_TOKEN) {
                        tokensToPackagesMap.put(key, strings);
                    }
                    return;
            }
        }
    }

    /**
     * Populates the package mappings from the input stream given.
     *
     * @param in the input stream from which to read the mappings.
     * @param packagesTokenData the packages data object to which the data will be read to.
     */
    static void readObfuscatedData(InputStream in, PackagesTokenData packagesTokenData)
            throws IOException {
        final ProtoInputStream proto = new ProtoInputStream(in);
        while (true) {
            switch (proto.nextField()) {
                case (int) ObfuscatedPackagesProto.COUNTER:
                    packagesTokenData.counter = proto.readInt(ObfuscatedPackagesProto.COUNTER);
                    break;
                case (int) ObfuscatedPackagesProto.PACKAGES_MAP:
                    final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP);
                    loadPackagesMap(proto, packagesTokenData.tokensToPackagesMap);
                    proto.end(token);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return;
            }
        }
    }

    /**
     * Writes the packages mapping data to a ProtoBuf file.
     *
     * @param out the output stream to which to write the mappings.
     * @param packagesTokenData the packages data object holding the data to write.
     */
    static void writeObfuscatedData(OutputStream out, PackagesTokenData packagesTokenData)
            throws IOException, IllegalArgumentException {
        final ProtoOutputStream proto = new ProtoOutputStream(out);
        proto.write(ObfuscatedPackagesProto.COUNTER, packagesTokenData.counter);

        final int mapSize = packagesTokenData.tokensToPackagesMap.size();
        for (int i = 0; i < mapSize; i++) {
            final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP);
            int packageToken = packagesTokenData.tokensToPackagesMap.keyAt(i);
            proto.write(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN, packageToken + 1);

            final ArrayList<String> strings = packagesTokenData.tokensToPackagesMap.valueAt(i);
            final int listSize = strings.size();
            for (int j = 0; j < listSize; j++) {
                proto.write(ObfuscatedPackagesProto.PackagesMap.STRINGS, strings.get(j));
            }
            proto.end(token);
        }

        proto.flush();
    }

    /***** Read/Write pending events logic. *****/

    private static UsageEvents.Event parsePendingEvent(ProtoInputStream proto) throws IOException {
        final UsageEvents.Event event = new UsageEvents.Event();
        while (true) {
            switch (proto.nextField()) {
                case (int) PendingEventProto.PACKAGE_NAME:
                    event.mPackage = proto.readString(PendingEventProto.PACKAGE_NAME);
                    break;
                case (int) PendingEventProto.CLASS_NAME:
                    event.mClass = proto.readString(PendingEventProto.CLASS_NAME);
                    break;
                case (int) PendingEventProto.TIME_MS:
                    event.mTimeStamp = proto.readLong(PendingEventProto.TIME_MS);
                    break;
                case (int) PendingEventProto.FLAGS:
                    event.mFlags = proto.readInt(PendingEventProto.FLAGS);
                    break;
                case (int) PendingEventProto.TYPE:
                    event.mEventType = proto.readInt(PendingEventProto.TYPE);
                    break;
                case (int) PendingEventProto.CONFIG:
                    event.mConfiguration = new Configuration();
                    event.mConfiguration.readFromProto(proto, PendingEventProto.CONFIG);
                    break;
                case (int) PendingEventProto.SHORTCUT_ID:
                    event.mShortcutId = proto.readString(PendingEventProto.SHORTCUT_ID);
                    break;
                case (int) PendingEventProto.STANDBY_BUCKET:
                    event.mBucketAndReason = proto.readInt(PendingEventProto.STANDBY_BUCKET);
                    break;
                case (int) PendingEventProto.NOTIFICATION_CHANNEL_ID:
                    event.mNotificationChannelId = proto.readString(
                            PendingEventProto.NOTIFICATION_CHANNEL_ID);
                    break;
                case (int) PendingEventProto.INSTANCE_ID:
                    event.mInstanceId = proto.readInt(PendingEventProto.INSTANCE_ID);
                    break;
                case (int) PendingEventProto.TASK_ROOT_PACKAGE:
                    event.mTaskRootPackage = proto.readString(PendingEventProto.TASK_ROOT_PACKAGE);
                    break;
                case (int) PendingEventProto.TASK_ROOT_CLASS:
                    event.mTaskRootClass = proto.readString(PendingEventProto.TASK_ROOT_CLASS);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    // Handle default values for certain events types
                    switch (event.mEventType) {
                        case UsageEvents.Event.CONFIGURATION_CHANGE:
                            if (event.mConfiguration == null) {
                                event.mConfiguration = new Configuration();
                            }
                            break;
                        case UsageEvents.Event.SHORTCUT_INVOCATION:
                            if (event.mShortcutId == null) {
                                event.mShortcutId = "";
                            }
                            break;
                        case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                            if (event.mNotificationChannelId == null) {
                                event.mNotificationChannelId = "";
                            }
                            break;
                    }
                    return event.mPackage == null ? null : event;
            }
        }
    }

    /**
     * Populates the list of pending events from the input stream given.
     *
     * @param in the input stream from which to read the pending events.
     * @param events the list of pending events to populate.
     */
    static void readPendingEvents(InputStream in, LinkedList<UsageEvents.Event> events)
            throws IOException {
        final ProtoInputStream proto = new ProtoInputStream(in);
        while (true) {
            switch (proto.nextField()) {
                case (int) IntervalStatsObfuscatedProto.PENDING_EVENTS:
                    try {
                        final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
                        UsageEvents.Event event = parsePendingEvent(proto);
                        proto.end(token);
                        if (event != null) {
                            events.add(event);
                        }
                    } catch (IOException e) {
                        Slog.e(TAG, "Unable to parse some pending events from proto.", e);
                    }
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return;
            }
        }
    }

    private static void writePendingEvent(ProtoOutputStream proto, UsageEvents.Event event)
            throws IllegalArgumentException {
        proto.write(PendingEventProto.PACKAGE_NAME, event.mPackage);
        if (event.mClass != null) {
            proto.write(PendingEventProto.CLASS_NAME, event.mClass);
        }
        proto.write(PendingEventProto.TIME_MS, event.mTimeStamp);
        proto.write(PendingEventProto.FLAGS, event.mFlags);
        proto.write(PendingEventProto.TYPE, event.mEventType);
        proto.write(PendingEventProto.INSTANCE_ID, event.mInstanceId);
        if (event.mTaskRootPackage != null) {
            proto.write(PendingEventProto.TASK_ROOT_PACKAGE, event.mTaskRootPackage);
        }
        if (event.mTaskRootClass != null) {
            proto.write(PendingEventProto.TASK_ROOT_CLASS, event.mTaskRootClass);
        }
        switch (event.mEventType) {
            case UsageEvents.Event.CONFIGURATION_CHANGE:
                if (event.mConfiguration != null) {
                    event.mConfiguration.dumpDebug(proto, PendingEventProto.CONFIG);
                }
                break;
            case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
                if (event.mBucketAndReason != 0) {
                    proto.write(PendingEventProto.STANDBY_BUCKET, event.mBucketAndReason);
                }
                break;
            case UsageEvents.Event.SHORTCUT_INVOCATION:
                if (event.mShortcutId != null) {
                    proto.write(PendingEventProto.SHORTCUT_ID, event.mShortcutId);
                }
                break;
            case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                if (event.mNotificationChannelId != null) {
                    proto.write(PendingEventProto.NOTIFICATION_CHANNEL_ID,
                            event.mNotificationChannelId);
                }
                break;
        }
    }

    /**
     * Writes the pending events to a ProtoBuf file.
     *
     * @param out the output stream to which to write the pending events.
     * @param events the list of pending events.
     */
    static void writePendingEvents(OutputStream out, LinkedList<UsageEvents.Event> events)
            throws IOException, IllegalArgumentException {
        final ProtoOutputStream proto = new ProtoOutputStream(out);
        final int eventCount = events.size();
        for (int i = 0; i < eventCount; i++) {
            try {
                final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
                writePendingEvent(proto, events.get(i));
                proto.end(token);
            } catch (IllegalArgumentException e) {
                Slog.e(TAG, "Unable to write some pending events to proto.", e);
            }
        }
        proto.flush();
    }
}
