Obfuscate usage stats data stored on disk.

All of the usage stats data stored on disk will now be obfuscated. There
will be a package to tokens mappings file stored on disk which has a
hierarchy of mappings for each string in each package's usage stats
data. A UsageStatsProtoV2 was added to keep the logic clean and separate
from the original usage stats proto parser.

Initial observations show a memory gain of over 60% w.r.t. the usage
stats data size on disk. There is also no performance hit because of this
change - in fact, even with the obfuscation overhead, reads are now over
65% faster and writes are up to 50% faster.

Bug: 135484470
Test: atest UsageStatsTest
Test: atest UsageStatsDatabaseTest
Test: atest UsageStatsDatabasePerfTest
Change-Id: I55ce729033d8b6e4051271802d57c72684053c32
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 5dbca12..4bf9c04 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -312,6 +312,11 @@
         public static final int VALID_FLAG_BITS = FLAG_IS_PACKAGE_INSTANT_APP;
 
         /**
+         * @hide
+         */
+        private static final int UNASSIGNED_TOKEN = -1;
+
+        /**
          * {@hide}
          */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
@@ -320,12 +325,22 @@
         /**
          * {@hide}
          */
+        public int mPackageToken = UNASSIGNED_TOKEN;
+
+        /**
+         * {@hide}
+         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         public String mClass;
 
         /**
          * {@hide}
          */
+        public int mClassToken = UNASSIGNED_TOKEN;
+
+        /**
+         * {@hide}
+         */
         public int mInstanceId;
 
         /**
@@ -336,11 +351,21 @@
         /**
          * {@hide}
          */
+        public int mTaskRootPackageToken = UNASSIGNED_TOKEN;
+
+        /**
+         * {@hide}
+         */
         public String mTaskRootClass;
 
         /**
          * {@hide}
          */
+        public int mTaskRootClassToken = UNASSIGNED_TOKEN;
+
+        /**
+         * {@hide}
+         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         public long mTimeStamp;
 
@@ -365,6 +390,11 @@
         public String mShortcutId;
 
         /**
+         * {@hide}
+         */
+        public int mShortcutIdToken = UNASSIGNED_TOKEN;
+
+        /**
          * Action type passed to ChooserActivity
          * Only present for {@link #CHOOSER_ACTION} event types.
          * {@hide}
@@ -401,6 +431,11 @@
          */
         public String mNotificationChannelId;
 
+        /**
+         * {@hide}
+         */
+        public int mNotificationChannelIdToken = UNASSIGNED_TOKEN;
+
         /** @hide */
         @EventFlags
         public int mFlags;
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index 2c021cc..9d43dd3 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -35,6 +35,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
+import android.util.SparseArray;
 import android.util.SparseIntArray;
 
 /**
@@ -52,6 +53,11 @@
     /**
      * {@hide}
      */
+    public int mPackageToken = -1;
+
+    /**
+     * {@hide}
+     */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public long mBeginTimeStamp;
 
@@ -143,6 +149,11 @@
     /**
      * {@hide}
      */
+    public SparseArray<SparseIntArray> mChooserCountsObfuscated = new SparseArray<>();
+
+    /**
+     * {@hide}
+     */
     public UsageStats() {
     }
 
diff --git a/core/proto/android/server/usagestatsservice.proto b/core/proto/android/server/usagestatsservice.proto
index 75f265e..f26eefa 100644
--- a/core/proto/android/server/usagestatsservice.proto
+++ b/core/proto/android/server/usagestatsservice.proto
@@ -114,6 +114,4 @@
   repeated UsageStats packages = 20;
   repeated Configuration configurations = 21;
   repeated Event event_log = 22;
-
-  repeated Event pending_events = 23; // TODO: move to usagestatsservice_v2.proto
 }
diff --git a/core/proto/android/server/usagestatsservice_v2.proto b/core/proto/android/server/usagestatsservice_v2.proto
new file mode 100644
index 0000000..a28fcf3
--- /dev/null
+++ b/core/proto/android/server/usagestatsservice_v2.proto
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+package com.android.server.usage;
+import "frameworks/base/core/proto/android/content/configuration.proto";
+import "frameworks/base/core/proto/android/privacy.proto";
+
+option java_multiple_files = true;
+
+/**
+ * Obfuscated version of android.service.IntervalStatsProto (usagestatsservice.proto).
+ */
+message IntervalStatsObfuscatedProto {
+
+  message CountAndTime {
+    optional int32 count = 1;
+    optional int64 time_ms = 2;
+  }
+
+  // Stores the relevant information an IntervalStats will have about a Configuration
+  message Configuration {
+    optional .android.content.ConfigurationProto config = 1;
+    optional int64 last_time_active_ms = 2;
+    optional int64 total_time_active_ms = 3;
+    optional int32 count = 4;
+    optional bool active = 5;
+  }
+
+  // The following fields contain supplemental data used to build IntervalStats.
+  optional int64 end_time_ms = 1;
+  optional int32 major_version = 2;
+  optional int32 minor_version = 3;
+
+  // The following fields contain aggregated usage stats data
+  optional CountAndTime interactive = 10;
+  optional CountAndTime non_interactive = 11;
+  optional CountAndTime keyguard_shown = 12;
+  optional CountAndTime keyguard_hidden = 13;
+
+  // The following fields contain listed usage stats data
+  repeated UsageStatsObfuscatedProto packages = 20;
+  repeated Configuration configurations = 21;
+  repeated EventObfuscatedProto event_log = 22;
+  // The following field is only used to persist the reported events before a user unlock
+  repeated PendingEventProto pending_events = 23;
+}
+
+/**
+ * Stores the relevant information from an obfuscated UsageStats.
+ */
+message UsageStatsObfuscatedProto {
+  message ChooserAction {
+    message CategoryCount {
+      optional int32 category_token = 1;
+      optional int32 count = 2;
+    }
+    optional int32 action_token = 1;
+    repeated CategoryCount counts = 2;
+  }
+  optional int32 package_token = 1;
+  optional int64 last_time_active_ms = 3;
+  optional int64 total_time_active_ms = 4;
+  optional int32 last_event = 5;
+  optional int32 app_launch_count = 6;
+  repeated ChooserAction chooser_actions = 7;
+  optional int64 last_time_service_used_ms = 8;
+  optional int64 total_time_service_used_ms = 9;
+  optional int64 last_time_visible_ms = 10;
+  optional int64 total_time_visible_ms = 11;
+}
+
+/**
+ * Stores the relevant information from an obfuscated Event.
+ */
+message EventObfuscatedProto {
+  optional int32 package_token = 1;
+  optional int32 class_token = 2;
+  optional int64 time_ms = 3;
+  optional int32 flags = 4;
+  optional int32 type = 5;
+  optional .android.content.ConfigurationProto config = 6;
+  optional int32 shortcut_id_token = 7;
+  optional int32 standby_bucket = 8;
+  optional int32 notification_channel_id_token = 9;
+  optional int32 instance_id = 10;
+  optional int32 task_root_package_token = 11;
+  optional int32 task_root_class_token = 12;
+}
+
+/**
+ * This message stores all of the fields in an Event object as strings instead of tokens.
+ */
+message PendingEventProto {
+  optional string package_name = 1;
+  optional string class_name = 2;
+  optional int64 time_ms = 3;
+  optional int32 flags = 4;
+  optional int32 type = 5;
+  optional .android.content.ConfigurationProto config = 6;
+  optional string shortcut_id = 7;
+  optional int32 standby_bucket = 8;
+  optional string notification_channel_id = 9;
+  optional int32 instance_id = 10;
+  optional string task_root_package = 11;
+  optional string task_root_class = 12;
+}
+
+/**
+ * A proto message representing the obfuscated tokens mappings for Usage Stats.
+ */
+message ObfuscatedPackagesProto {
+  message PackagesMap {
+    optional int32 package_token = 1;
+    // The list of strings for each package where their indices are the token
+    repeated string strings = 2;
+  }
+
+  optional int32 counter = 1;
+  // Stores the mappings for every package
+  repeated PackagesMap packages_map = 2;
+}
diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
index c55f459..3a0ad4d 100644
--- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
+++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
@@ -48,7 +48,7 @@
 @SmallTest
 public class UsageStatsDatabaseTest {
 
-    private static final int MAX_TESTED_VERSION = 4;
+    private static final int MAX_TESTED_VERSION = 5;
     protected Context mContext;
     private UsageStatsDatabase mUsageStatsDatabase;
     private File mTestDir;
@@ -259,6 +259,24 @@
 
     void compareUsageEvent(Event e1, Event e2, int debugId, int minVersion) {
         switch (minVersion) {
+            case 5: // test fields added in version 5
+                assertEquals(e1.mPackageToken, e2.mPackageToken, "Usage event " + debugId);
+                assertEquals(e1.mClassToken, e2.mClassToken, "Usage event " + debugId);
+                assertEquals(e1.mTaskRootPackageToken, e2.mTaskRootPackageToken,
+                        "Usage event " + debugId);
+                assertEquals(e1.mTaskRootClassToken, e2.mTaskRootClassToken,
+                        "Usage event " + debugId);
+                switch (e1.mEventType) {
+                    case Event.SHORTCUT_INVOCATION:
+                        assertEquals(e1.mShortcutIdToken, e2.mShortcutIdToken,
+                                "Usage event " + debugId);
+                        break;
+                    case Event.NOTIFICATION_INTERRUPTION:
+                        assertEquals(e1.mNotificationChannelIdToken, e2.mNotificationChannelIdToken,
+                                "Usage event " + debugId);
+                        break;
+                }
+                // fallthrough
             case 4: // test fields added in version 4
                 assertEquals(e1.mInstanceId, e2.mInstanceId, "Usage event " + debugId);
                 assertEquals(e1.mTaskRootPackage, e2.mTaskRootPackage, "Usage event " + debugId);
@@ -372,6 +390,9 @@
         UsageStatsDatabase prevDB = new UsageStatsDatabase(mTestDir, oldVersion);
         prevDB.init(1);
         prevDB.putUsageStats(interval, mIntervalStats);
+        if (oldVersion >= 5) {
+            prevDB.writeMappingsLocked();
+        }
 
         // Simulate an upgrade to a new version and read from the disk
         UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir, newVersion);
@@ -438,6 +459,28 @@
         runVersionChangeTest(3, 4, UsageStatsManager.INTERVAL_YEARLY);
     }
 
+    /**
+     * Test the version upgrade from 4 to 5
+     */
+    @Test
+    public void testVersionUpgradeFrom4to5() throws IOException {
+        runVersionChangeTest(4, 5, UsageStatsManager.INTERVAL_DAILY);
+        runVersionChangeTest(4, 5, UsageStatsManager.INTERVAL_WEEKLY);
+        runVersionChangeTest(4, 5, UsageStatsManager.INTERVAL_MONTHLY);
+        runVersionChangeTest(4, 5, UsageStatsManager.INTERVAL_YEARLY);
+    }
+
+    /**
+     * Test the version upgrade from 3 to 5
+     */
+    @Test
+    public void testVersionUpgradeFrom3to5() throws IOException {
+        runVersionChangeTest(3, 5, UsageStatsManager.INTERVAL_DAILY);
+        runVersionChangeTest(3, 5, UsageStatsManager.INTERVAL_WEEKLY);
+        runVersionChangeTest(3, 5, UsageStatsManager.INTERVAL_MONTHLY);
+        runVersionChangeTest(3, 5, UsageStatsManager.INTERVAL_YEARLY);
+    }
+
 
     /**
      * Test the version upgrade from 3 to 4
@@ -492,4 +535,30 @@
             assertEquals(extra, files.keyAt(0));
         }
     }
+
+    private void compareObfuscatedData(int interval) throws IOException {
+        // Write IntervalStats to disk
+        UsageStatsDatabase prevDB = new UsageStatsDatabase(mTestDir, 5);
+        prevDB.init(1);
+        prevDB.putUsageStats(interval, mIntervalStats);
+        prevDB.writeMappingsLocked();
+
+        // Read IntervalStats from disk into a new db
+        UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir, 5);
+        newDB.init(mEndTime);
+        List<IntervalStats> stats = newDB.queryUsageStats(interval, 0, mEndTime,
+                mIntervalStatsVerifier);
+
+        assertEquals(1, stats.size());
+        // The written and read IntervalStats should match
+        compareIntervalStats(mIntervalStats, stats.get(0), 5);
+    }
+
+    @Test
+    public void testObfuscation() throws IOException {
+        compareObfuscatedData(UsageStatsManager.INTERVAL_DAILY);
+        compareObfuscatedData(UsageStatsManager.INTERVAL_WEEKLY);
+        compareObfuscatedData(UsageStatsManager.INTERVAL_MONTHLY);
+        compareObfuscatedData(UsageStatsManager.INTERVAL_YEARLY);
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java
index a783a40..02e48928 100644
--- a/services/usage/java/com/android/server/usage/IntervalStats.java
+++ b/services/usage/java/com/android/server/usage/IntervalStats.java
@@ -42,8 +42,11 @@
 import android.app.usage.UsageEvents.Event;
 import android.app.usage.UsageStats;
 import android.content.res.Configuration;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.util.proto.ProtoInputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -64,6 +67,8 @@
     public final EventTracker keyguardShownTracker = new EventTracker();
     public final EventTracker keyguardHiddenTracker = new EventTracker();
     public final ArrayMap<String, UsageStats> packageStats = new ArrayMap<>();
+    /** @hide */
+    public final SparseArray<UsageStats> packageStatsObfuscated = new SparseArray<>();
     public final ArrayMap<Configuration, ConfigurationStats> configurations = new ArrayMap<>();
     public Configuration activeConfiguration;
     public final EventList events = new EventList();
@@ -436,4 +441,182 @@
         */
         majorVersion = CURRENT_MAJOR_VERSION;
     }
+
+    /**
+     * Parses all of the tokens to strings in the obfuscated usage stats data. This includes
+     * deobfuscating each of the package tokens and chooser actions and categories.
+     */
+    private void deobfuscateUsageStats(PackagesTokenData packagesTokenData) {
+        final int usageStatsSize = packageStatsObfuscated.size();
+        for (int statsIndex = 0; statsIndex < usageStatsSize; statsIndex++) {
+            final int packageToken = packageStatsObfuscated.keyAt(statsIndex);
+            final UsageStats usageStats = packageStatsObfuscated.valueAt(statsIndex);
+            usageStats.mPackageName = packagesTokenData.getString(packageToken,
+                    PackagesTokenData.PACKAGE_NAME_INDEX);
+
+            // Update chooser counts
+            final int chooserActionsSize = usageStats.mChooserCountsObfuscated.size();
+            for (int actionIndex = 0; actionIndex < chooserActionsSize; actionIndex++) {
+                final ArrayMap<String, Integer> categoryCountsMap = new ArrayMap<>();
+                final int actionToken = usageStats.mChooserCountsObfuscated.keyAt(actionIndex);
+                final String action = packagesTokenData.getString(packageToken, actionToken);
+                final SparseIntArray categoryCounts =
+                        usageStats.mChooserCountsObfuscated.valueAt(actionIndex);
+                final int categoriesSize = categoryCounts.size();
+                for (int categoryIndex = 0; categoryIndex < categoriesSize; categoryIndex++) {
+                    final int categoryToken = categoryCounts.keyAt(categoryIndex);
+                    final String category = packagesTokenData.getString(packageToken,
+                            categoryToken);
+                    categoryCountsMap.put(category, categoryCounts.valueAt(categoryIndex));
+                }
+                usageStats.mChooserCounts.put(action, categoryCountsMap);
+            }
+            packageStats.put(usageStats.mPackageName, usageStats);
+        }
+    }
+
+    /**
+     * Parses all of the tokens to strings in the obfuscated events data. This includes
+     * deobfuscating the package token, along with any class, task root package/class tokens, and
+     * shortcut or notification channel tokens.
+     */
+    private void deobfuscateEvents(PackagesTokenData packagesTokenData) {
+        final int eventsSize = this.events.size();
+        for (int i = 0; i < eventsSize; i++) {
+            final Event event = this.events.get(i);
+            final int packageToken = event.mPackageToken;
+            event.mPackage = packagesTokenData.getString(packageToken,
+                    PackagesTokenData.PACKAGE_NAME_INDEX);
+            if (event.mClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
+                event.mClass = packagesTokenData.getString(packageToken, event.mClassToken);
+            }
+            if (event.mTaskRootPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) {
+                event.mTaskRootPackage = packagesTokenData.getString(packageToken,
+                        event.mTaskRootPackageToken);
+            }
+            if (event.mTaskRootClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
+                event.mTaskRootClass = packagesTokenData.getString(packageToken,
+                        event.mTaskRootClassToken);
+            }
+            switch (event.mEventType) {
+                case CONFIGURATION_CHANGE:
+                    if (event.mConfiguration == null) {
+                        event.mConfiguration = new Configuration();
+                    }
+                    break;
+                case SHORTCUT_INVOCATION:
+                    event.mShortcutId = packagesTokenData.getString(packageToken,
+                            event.mShortcutIdToken);
+                    break;
+                case NOTIFICATION_INTERRUPTION:
+                    event.mNotificationChannelId = packagesTokenData.getString(packageToken,
+                            event.mNotificationChannelIdToken);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Parses the obfuscated tokenized data held in this interval stats object.
+     *
+     * @hide
+     */
+    public void deobfuscateData(PackagesTokenData packagesTokenData) {
+        deobfuscateUsageStats(packagesTokenData);
+        deobfuscateEvents(packagesTokenData);
+    }
+
+    /**
+     * Obfuscates certain strings within each package stats such as the package name, and the
+     * chooser actions and categories.
+     */
+    private void obfuscateUsageStatsData(PackagesTokenData packagesTokenData) {
+        final int usageStatsSize = packageStats.size();
+        for (int statsIndex = 0; statsIndex < usageStatsSize; statsIndex++) {
+            final String packageName = packageStats.keyAt(statsIndex);
+            final UsageStats usageStats = packageStats.valueAt(statsIndex);
+            if (usageStats == null) {
+                continue;
+            }
+
+            final int packageToken = packagesTokenData.getPackageTokenOrAdd(packageName);
+            usageStats.mPackageToken = packageToken;
+            // Update chooser counts.
+            final int chooserActionsSize = usageStats.mChooserCounts.size();
+            for (int actionIndex = 0; actionIndex < chooserActionsSize; actionIndex++) {
+                final String action = usageStats.mChooserCounts.keyAt(actionIndex);
+                final ArrayMap<String, Integer> categoriesMap =
+                        usageStats.mChooserCounts.valueAt(actionIndex);
+                if (categoriesMap == null) {
+                    continue;
+                }
+
+                final SparseIntArray categoryCounts = new SparseIntArray();
+                final int categoriesSize = categoriesMap.size();
+                for (int categoryIndex = 0; categoryIndex < categoriesSize; categoryIndex++) {
+                    String category = categoriesMap.keyAt(categoryIndex);
+                    int categoryToken = packagesTokenData.getTokenOrAdd(packageToken, packageName,
+                            category);
+                    categoryCounts.put(categoryToken, categoriesMap.valueAt(categoryIndex));
+                }
+                int actionToken = packagesTokenData.getTokenOrAdd(packageToken, packageName,
+                        action);
+                usageStats.mChooserCountsObfuscated.put(actionToken, categoryCounts);
+            }
+            packageStatsObfuscated.put(packageToken, usageStats);
+        }
+    }
+
+    /**
+     * Obfuscates certain strings within an event such as the package name, the class name,
+     * task root package and class names, and shortcut and notification channel ids.
+     */
+    private void obfuscateEventsData(PackagesTokenData packagesTokenData) {
+        final int eventSize = events.size();
+        for (int i = 0; i < eventSize; i++) {
+            final Event event = events.get(i);
+            if (event == null) {
+                continue;
+            }
+
+            final int packageToken = packagesTokenData.getPackageTokenOrAdd(event.mPackage);
+            event.mPackageToken = packageToken;
+            if (!TextUtils.isEmpty(event.mClass)) {
+                event.mClassToken = packagesTokenData.getTokenOrAdd(packageToken,
+                        event.mPackage, event.mClass);
+            }
+            if (!TextUtils.isEmpty(event.mTaskRootPackage)) {
+                event.mTaskRootPackageToken = packagesTokenData.getTokenOrAdd(packageToken,
+                        event.mPackage, event.mTaskRootPackage);
+            }
+            if (!TextUtils.isEmpty(event.mTaskRootClass)) {
+                event.mTaskRootClassToken = packagesTokenData.getTokenOrAdd(packageToken,
+                        event.mPackage, event.mTaskRootClass);
+            }
+            switch (event.mEventType) {
+                case SHORTCUT_INVOCATION:
+                    if (!TextUtils.isEmpty(event.mShortcutId)) {
+                        event.mShortcutIdToken = packagesTokenData.getTokenOrAdd(packageToken,
+                                event.mPackage, event.mShortcutId);
+                    }
+                    break;
+                case NOTIFICATION_INTERRUPTION:
+                    if (!TextUtils.isEmpty(event.mNotificationChannelId)) {
+                        event.mNotificationChannelIdToken = packagesTokenData.getTokenOrAdd(
+                                packageToken, event.mPackage, event.mNotificationChannelId);
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Obfuscates the data in this instance of interval stats.
+     *
+     * @hide
+     */
+    public void obfuscateData(PackagesTokenData packagesTokenData) {
+        obfuscateUsageStatsData(packagesTokenData);
+        obfuscateEventsData(packagesTokenData);
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/PackagesTokenData.java b/services/usage/java/com/android/server/usage/PackagesTokenData.java
new file mode 100644
index 0000000..8e4c639
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/PackagesTokenData.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 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.util.ArrayMap;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+
+/**
+ * An object holding data defining the obfuscated packages and their token mappings.
+ * Used by {@link UsageStatsDatabase}.
+ *
+ * @hide
+ */
+public final class PackagesTokenData {
+    /**
+     * The default token for any string that hasn't been tokenized yet.
+     */
+    public static final int UNASSIGNED_TOKEN = -1;
+
+    /**
+     * The package name is always stored at index 0 in {@code tokensToPackagesMap}.
+     */
+    public static final int PACKAGE_NAME_INDEX = 0;
+
+    /**
+     * The main token counter for each package.
+     */
+    public int counter = 1;
+    /**
+     * Stores a hierarchy of token to string mappings for each package, indexed by the main
+     * package token. The 0th index within the array list will always hold the package name.
+     */
+    public final SparseArray<ArrayList<String>> tokensToPackagesMap = new SparseArray<>();
+    /**
+     * Stores a hierarchy of strings to token mappings for each package. This is simply an inverse
+     * map of the {@code tokenToPackagesMap} in this class, mainly for an O(1) access to the tokens.
+     */
+    public final ArrayMap<String, ArrayMap<String, Integer>> packagesToTokensMap = new ArrayMap<>();
+
+    public PackagesTokenData() {
+    }
+
+    /**
+     * Fetches the token mapped to the given package name. If there is no mapping, a new token is
+     * created and the relevant mappings are updated.
+     *
+     * @param packageName the package name whose token is being fetched
+     * @return the mapped token
+     */
+    public int getPackageTokenOrAdd(String packageName) {
+        ArrayMap<String, Integer> packageTokensMap = packagesToTokensMap.get(packageName);
+        if (packageTokensMap == null) {
+            packageTokensMap = new ArrayMap<>();
+            packagesToTokensMap.put(packageName, packageTokensMap);
+        }
+        int token = packageTokensMap.getOrDefault(packageName, UNASSIGNED_TOKEN);
+        if (token == UNASSIGNED_TOKEN) {
+            token = counter++;
+            // package name should always be at index 0 in the sub-mapping
+            ArrayList<String> tokenPackages = new ArrayList<>();
+            tokenPackages.add(packageName);
+            packageTokensMap.put(packageName, token);
+            tokensToPackagesMap.put(token, tokenPackages);
+        }
+        return token;
+    }
+
+    /**
+     * Fetches the token mapped to the given key within the package's context. If there is no
+     * mapping, a new token is created and the relevant mappings are updated.
+     *
+     * @param packageToken the package token for which the given key belongs to
+     * @param packageName the package name for which the given key belongs to
+     * @param key the key whose token is being fetched
+     * @return the mapped token
+     */
+    public int getTokenOrAdd(int packageToken, String packageName, String key) {
+        if (packageName.equals(key)) {
+            return PACKAGE_NAME_INDEX;
+        }
+        int token = packagesToTokensMap.get(packageName).getOrDefault(key, UNASSIGNED_TOKEN);
+        if (token == UNASSIGNED_TOKEN) {
+            token = tokensToPackagesMap.get(packageToken).size();
+            packagesToTokensMap.get(packageName).put(key, token);
+            tokensToPackagesMap.get(packageToken).add(key);
+        }
+        return token;
+    }
+
+    /**
+     * Fetches the string represented by the given token.
+     *
+     * @param packageToken the package token for which this token belongs to
+     * @param token the token whose string needs to be fetched
+     * @return the string representing the given token
+     */
+    public String getString(int packageToken, int token) {
+        return tokensToPackagesMap.get(packageToken).get(token);
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index 5197b3b..d29b77c 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -21,8 +21,10 @@
 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;
@@ -75,7 +77,7 @@
  * directory should be deserialized.
  */
 public class UsageStatsDatabase {
-    private static final int DEFAULT_CURRENT_VERSION = 4;
+    private static final int DEFAULT_CURRENT_VERSION = 5;
     /**
      * Current version of the backup schema
      *
@@ -93,7 +95,8 @@
 
     // Persist versioned backup files.
     // Should be false, except when testing new versions
-    static final boolean KEEP_BACKUP_DIR = false;
+    // STOPSHIP: b/139937606 this should be false on launch
+    static final boolean KEEP_BACKUP_DIR = true;
 
     private static final String TAG = "UsageStatsDatabase";
     private static final boolean DEBUG = UsageStatsService.DEBUG;
@@ -119,6 +122,11 @@
     private boolean mFirstUpdate;
     private boolean mNewUpdate;
 
+    // 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.
+    private final PackagesTokenData mPackagesTokenData = new PackagesTokenData();
+
     /**
      * UsageStatsDatabase constructor that allows setting the version number.
      * This should only be used for testing.
@@ -138,6 +146,7 @@
         mBackupsDir = new File(dir, "backups");
         mUpdateBreadcrumb = new File(dir, "breadcrumb");
         mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
+        mPackageMappingsFile = new File(dir, "mappings");
         mCal = new UnixCalendar(0);
     }
 
@@ -150,6 +159,8 @@
      */
     public void init(long currentTimeMillis) {
         synchronized (mLock) {
+            readMappingsLocked();
+
             for (File f : mIntervalDirs) {
                 f.mkdirs();
                 if (!f.exists()) {
@@ -479,6 +490,11 @@
     private void continueUpgradeLocked(int version, long token) {
         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++) {
@@ -494,9 +510,16 @@
                     }
                     try {
                         IntervalStats stats = new IntervalStats();
-                        readLocked(new AtomicFile(files[j]), stats, version);
+                        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);
+                                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());
@@ -504,6 +527,15 @@
                 }
             }
         }
+
+        // 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.");
+            }
+        }
     }
 
     public void onTimeChanged(long timeDiffMillis) {
@@ -808,14 +840,14 @@
     }
 
     private void writeLocked(AtomicFile file, IntervalStats stats) throws IOException {
-        writeLocked(file, stats, mCurrentVersion);
+        writeLocked(file, stats, mCurrentVersion, mPackagesTokenData);
     }
 
-    private static void writeLocked(AtomicFile file, IntervalStats stats, int version)
-            throws IOException {
+    private static void writeLocked(AtomicFile file, IntervalStats stats, int version,
+            PackagesTokenData packagesTokenData) throws IOException {
         FileOutputStream fos = file.startWrite();
         try {
-            writeLocked(fos, stats, version);
+            writeLocked(fos, stats, version, packagesTokenData);
             file.finishWrite(fos);
             fos = null;
         } finally {
@@ -825,11 +857,11 @@
     }
 
     private void writeLocked(OutputStream out, IntervalStats stats) throws IOException {
-        writeLocked(out, stats, mCurrentVersion);
+        writeLocked(out, stats, mCurrentVersion, mPackagesTokenData);
     }
 
-    private static void writeLocked(OutputStream out, IntervalStats stats, int version)
-            throws IOException {
+    private static void writeLocked(OutputStream out, IntervalStats stats, int version,
+            PackagesTokenData packagesTokenData) throws IOException {
         switch (version) {
             case 1:
             case 2:
@@ -839,6 +871,10 @@
             case 4:
                 UsageStatsProto.write(out, stats);
                 break;
+            case 5:
+                stats.obfuscateData(packagesTokenData);
+                UsageStatsProtoV2.write(out, stats);
+                break;
             default:
                 throw new RuntimeException(
                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
@@ -847,16 +883,16 @@
     }
 
     private void readLocked(AtomicFile file, IntervalStats statsOut) throws IOException {
-        readLocked(file, statsOut, mCurrentVersion);
+        readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData);
     }
 
-    private static void readLocked(AtomicFile file, IntervalStats statsOut, int version)
-            throws IOException {
+    private static void readLocked(AtomicFile file, IntervalStats statsOut, int version,
+            PackagesTokenData packagesTokenData) throws IOException {
         try {
             FileInputStream in = file.openRead();
             try {
                 statsOut.beginTime = parseBeginTime(file);
-                readLocked(in, statsOut, version);
+                readLocked(in, statsOut, version, packagesTokenData);
                 statsOut.lastTimeSaved = file.getLastModifiedTime();
             } finally {
                 try {
@@ -872,11 +908,11 @@
     }
 
     private void readLocked(InputStream in, IntervalStats statsOut) throws IOException {
-        readLocked(in, statsOut, mCurrentVersion);
+        readLocked(in, statsOut, mCurrentVersion, mPackagesTokenData);
     }
 
-    private static void readLocked(InputStream in, IntervalStats statsOut, int version)
-            throws IOException {
+    private static void readLocked(InputStream in, IntervalStats statsOut, int version,
+            PackagesTokenData packagesTokenData) throws IOException {
         switch (version) {
             case 1:
             case 2:
@@ -886,6 +922,10 @@
             case 4:
                 UsageStatsProto.read(in, statsOut);
                 break;
+            case 5:
+                UsageStatsProtoV2.read(in, statsOut);
+                statsOut.deobfuscateData(packagesTokenData);
+                break;
             default:
                 throw new RuntimeException(
                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
@@ -895,6 +935,61 @@
     }
 
     /**
+     * Reads the obfuscated data file from disk containing the tokens to packages mappings and
+     * rebuilds the packages to tokens mappings based on that data.
+     */
+    private 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 (IOException 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;
+        } 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 {
@@ -1098,7 +1193,7 @@
         DataOutputStream out = new DataOutputStream(baos);
         try {
             out.writeLong(stats.beginTime);
-            writeLocked(out, stats, version);
+            writeLocked(out, stats, version, mPackagesTokenData);
         } catch (Exception ioe) {
             Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
             baos.reset();
@@ -1112,7 +1207,7 @@
         IntervalStats stats = new IntervalStats();
         try {
             stats.beginTime = in.readLong();
-            readLocked(in, stats, version);
+            readLocked(in, stats, version, mPackagesTokenData);
         } catch (IOException ioe) {
             Slog.d(TAG, "DeSerializing IntervalStats Failed", ioe);
             stats = null;
@@ -1142,13 +1237,18 @@
     }
 
     /**
-     * print total number and list of stats files for each interval type.
-     * @param pw
+     * 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();
@@ -1173,6 +1273,23 @@
         }
     }
 
+    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());
+            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();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsProto.java b/services/usage/java/com/android/server/usage/UsageStatsProto.java
index 6d3f416..53ab230 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsProto.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsProto.java
@@ -407,7 +407,6 @@
         proto.write(IntervalStatsProto.Configuration.COUNT, configStats.mActivationCount);
         proto.write(IntervalStatsProto.Configuration.ACTIVE, isActive);
         proto.end(token);
-
     }
 
     private static void writeEvent(ProtoOutputStream proto, long fieldId, final IntervalStats stats,
@@ -606,37 +605,4 @@
 
         proto.flush();
     }
-
-    // TODO: move to UsageStatsProtoV2
-    static void readPendingEvents(InputStream in, List<UsageEvents.Event> events)
-            throws IOException {
-        final ProtoInputStream proto = new ProtoInputStream(in);
-        final List<String> stringPool = new ArrayList<>();
-        final IntervalStats tmpStatsObj = new IntervalStats();
-        while (true) {
-            switch (proto.nextField()) {
-                case (int) IntervalStatsProto.PENDING_EVENTS:
-                    loadEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, stringPool);
-                    break;
-                case ProtoInputStream.NO_MORE_FIELDS:
-                    final int eventCount = tmpStatsObj.events.size();
-                    for (int i = 0; i < eventCount; i++) {
-                        events.add(tmpStatsObj.events.get(i));
-                    }
-                    return;
-            }
-        }
-    }
-
-    // TODO: move to UsageStatsProtoV2
-    static void writePendingEvents(OutputStream out, List<UsageEvents.Event> events)
-            throws IOException {
-        final ProtoOutputStream proto = new ProtoOutputStream(out);
-        final IntervalStats tmpStatsObj = new IntervalStats();
-        final int eventCount = events.size();
-        for (int i = 0; i < eventCount; i++) {
-            writeEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, events.get(i));
-        }
-        proto.flush();
-    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsProtoV2.java b/services/usage/java/com/android/server/usage/UsageStatsProtoV2.java
new file mode 100644
index 0000000..badb3ee
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/UsageStatsProtoV2.java
@@ -0,0 +1,730 @@
+/*
+ * 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.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;
+
+/**
+ * UsageStats reader/writer V2 for Protocol Buffer format.
+ */
+final class UsageStatsProtoV2 {
+    private static final String TAG = "UsageStatsProtoV2";
+
+    // 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:
+                    final long token = proto.start(UsageStatsObfuscatedProto.CHOOSER_ACTIONS);
+                    loadChooserCounts(proto, stats);
+                    proto.end(token);
+                    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:
+                    // mLastTimeUsed was not read, assume default value of 0 plus beginTime
+                    if (stats.mLastTimeUsed == 0) {
+                        stats.mLastTimeUsed = beginTime;
+                    }
+                    return stats;
+            }
+        }
+    }
+
+    private static void loadCountAndTime(ProtoInputStream proto, long fieldId,
+            IntervalStats.EventTracker tracker) throws IOException {
+        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;
+            }
+        }
+    }
+
+    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:
+                    // mLastTimeActive was not assigned, assume default value of 0 plus beginTime
+                    if (configStats.mLastTimeActive == 0) {
+                        configStats.mLastTimeActive = stats.beginTime;
+                    }
+                    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 ProtoInputStream.NO_MORE_FIELDS:
+                    // timeStamp was not read, assume default value 0 plus beginTime
+                    if (event.mTimeStamp == 0) {
+                        event.mTimeStamp = beginTime;
+                    }
+                    return event.mPackageToken == PackagesTokenData.UNASSIGNED_TOKEN ? null : event;
+            }
+        }
+    }
+
+    private static void writeUsageStats(ProtoOutputStream proto, final long beginTime,
+            final UsageStats stats) throws IOException {
+        // Time attributes stored as an offset of the beginTime.
+        proto.write(UsageStatsObfuscatedProto.PACKAGE_TOKEN, stats.mPackageToken + 1);
+        proto.write(UsageStatsObfuscatedProto.LAST_TIME_ACTIVE_MS, stats.mLastTimeUsed - beginTime);
+        proto.write(UsageStatsObfuscatedProto.TOTAL_TIME_ACTIVE_MS, stats.mTotalTimeInForeground);
+        proto.write(UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS,
+                stats.mLastTimeForegroundServiceUsed - beginTime);
+        proto.write(UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS,
+                stats.mTotalTimeForegroundServiceUsed);
+        proto.write(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);
+        writeChooserCounts(proto, stats);
+    }
+
+    private static void writeCountAndTime(ProtoOutputStream proto, long fieldId, int count,
+            long time) throws IOException {
+        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 IOException {
+        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 IOException {
+        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 IOException {
+        configStats.mConfiguration.writeToProto(proto,
+                IntervalStatsObfuscatedProto.Configuration.CONFIG);
+        proto.write(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 IOException {
+        proto.write(EventObfuscatedProto.PACKAGE_TOKEN, event.mPackageToken + 1);
+        if (event.mClassToken != PackagesTokenData.UNASSIGNED_TOKEN) {
+            proto.write(EventObfuscatedProto.CLASS_TOKEN, event.mClassToken + 1);
+        }
+        proto.write(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.writeToProto(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.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:
+                    final long packagesToken = proto.start(IntervalStatsObfuscatedProto.PACKAGES);
+                    UsageStats usageStats = parseUsageStats(proto, stats.beginTime);
+                    if (usageStats.mPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) {
+                        stats.packageStatsObfuscated.put(usageStats.mPackageToken, usageStats);
+                    }
+                    proto.end(packagesToken);
+                    break;
+                case (int) IntervalStatsObfuscatedProto.CONFIGURATIONS:
+                    final long configsToken = proto.start(
+                            IntervalStatsObfuscatedProto.CONFIGURATIONS);
+                    loadConfigStats(proto, stats);
+                    proto.end(configsToken);
+                    break;
+                case (int) IntervalStatsObfuscatedProto.EVENT_LOG:
+                    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);
+                    }
+                    break;
+                case ProtoInputStream.NO_MORE_FIELDS:
+                    // endTime not assigned, assume default value of 0 plus beginTime
+                    if (stats.endTime == 0) {
+                        stats.endTime = stats.beginTime;
+                    }
+                    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 {
+        final ProtoOutputStream proto = new ProtoOutputStream(out);
+        proto.write(IntervalStatsObfuscatedProto.END_TIME_MS, stats.endTime - stats.beginTime);
+        proto.write(IntervalStatsObfuscatedProto.MAJOR_VERSION, stats.majorVersion);
+        proto.write(IntervalStatsObfuscatedProto.MINOR_VERSION, stats.minorVersion);
+
+        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);
+
+        final int statsCount = stats.packageStatsObfuscated.size();
+        for (int i = 0; i < statsCount; i++) {
+            final long token = proto.start(IntervalStatsObfuscatedProto.PACKAGES);
+            writeUsageStats(proto, stats.beginTime, stats.packageStatsObfuscated.valueAt(i));
+            proto.end(token);
+        }
+        final int configCount = stats.configurations.size();
+        for (int i = 0; i < configCount; i++) {
+            boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i));
+            final long token = proto.start(IntervalStatsObfuscatedProto.CONFIGURATIONS);
+            writeConfigStats(proto, stats.beginTime, stats.configurations.valueAt(i), active);
+            proto.end(token);
+        }
+        final int eventCount = stats.events.size();
+        for (int i = 0; i < eventCount; i++) {
+            final long token = proto.start(IntervalStatsObfuscatedProto.EVENT_LOG);
+            writeEvent(proto, stats.beginTime, stats.events.get(i));
+            proto.end(token);
+        }
+
+        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 {
+        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:
+                    final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
+                    UsageEvents.Event event = parsePendingEvent(proto);
+                    proto.end(token);
+                    if (event != null) {
+                        events.add(event);
+                    }
+                    break;
+                case ProtoInputStream.NO_MORE_FIELDS:
+                    return;
+            }
+        }
+    }
+
+    /**
+     * 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 {
+        final ProtoOutputStream proto = new ProtoOutputStream(out);
+        final int eventCount = events.size();
+        for (int i = 0; i < eventCount; i++) {
+            final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
+            final UsageEvents.Event event = events.get(i);
+            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.writeToProto(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;
+            }
+            proto.end(token);
+        }
+        proto.flush();
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 59d0735..8e392a7 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -620,7 +620,7 @@
             final AtomicFile af = new AtomicFile(pendingEventsFiles[i]);
             try {
                 try (FileInputStream in = af.openRead()) {
-                    UsageStatsProto.readPendingEvents(in, pendingEvents);
+                    UsageStatsProtoV2.readPendingEvents(in, pendingEvents);
                 }
             } catch (IOException e) {
                 // Even if one file read fails, exit here to keep all events in order on disk -
@@ -650,7 +650,7 @@
         FileOutputStream fos = null;
         try {
             fos = af.startWrite();
-            UsageStatsProto.writePendingEvents(fos, pendingEvents);
+            UsageStatsProtoV2.writePendingEvents(fos, pendingEvents);
             af.finishWrite(fos);
             fos = null;
             pendingEvents.clear();
@@ -1033,21 +1033,13 @@
                                 ipw.decreaseIndent();
                             }
                         } else {
-                            final int user;
-                            try {
-                                user = Integer.valueOf(args[i + 1]);
-                            } catch (NumberFormatException nfe) {
-                                ipw.println("invalid user specified.");
-                                return;
+                            final int user = parseUserIdFromArgs(args, i, ipw);
+                            if (user != UserHandle.USER_NULL) {
+                                final String[] remainingArgs = Arrays.copyOfRange(
+                                        args, i + 2, args.length);
+                                // dump everything for the specified user
+                                mUserState.get(user).dumpFile(ipw, remainingArgs);
                             }
-                            if (mUserState.indexOfKey(user) < 0) {
-                                ipw.println("the specified user does not exist.");
-                                return;
-                            }
-                            final String[] remainingArgs = Arrays.copyOfRange(
-                                    args, i + 2, args.length);
-                            // dump everything for the specified user
-                            mUserState.get(user).dumpFile(ipw, remainingArgs);
                         }
                         return;
                     } else if ("database-info".equals(arg)) {
@@ -1062,19 +1054,11 @@
                                 ipw.decreaseIndent();
                             }
                         } else {
-                            final int user;
-                            try {
-                                user = Integer.valueOf(args[i + 1]);
-                            } catch (NumberFormatException nfe) {
-                                ipw.println("invalid user specified.");
-                                return;
+                            final int user = parseUserIdFromArgs(args, i, ipw);
+                            if (user != UserHandle.USER_NULL) {
+                                // dump info only for the specified user
+                                mUserState.get(user).dumpDatabaseInfo(ipw);
                             }
-                            if (mUserState.indexOfKey(user) < 0) {
-                                ipw.println("the specified user does not exist.");
-                                return;
-                            }
-                            // dump info only for the specified user
-                            mUserState.get(user).dumpDatabaseInfo(ipw);
                         }
                         return;
                     } else if ("appstandby".equals(arg)) {
@@ -1082,15 +1066,18 @@
                         return;
                     } else if ("stats-directory".equals(arg)) {
                         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
-                        final int userId;
-                        try {
-                            userId = Integer.valueOf(args[i + 1]);
-                        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
-                            ipw.println("invalid user specified.");
-                            return;
+                        final int userId = parseUserIdFromArgs(args, i, ipw);
+                        if (userId != UserHandle.USER_NULL) {
+                            ipw.println(new File(Environment.getDataSystemCeDirectory(userId),
+                                    "usagestats").getAbsolutePath());
                         }
-                        ipw.println(new File(Environment.getDataSystemCeDirectory(userId),
-                                "usagestats").getAbsolutePath());
+                        return;
+                    } else if ("mappings".equals(arg)) {
+                        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+                        final int userId = parseUserIdFromArgs(args, i, ipw);
+                        if (userId != UserHandle.USER_NULL) {
+                            mUserState.get(userId).dumpMappings(ipw);
+                        }
                         return;
                     } else if (arg != null && !arg.startsWith("-")) {
                         // Anything else that doesn't start with '-' is a pkg to filter
@@ -1129,6 +1116,21 @@
         }
     }
 
+    private int parseUserIdFromArgs(String[] args, int index, IndentingPrintWriter ipw) {
+        final int userId;
+        try {
+            userId = Integer.valueOf(args[index + 1]);
+        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+            ipw.println("invalid user specified.");
+            return UserHandle.USER_NULL;
+        }
+        if (mUserState.indexOfKey(userId) < 0) {
+            ipw.println("the specified user does not exist.");
+            return UserHandle.USER_NULL;
+        }
+        return userId;
+    }
+
     class H extends Handler {
         public H(Looper looper) {
             super(looper);
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index 1560b9e..ec6cade 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -524,6 +524,8 @@
         if (mStatsChanged) {
             Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
             try {
+                mDatabase.obfuscateCurrentStats(mCurrentStats);
+                mDatabase.writeMappingsLocked();
                 for (int i = 0; i < mCurrentStats.length; i++) {
                     mDatabase.putUsageStats(i, mCurrentStats[i]);
                 }
@@ -700,6 +702,10 @@
         mDatabase.dump(ipw, false);
     }
 
+    void dumpMappings(IndentingPrintWriter ipw) {
+        mDatabase.dumpMappings(ipw);
+    }
+
     void dumpFile(IndentingPrintWriter ipw, String[] args) {
         if (args == null || args.length == 0) {
             // dump all files for every interval for specified user
diff --git a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
index 7d9d0d5..62aef87 100644
--- a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
+++ b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java
@@ -30,6 +30,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.usage.IntervalStats;
+import com.android.server.usage.PackagesTokenData;
 import com.android.server.usage.UsageStatsDatabase;
 import com.android.server.usage.UsageStatsDatabase.StatCombiner;
 
@@ -140,6 +141,37 @@
         }
     }
 
+    private void runObfuscateStatsTest(int packageCount, int eventsPerPackage) {
+        final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
+        IntervalStats intervalStats = new IntervalStats();
+        populateIntervalStats(intervalStats, packageCount, eventsPerPackage);
+        long elapsedTimeNs = 0;
+        while (benchmarkState.keepRunning(elapsedTimeNs)) {
+            final long startTime = SystemClock.elapsedRealtimeNanos();
+            PackagesTokenData packagesTokenData = new PackagesTokenData();
+            intervalStats.obfuscateData(packagesTokenData);
+            final long endTime = SystemClock.elapsedRealtimeNanos();
+            elapsedTimeNs = endTime - startTime;
+            clearUsageStatsFiles();
+        }
+    }
+
+    private void runDeobfuscateStatsTest(int packageCount, int eventsPerPackage) {
+        final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
+        IntervalStats intervalStats = new IntervalStats();
+        populateIntervalStats(intervalStats, packageCount, eventsPerPackage);
+        long elapsedTimeNs = 0;
+        while (benchmarkState.keepRunning(elapsedTimeNs)) {
+            PackagesTokenData packagesTokenData = new PackagesTokenData();
+            intervalStats.obfuscateData(packagesTokenData);
+            final long startTime = SystemClock.elapsedRealtimeNanos();
+            intervalStats.deobfuscateData(packagesTokenData);
+            final long endTime = SystemClock.elapsedRealtimeNanos();
+            elapsedTimeNs = endTime - startTime;
+            clearUsageStatsFiles();
+        }
+    }
+
     @Test
     public void testQueryUsageStats_FewPkgsLightUse() throws IOException {
         runQueryUsageStatsTest(FEW_PKGS, LIGHT_USE);
@@ -151,6 +183,16 @@
     }
 
     @Test
+    public void testObfuscateStats_FewPkgsLightUse() {
+        runObfuscateStatsTest(FEW_PKGS, LIGHT_USE);
+    }
+
+    @Test
+    public void testDeobfuscateStats_FewPkgsLightUse() {
+        runDeobfuscateStatsTest(FEW_PKGS, LIGHT_USE);
+    }
+
+    @Test
     public void testQueryUsageStats_FewPkgsHeavyUse() throws IOException {
         runQueryUsageStatsTest(FEW_PKGS, HEAVY_USE);
     }
@@ -161,6 +203,16 @@
     }
 
     @Test
+    public void testObfuscateStats_FewPkgsHeavyUse() {
+        runObfuscateStatsTest(FEW_PKGS, HEAVY_USE);
+    }
+
+    @Test
+    public void testDeobfuscateStats_FewPkgsHeavyUse() {
+        runDeobfuscateStatsTest(FEW_PKGS, HEAVY_USE);
+    }
+
+    @Test
     public void testQueryUsageStats_ManyPkgsLightUse() throws IOException {
         runQueryUsageStatsTest(MANY_PKGS, LIGHT_USE);
     }
@@ -171,6 +223,16 @@
     }
 
     @Test
+    public void testObfuscateStats_ManyPkgsLightUse() {
+        runObfuscateStatsTest(MANY_PKGS, LIGHT_USE);
+    }
+
+    @Test
+    public void testDeobfuscateStats_ManyPkgsLightUse() {
+        runDeobfuscateStatsTest(MANY_PKGS, LIGHT_USE);
+    }
+
+    @Test
     public void testQueryUsageStats_ManyPkgsHeavyUse() throws IOException {
         runQueryUsageStatsTest(MANY_PKGS, HEAVY_USE);
     }
@@ -179,4 +241,14 @@
     public void testPutUsageStats_ManyPkgsHeavyUse() throws IOException {
         runPutUsageStatsTest(MANY_PKGS, HEAVY_USE);
     }
+
+    @Test
+    public void testObfuscateStats_ManyPkgsHeavyUse() {
+        runObfuscateStatsTest(MANY_PKGS, HEAVY_USE);
+    }
+
+    @Test
+    public void testDeobfuscateStats_ManyPkgsHeavyUse() {
+        runDeobfuscateStatsTest(MANY_PKGS, HEAVY_USE);
+    }
 }