diff --git a/core/java/android/app/usage/EventList.java b/core/java/android/app/usage/EventList.java index 8c03405855736..afdcbe6d164f0 100644 --- a/core/java/android/app/usage/EventList.java +++ b/core/java/android/app/usage/EventList.java @@ -78,6 +78,21 @@ public class EventList { mEvents.add(insertIndex, event); } + /** + * Removes the event at the given index. + * + * @param index the index of the event to remove + * @return the event removed, or {@code null} if the index was out of bounds + */ + public UsageEvents.Event remove(int index) { + try { + return mEvents.remove(index); + } catch (IndexOutOfBoundsException e) { + // catch and handle the exception here instead of throwing it to the client + return null; + } + } + /** * Finds the index of the first event whose timestamp is greater than or equal to the given * timestamp. diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 5dbca12fa3ddf..4bf9c04dfadec 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -311,18 +311,33 @@ public final class UsageEvents implements Parcelable { */ 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) public String mPackage; + /** + * {@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} */ @@ -333,11 +348,21 @@ public final class UsageEvents implements Parcelable { */ public String mTaskRootPackage; + /** + * {@hide} + */ + public int mTaskRootPackageToken = UNASSIGNED_TOKEN; + /** * {@hide} */ public String mTaskRootClass; + /** + * {@hide} + */ + public int mTaskRootClassToken = UNASSIGNED_TOKEN; + /** * {@hide} */ @@ -364,6 +389,11 @@ public final class UsageEvents implements Parcelable { */ public String mShortcutId; + /** + * {@hide} + */ + public int mShortcutIdToken = UNASSIGNED_TOKEN; + /** * Action type passed to ChooserActivity * Only present for {@link #CHOOSER_ACTION} event types. @@ -401,6 +431,11 @@ public final class UsageEvents implements Parcelable { */ 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 2c021cc42cb5c..9d43dd34d5581 100644 --- a/core/java/android/app/usage/UsageStats.java +++ b/core/java/android/app/usage/UsageStats.java @@ -35,6 +35,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; +import android.util.SparseArray; import android.util.SparseIntArray; /** @@ -49,6 +50,11 @@ public final class UsageStats implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public String mPackageName; + /** + * {@hide} + */ + public int mPackageToken = -1; + /** * {@hide} */ @@ -140,6 +146,11 @@ public final class UsageStats implements Parcelable { */ public ArrayMap> mChooserCounts = new ArrayMap<>(); + /** + * {@hide} + */ + public SparseArray mChooserCountsObfuscated = new SparseArray<>(); + /** * {@hide} */ diff --git a/core/proto/android/server/usagestatsservice.proto b/core/proto/android/server/usagestatsservice.proto index 75f265ea6a881..f26eefad24e18 100644 --- a/core/proto/android/server/usagestatsservice.proto +++ b/core/proto/android/server/usagestatsservice.proto @@ -114,6 +114,4 @@ message IntervalStatsProto { 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 0000000000000..a28fcf3589f18 --- /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/IntervalStatsTests.java b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java new file mode 100644 index 0000000000000..f1b2ef8118851 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java @@ -0,0 +1,191 @@ +/* + * 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 static android.app.usage.UsageEvents.Event.MAX_EVENT_TYPE; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import android.app.usage.UsageEvents; +import android.content.res.Configuration; +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IntervalStatsTests { + private static final int NUMBER_OF_PACKAGES = 7; + private static final int NUMBER_OF_EVENTS_PER_PACKAGE = 200; + private static final int NUMBER_OF_EVENTS = NUMBER_OF_PACKAGES * NUMBER_OF_EVENTS_PER_PACKAGE; + + private long mEndTime = 0; + + private void populateIntervalStats(IntervalStats intervalStats) { + final int timeProgression = 23; + long time = System.currentTimeMillis() - (NUMBER_OF_EVENTS * timeProgression); + + intervalStats.majorVersion = 7; + intervalStats.minorVersion = 8; + intervalStats.beginTime = time; + intervalStats.interactiveTracker.count = 2; + intervalStats.interactiveTracker.duration = 111111; + intervalStats.nonInteractiveTracker.count = 3; + intervalStats.nonInteractiveTracker.duration = 222222; + intervalStats.keyguardShownTracker.count = 4; + intervalStats.keyguardShownTracker.duration = 333333; + intervalStats.keyguardHiddenTracker.count = 5; + intervalStats.keyguardHiddenTracker.duration = 4444444; + + for (int i = 0; i < NUMBER_OF_EVENTS; i++) { + UsageEvents.Event event = new UsageEvents.Event(); + final int packageInt = ((i / 3) % NUMBER_OF_PACKAGES); // clusters of 3 events + event.mPackage = "fake.package.name" + packageInt; + if (packageInt == 3) { + // Third app is an instant app + event.mFlags |= UsageEvents.Event.FLAG_IS_PACKAGE_INSTANT_APP; + } + + final int instanceId = i % 11; + event.mClass = ".fake.class.name" + instanceId; + event.mTimeStamp = time; + event.mEventType = i % (MAX_EVENT_TYPE + 1); //"random" event type + event.mInstanceId = instanceId; + + + final int rootPackageInt = (i % 5); // 5 "apps" start each task + event.mTaskRootPackage = "fake.package.name" + rootPackageInt; + + final int rootClassInt = i % 6; + event.mTaskRootClass = ".fake.class.name" + rootClassInt; + + switch (event.mEventType) { + case UsageEvents.Event.CONFIGURATION_CHANGE: + event.mConfiguration = new Configuration(); //empty config + break; + case UsageEvents.Event.SHORTCUT_INVOCATION: + event.mShortcutId = "shortcut" + (i % 8); //"random" shortcut + break; + case UsageEvents.Event.STANDBY_BUCKET_CHANGED: + //"random" bucket and reason + event.mBucketAndReason = (((i % 5 + 1) * 10) << 16) & (i % 5 + 1) << 8; + break; + case UsageEvents.Event.NOTIFICATION_INTERRUPTION: + event.mNotificationChannelId = "channel" + (i % 5); //"random" channel + break; + } + + intervalStats.addEvent(event); + intervalStats.update(event.mPackage, event.mClass, event.mTimeStamp, event.mEventType, + event.mInstanceId); + + time += timeProgression; // Arbitrary progression of time + } + mEndTime = time; + + final Configuration config1 = new Configuration(); + config1.fontScale = 3.3f; + config1.mcc = 4; + intervalStats.getOrCreateConfigurationStats(config1); + + final Configuration config2 = new Configuration(); + config2.mnc = 5; + config2.setLocale(new Locale("en", "US")); + intervalStats.getOrCreateConfigurationStats(config2); + + intervalStats.activeConfiguration = config2; + } + + @Test + public void testObfuscation() { + final IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats); + + final PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + + // data is populated with 7 different "apps" + assertEquals(packagesTokenData.tokensToPackagesMap.size(), NUMBER_OF_PACKAGES); + assertEquals(packagesTokenData.packagesToTokensMap.size(), NUMBER_OF_PACKAGES); + assertEquals(packagesTokenData.counter, NUMBER_OF_PACKAGES + 1); + + assertEquals(intervalStats.events.size(), NUMBER_OF_EVENTS); + assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES); + } + + @Test + public void testDeobfuscation() { + final IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats); + + final PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + intervalStats.deobfuscateData(packagesTokenData); + + // ensure deobfuscation doesn't update any of the mappings data + assertEquals(packagesTokenData.tokensToPackagesMap.size(), NUMBER_OF_PACKAGES); + assertEquals(packagesTokenData.packagesToTokensMap.size(), NUMBER_OF_PACKAGES); + assertEquals(packagesTokenData.counter, NUMBER_OF_PACKAGES + 1); + + // ensure deobfuscation didn't remove any events or usage stats + assertEquals(intervalStats.events.size(), NUMBER_OF_EVENTS); + assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES); + } + + @Test + public void testBadDataOnDeobfuscation() { + final IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats); + + final PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + intervalStats.packageStats.clear(); + + // remove the mapping for token 2 + packagesTokenData.tokensToPackagesMap.remove(2); + + intervalStats.deobfuscateData(packagesTokenData); + // deobfuscation should have removed all events mapped to package token 2 + assertEquals(intervalStats.events.size(), + NUMBER_OF_EVENTS - NUMBER_OF_EVENTS_PER_PACKAGE - 1); + assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES - 1); + } + + @Test + public void testBadPackageDataOnDeobfuscation() { + final IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats); + + final PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + intervalStats.packageStats.clear(); + + // remove mapping number 2 within package 3 (random) + packagesTokenData.tokensToPackagesMap.valueAt(3).remove(2); + + intervalStats.deobfuscateData(packagesTokenData); + // deobfuscation should not have removed all events for a package - however, it's possible + // that some events were removed because of how shortcut and notification events are handled + assertTrue(intervalStats.events.size() > NUMBER_OF_EVENTS - NUMBER_OF_EVENTS_PER_PACKAGE); + assertEquals(intervalStats.packageStats.size(), NUMBER_OF_PACKAGES); + } +} 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 c55f459c9fe76..df0c37a9856c6 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 @@ import java.util.Locale; @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; @@ -74,6 +74,7 @@ public class UsageStatsDatabaseTest { mContext = InstrumentationRegistry.getTargetContext(); mTestDir = new File(mContext.getFilesDir(), "UsageStatsDatabaseTest"); mUsageStatsDatabase = new UsageStatsDatabase(mTestDir); + mUsageStatsDatabase.readMappingsLocked(); mUsageStatsDatabase.init(1); populateIntervalStats(); clearUsageStatsFiles(); @@ -259,6 +260,24 @@ public class UsageStatsDatabaseTest { 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); @@ -370,11 +389,16 @@ public class UsageStatsDatabaseTest { void runVersionChangeTest(int oldVersion, int newVersion, int interval) throws IOException { // Write IntervalStats to disk in old version format UsageStatsDatabase prevDB = new UsageStatsDatabase(mTestDir, oldVersion); + prevDB.readMappingsLocked(); 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); + newDB.readMappingsLocked(); newDB.init(mEndTime); List stats = newDB.queryUsageStats(interval, 0, mEndTime, mIntervalStatsVerifier); @@ -394,6 +418,7 @@ public class UsageStatsDatabaseTest { */ void runBackupRestoreTest(int version) throws IOException { UsageStatsDatabase prevDB = new UsageStatsDatabase(mTestDir); + prevDB.readMappingsLocked(); prevDB.init(1); prevDB.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats); // Create a backup with a specific version @@ -402,6 +427,7 @@ public class UsageStatsDatabaseTest { clearUsageStatsFiles(); UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir); + newDB.readMappingsLocked(); newDB.init(1); // Attempt to restore the usage stats from the backup newDB.applyRestoredPayload(KEY_USAGE_STATS, blob); @@ -438,6 +464,28 @@ public class UsageStatsDatabaseTest { 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 +540,68 @@ public class UsageStatsDatabaseTest { assertEquals(extra, files.keyAt(0)); } } + + private void compareObfuscatedData(int interval) throws IOException { + // Write IntervalStats to disk + UsageStatsDatabase prevDB = new UsageStatsDatabase(mTestDir, 5); + prevDB.readMappingsLocked(); + prevDB.init(1); + prevDB.putUsageStats(interval, mIntervalStats); + prevDB.writeMappingsLocked(); + + // Read IntervalStats from disk into a new db + UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir, 5); + newDB.readMappingsLocked(); + newDB.init(mEndTime); + List 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); + } + + private void verifyPackageNotRetained(int interval) throws IOException { + UsageStatsDatabase db = new UsageStatsDatabase(mTestDir, 5); + db.readMappingsLocked(); + db.init(1); + db.putUsageStats(interval, mIntervalStats); + + final String removedPackage = "fake.package.name0"; + // invoke handler call directly from test to remove package + db.onPackageRemoved(removedPackage, System.currentTimeMillis()); + + List stats = db.queryUsageStats(interval, 0, mEndTime, + mIntervalStatsVerifier); + for (int i = 0; i < stats.size(); i++) { + final IntervalStats stat = stats.get(i); + if (stat.packageStats.containsKey(removedPackage)) { + fail("Found removed package " + removedPackage + " in package stats."); + return; + } + for (int j = 0; j < stat.events.size(); j++) { + final Event event = stat.events.get(j); + if (removedPackage.equals(event.mPackage)) { + fail("Found an event from removed package " + removedPackage); + return; + } + } + } + } + + @Test + public void testPackageRetention() throws IOException { + verifyPackageNotRetained(UsageStatsManager.INTERVAL_DAILY); + verifyPackageNotRetained(UsageStatsManager.INTERVAL_WEEKLY); + verifyPackageNotRetained(UsageStatsManager.INTERVAL_MONTHLY); + verifyPackageNotRetained(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 a783a4007da17..46b261b64192d 100644 --- a/services/usage/java/com/android/server/usage/IntervalStats.java +++ b/services/usage/java/com/android/server/usage/IntervalStats.java @@ -42,8 +42,12 @@ import android.app.usage.EventStats; 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.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; import android.util.proto.ProtoInputStream; import com.android.internal.annotations.VisibleForTesting; @@ -52,6 +56,8 @@ import java.io.IOException; import java.util.List; public class IntervalStats { + private static final String TAG = "IntervalStats"; + public static final int CURRENT_MAJOR_VERSION = 1; public static final int CURRENT_MINOR_VERSION = 1; public int majorVersion = CURRENT_MAJOR_VERSION; @@ -64,6 +70,8 @@ public class IntervalStats { public final EventTracker keyguardShownTracker = new EventTracker(); public final EventTracker keyguardHiddenTracker = new EventTracker(); public final ArrayMap packageStats = new ArrayMap<>(); + /** @hide */ + public final SparseArray packageStatsObfuscated = new SparseArray<>(); public final ArrayMap configurations = new ArrayMap<>(); public Configuration activeConfiguration; public final EventList events = new EventList(); @@ -436,4 +444,234 @@ public class IntervalStats { */ 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.getPackageString(packageToken); + if (usageStats.mPackageName == null) { + Slog.e(TAG, "Unable to parse usage stats package " + packageToken); + continue; + } + + // Update chooser counts + final int chooserActionsSize = usageStats.mChooserCountsObfuscated.size(); + for (int actionIndex = 0; actionIndex < chooserActionsSize; actionIndex++) { + final ArrayMap categoryCountsMap = new ArrayMap<>(); + final int actionToken = usageStats.mChooserCountsObfuscated.keyAt(actionIndex); + final String action = packagesTokenData.getString(packageToken, actionToken); + if (action == null) { + Slog.i(TAG, "Unable to parse chooser action " + actionToken + + " for package " + packageToken); + continue; + } + 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); + if (category == null) { + Slog.i(TAG, "Unable to parse chooser category " + categoryToken + + " for package " + packageToken); + continue; + } + 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) { + for (int i = this.events.size() - 1; i >= 0; i--) { + final Event event = this.events.get(i); + final int packageToken = event.mPackageToken; + event.mPackage = packagesTokenData.getPackageString(packageToken); + if (event.mPackage == null) { + Slog.e(TAG, "Unable to parse event package " + packageToken); + this.events.remove(i); + continue; + } + + if (event.mClassToken != PackagesTokenData.UNASSIGNED_TOKEN) { + event.mClass = packagesTokenData.getString(packageToken, event.mClassToken); + if (event.mClass == null) { + Slog.i(TAG, "Unable to parse class " + event.mClassToken + + " for package " + packageToken); + } + } + if (event.mTaskRootPackageToken != PackagesTokenData.UNASSIGNED_TOKEN) { + event.mTaskRootPackage = packagesTokenData.getString(packageToken, + event.mTaskRootPackageToken); + if (event.mTaskRootPackage == null) { + Slog.i(TAG, "Unable to parse task root package " + event.mTaskRootPackageToken + + " for package " + packageToken); + } + } + if (event.mTaskRootClassToken != PackagesTokenData.UNASSIGNED_TOKEN) { + event.mTaskRootClass = packagesTokenData.getString(packageToken, + event.mTaskRootClassToken); + if (event.mTaskRootClass == null) { + Slog.i(TAG, "Unable to parse task root class " + event.mTaskRootClassToken + + " for package " + packageToken); + } + } + 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); + if (event.mShortcutId == null) { + Slog.e(TAG, "Unable to parse shortcut " + event.mShortcutIdToken + + " for package " + packageToken); + this.events.remove(i); + continue; + } + break; + case NOTIFICATION_INTERRUPTION: + event.mNotificationChannelId = packagesTokenData.getString(packageToken, + event.mNotificationChannelIdToken); + if (event.mNotificationChannelId == null) { + Slog.e(TAG, "Unable to parse notification channel " + + event.mNotificationChannelIdToken + " for package " + + packageToken); + this.events.remove(i); + continue; + } + 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.mEndTimeStamp); + // don't obfuscate stats whose packages have been removed + if (packageToken == PackagesTokenData.UNASSIGNED_TOKEN) { + continue; + } + 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 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) { + for (int i = events.size() - 1; i >= 0; i--) { + final Event event = events.get(i); + if (event == null) { + continue; + } + + final int packageToken = packagesTokenData.getPackageTokenOrAdd( + event.mPackage, event.mTimeStamp); + // don't obfuscate events from packages that have been removed + if (packageToken == PackagesTokenData.UNASSIGNED_TOKEN) { + events.remove(i); + continue; + } + 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 0000000000000..4bf08a49af0f1 --- /dev/null +++ b/services/usage/java/com/android/server/usage/PackagesTokenData.java @@ -0,0 +1,176 @@ +/* + * 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.Slog; +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 package name is always stored at index 0 in {@code tokensToPackagesMap}. + */ + private static final int PACKAGE_NAME_INDEX = 0; + + /** + * The default token for any string that hasn't been tokenized yet. + */ + public static final int UNASSIGNED_TOKEN = -1; + + /** + * 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> 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> packagesToTokensMap = new ArrayMap<>(); + /** + * Stores a map of packages that were removed and when they were removed. + */ + public final ArrayMap removedPackagesMap = 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 + * @param timeStamp the time stamp of the event or end time of the usage stats; used to verify + * the package hasn't been removed + * @return the mapped token + */ + public int getPackageTokenOrAdd(String packageName, long timeStamp) { + final Long timeRemoved = removedPackagesMap.get(packageName); + if (timeRemoved != null && timeRemoved > timeStamp) { + return UNASSIGNED_TOKEN; // package was removed + /* + Note: instead of querying Package Manager each time for a list of packages to verify + if this package is still installed, it's more efficient to check the internal list of + removed packages and verify with the incoming time stamp. Although rare, it is possible + that some asynchronous function is triggered after a package is removed and the + time stamp passed into this function is not accurate. We'll have to keep the respective + event/usage stat until the next time the device reboots and the mappings are cleaned. + Additionally, this is a data class with some helper methods - it doesn't make sense to + overload it with references to other services. + */ + } + + ArrayMap 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 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 package name for the given token. + * + * @param packageToken the package token representing the package name + * @return the string representing the given token or {@code null} if not found + */ + public String getPackageString(int packageToken) { + final ArrayList packageStrings = tokensToPackagesMap.get(packageToken); + if (packageStrings == null) { + return null; + } + return packageStrings.get(PACKAGE_NAME_INDEX); + } + + /** + * 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 or {@code null} if not found + */ + public String getString(int packageToken, int token) { + try { + return tokensToPackagesMap.get(packageToken).get(token); + } catch (NullPointerException npe) { + Slog.e("PackagesTokenData", + "Unable to find tokenized strings for package " + packageToken, npe); + return null; + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /** + * Removes the package from all known mappings. + * + * @param packageName the package to be removed + * @param timeRemoved the time stamp of when the package was removed + */ + public void removePackage(String packageName, long timeRemoved) { + removedPackagesMap.put(packageName, timeRemoved); + + if (!packagesToTokensMap.containsKey(packageName)) { + return; + } + final int packageToken = packagesToTokensMap.get(packageName).get(packageName); + packagesToTokensMap.remove(packageName); + tokensToPackagesMap.delete(packageToken); + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java index 5197b3bad14b2..db7ed1f58c1a1 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -17,12 +17,15 @@ package com.android.server.usage; import android.app.usage.TimeSparseArray; +import android.app.usage.UsageEvents; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.os.Build; import android.os.SystemProperties; +import android.util.ArrayMap; import android.util.AtomicFile; import android.util.Slog; +import android.util.SparseArray; import android.util.TimeUtils; import com.android.internal.annotations.VisibleForTesting; @@ -75,7 +78,7 @@ import java.util.List; * 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 +96,8 @@ public class UsageStatsDatabase { // 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 +123,11 @@ public class UsageStatsDatabase { 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. + final PackagesTokenData mPackagesTokenData = new PackagesTokenData(); + /** * UsageStatsDatabase constructor that allows setting the version number. * This should only be used for testing. @@ -138,6 +147,7 @@ public class UsageStatsDatabase { mBackupsDir = new File(dir, "backups"); mUpdateBreadcrumb = new File(dir, "breadcrumb"); mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length]; + mPackageMappingsFile = new File(dir, "mappings"); mCal = new UnixCalendar(0); } @@ -479,6 +489,11 @@ public class UsageStatsDatabase { 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 +509,16 @@ public class UsageStatsDatabase { } 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 +526,21 @@ public class UsageStatsDatabase { } } } + + // 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."); + } + } + } + + void onPackageRemoved(String packageName, long timeRemoved) { + synchronized (mLock) { + mPackagesTokenData.removePackage(packageName, timeRemoved); + } } public void onTimeChanged(long timeDiffMillis) { @@ -579,6 +616,37 @@ public class UsageStatsDatabase { return null; } + /** + * Filter out those stats from the given stats that belong to removed packages. Filtering out + * all of the stats at once has an amortized cost for future calls. + */ + void filterStats(IntervalStats stats) { + if (mPackagesTokenData.removedPackagesMap.isEmpty()) { + return; + } + final ArrayMap removedPackagesMap = mPackagesTokenData.removedPackagesMap; + + // filter out package usage stats + final int removedPackagesSize = removedPackagesMap.size(); + for (int i = 0; i < removedPackagesSize; i++) { + final String removedPackage = removedPackagesMap.keyAt(i); + final UsageStats usageStats = stats.packageStats.get(removedPackage); + if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) { + stats.packageStats.remove(removedPackage); + } + } + + // filter out events + final int eventsSize = stats.events.size(); + for (int i = stats.events.size() - 1; i >= 0; i--) { + final UsageEvents.Event event = stats.events.get(i); + final Long timeRemoved = removedPackagesMap.get(event.mPackage); + if (timeRemoved != null && timeRemoved > event.mTimeStamp) { + stats.events.remove(i); + } + } + } + /** * Figures out what to extract from the given IntervalStats object. */ @@ -808,14 +876,14 @@ public class UsageStatsDatabase { } 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 +893,11 @@ public class UsageStatsDatabase { } 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: @@ -837,7 +905,19 @@ public class UsageStatsDatabase { UsageStatsXml.write(out, stats); break; case 4: - UsageStatsProto.write(out, stats); + try { + UsageStatsProto.write(out, stats); + } catch (IOException | IllegalArgumentException e) { + Slog.e(TAG, "Unable to write interval stats to proto.", e); + } + break; + case 5: + stats.obfuscateData(packagesTokenData); + try { + UsageStatsProtoV2.write(out, stats); + } catch (IOException | IllegalArgumentException e) { + Slog.e(TAG, "Unable to write interval stats to proto.", e); + } break; default: throw new RuntimeException( @@ -847,16 +927,16 @@ public class UsageStatsDatabase { } 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 +952,11 @@ public class UsageStatsDatabase { } 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: @@ -884,7 +964,19 @@ public class UsageStatsDatabase { UsageStatsXml.read(in, statsOut); break; case 4: - UsageStatsProto.read(in, statsOut); + try { + UsageStatsProto.read(in, statsOut); + } catch (IOException e) { + Slog.e(TAG, "Unable to read interval stats from proto.", e); + } + break; + case 5: + try { + UsageStatsProtoV2.read(in, statsOut); + } catch (IOException e) { + Slog.e(TAG, "Unable to read interval stats from proto.", e); + } + statsOut.deobfuscateData(packagesTokenData); break; default: throw new RuntimeException( @@ -894,6 +986,63 @@ public class UsageStatsDatabase { } + /** + * Reads the obfuscated data file from disk containing the tokens to packages mappings and + * rebuilds the packages to tokens mappings based on that data. + */ + public void readMappingsLocked() { + if (!mPackageMappingsFile.exists()) { + return; // package mappings file is missing - recreate mappings on next write. + } + + try (FileInputStream in = new AtomicFile(mPackageMappingsFile).openRead()) { + UsageStatsProtoV2.readObfuscatedData(in, mPackagesTokenData); + } catch (IOException e) { + Slog.e(TAG, "Failed to read the obfuscated packages mapping file.", e); + return; + } + + final SparseArray> tokensToPackagesMap = + mPackagesTokenData.tokensToPackagesMap; + final int tokensToPackagesMapSize = tokensToPackagesMap.size(); + for (int i = 0; i < tokensToPackagesMapSize; i++) { + final int packageToken = tokensToPackagesMap.keyAt(i); + final ArrayList tokensMap = tokensToPackagesMap.valueAt(i); + final ArrayMap packageStringsMap = new ArrayMap<>(); + final int tokensMapSize = tokensMap.size(); + // package name will always be at index 0 but its token should not be 0 + packageStringsMap.put(tokensMap.get(0), packageToken); + for (int j = 1; j < tokensMapSize; j++) { + packageStringsMap.put(tokensMap.get(j), j); + } + mPackagesTokenData.packagesToTokensMap.put(tokensMap.get(0), packageStringsMap); + } + } + + void writeMappingsLocked() throws IOException { + final AtomicFile file = new AtomicFile(mPackageMappingsFile); + FileOutputStream fos = file.startWrite(); + try { + UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData); + file.finishWrite(fos); + fos = null; + } catch (IOException | IllegalArgumentException e) { + Slog.e(TAG, "Unable to write obfuscated data to proto.", e); + } finally { + file.failWrite(fos); + } + } + + void obfuscateCurrentStats(IntervalStats[] currentStats) { + if (mCurrentVersion < 5) { + return; + } + for (int i = 0; i < currentStats.length; i++) { + final IntervalStats stats = currentStats[i]; + stats.obfuscateData(mPackagesTokenData); + } + } + /** * Update the stats in the database. They may not be written to disk immediately. */ @@ -1098,7 +1247,7 @@ public class UsageStatsDatabase { 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 +1261,7 @@ public class UsageStatsDatabase { 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 +1291,18 @@ public class UsageStatsDatabase { } /** - * 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 files = mSortedStatFiles[i]; final int size = files.size(); @@ -1173,6 +1327,23 @@ public class UsageStatsDatabase { } } + 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 6d3f416065a37..5d1f730694ef2 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsProto.java +++ b/services/usage/java/com/android/server/usage/UsageStatsProto.java @@ -120,10 +120,14 @@ final class UsageStatsProto { IntervalStatsProto.UsageStats.APP_LAUNCH_COUNT); break; case (int) IntervalStatsProto.UsageStats.CHOOSER_ACTIONS: - final long chooserToken = proto.start( - IntervalStatsProto.UsageStats.CHOOSER_ACTIONS); - loadChooserCounts(proto, stats); - proto.end(chooserToken); + try { + final long chooserToken = proto.start( + IntervalStatsProto.UsageStats.CHOOSER_ACTIONS); + loadChooserCounts(proto, stats); + proto.end(chooserToken); + } catch (IOException e) { + Slog.e(TAG, "Unable to read chooser counts for " + stats.mPackageName, e); + } break; case (int) IntervalStatsProto.UsageStats.LAST_TIME_SERVICE_USED_MS: // Time attributes stored is an offset of the beginTime. @@ -153,20 +157,24 @@ final class UsageStatsProto { } 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) IntervalStatsProto.CountAndTime.COUNT: - tracker.count = proto.readInt(IntervalStatsProto.CountAndTime.COUNT); - break; - case (int) IntervalStatsProto.CountAndTime.TIME_MS: - tracker.duration = proto.readLong(IntervalStatsProto.CountAndTime.TIME_MS); - break; - case ProtoInputStream.NO_MORE_FIELDS: - proto.end(token); - return; + IntervalStats.EventTracker tracker) { + try { + final long token = proto.start(fieldId); + while (true) { + switch (proto.nextField()) { + case (int) IntervalStatsProto.CountAndTime.COUNT: + tracker.count = proto.readInt(IntervalStatsProto.CountAndTime.COUNT); + break; + case (int) IntervalStatsProto.CountAndTime.TIME_MS: + tracker.duration = proto.readLong(IntervalStatsProto.CountAndTime.TIME_MS); + break; + case ProtoInputStream.NO_MORE_FIELDS: + proto.end(token); + return; + } } + } catch (IOException e) { + Slog.e(TAG, "Unable to read event tracker " + fieldId, e); } } @@ -306,7 +314,7 @@ final class UsageStatsProto { } private static void writeStringPool(ProtoOutputStream proto, final IntervalStats stats) - throws IOException { + throws IllegalArgumentException { final long token = proto.start(IntervalStatsProto.STRINGPOOL); final int size = stats.mStringCache.size(); proto.write(IntervalStatsProto.StringPool.SIZE, size); @@ -317,7 +325,8 @@ final class UsageStatsProto { } private static void writeUsageStats(ProtoOutputStream proto, long fieldId, - final IntervalStats stats, final UsageStats usageStats) throws IOException { + final IntervalStats stats, final UsageStats usageStats) + throws IllegalArgumentException { final long token = proto.start(fieldId); // Write the package name first, so loadUsageStats can avoid creating an extra object final int packageIndex = stats.mStringCache.indexOf(usageStats.mPackageName); @@ -347,12 +356,16 @@ final class UsageStatsProto { proto.write(IntervalStatsProto.UsageStats.TOTAL_TIME_VISIBLE_MS, usageStats.mTotalTimeVisible); proto.write(IntervalStatsProto.UsageStats.APP_LAUNCH_COUNT, usageStats.mAppLaunchCount); - writeChooserCounts(proto, usageStats); + try { + writeChooserCounts(proto, usageStats); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write chooser counts for " + usageStats.mPackageName, e); + } proto.end(token); } private static void writeCountAndTime(ProtoOutputStream proto, long fieldId, int count, - long time) throws IOException { + long time) throws IllegalArgumentException { final long token = proto.start(fieldId); proto.write(IntervalStatsProto.CountAndTime.COUNT, count); proto.write(IntervalStatsProto.CountAndTime.TIME_MS, time); @@ -361,7 +374,7 @@ final class UsageStatsProto { private static void writeChooserCounts(ProtoOutputStream proto, final UsageStats usageStats) - throws IOException { + throws IllegalArgumentException { if (usageStats == null || usageStats.mChooserCounts == null || usageStats.mChooserCounts.keySet().isEmpty()) { return; @@ -381,7 +394,7 @@ final class UsageStatsProto { } private static void writeCountsForAction(ProtoOutputStream proto, - ArrayMap counts) throws IOException { + ArrayMap counts) throws IllegalArgumentException { final int countsSize = counts.size(); for (int i = 0; i < countsSize; i++) { String key = counts.keyAt(i); @@ -397,7 +410,7 @@ final class UsageStatsProto { private static void writeConfigStats(ProtoOutputStream proto, long fieldId, final IntervalStats stats, final ConfigurationStats configStats, boolean isActive) - throws IOException { + throws IllegalArgumentException { final long token = proto.start(fieldId); configStats.mConfiguration.writeToProto(proto, IntervalStatsProto.Configuration.CONFIG); proto.write(IntervalStatsProto.Configuration.LAST_TIME_ACTIVE_MS, @@ -407,11 +420,10 @@ final class UsageStatsProto { 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, - final UsageEvents.Event event) throws IOException { + final UsageEvents.Event event) throws IllegalArgumentException { final long token = proto.start(fieldId); final int packageIndex = stats.mStringCache.indexOf(event.mPackage); if (packageIndex >= 0) { @@ -542,17 +554,33 @@ final class UsageStatsProto { statsOut.keyguardHiddenTracker); break; case (int) IntervalStatsProto.STRINGPOOL: - stringPool = readStringPool(proto); - statsOut.mStringCache.addAll(stringPool); + try { + stringPool = readStringPool(proto); + statsOut.mStringCache.addAll(stringPool); + } catch (IOException e) { + Slog.e(TAG, "Unable to read string pool from proto.", e); + } break; case (int) IntervalStatsProto.PACKAGES: - loadUsageStats(proto, IntervalStatsProto.PACKAGES, statsOut, stringPool); + try { + loadUsageStats(proto, IntervalStatsProto.PACKAGES, statsOut, stringPool); + } catch (IOException e) { + Slog.e(TAG, "Unable to read some usage stats from proto.", e); + } break; case (int) IntervalStatsProto.CONFIGURATIONS: - loadConfigStats(proto, IntervalStatsProto.CONFIGURATIONS, statsOut); + try { + loadConfigStats(proto, IntervalStatsProto.CONFIGURATIONS, statsOut); + } catch (IOException e) { + Slog.e(TAG, "Unable to read some configuration stats from proto.", e); + } break; case (int) IntervalStatsProto.EVENT_LOG: - loadEvent(proto, IntervalStatsProto.EVENT_LOG, statsOut, stringPool); + try { + loadEvent(proto, IntervalStatsProto.EVENT_LOG, statsOut, stringPool); + } catch (IOException e) { + Slog.e(TAG, "Unable to read some events from proto.", e); + } break; case ProtoInputStream.NO_MORE_FIELDS: if (statsOut.endTime == 0) { @@ -571,72 +599,60 @@ final class UsageStatsProto { * @param proto The serializer to which to write the packageStats data. * @param stats The stats object to write to the XML file. */ - public static void write(OutputStream out, IntervalStats stats) throws IOException { + public static void write(OutputStream out, IntervalStats stats) + throws IOException, IllegalArgumentException { final ProtoOutputStream proto = new ProtoOutputStream(out); proto.write(IntervalStatsProto.END_TIME_MS, stats.endTime - stats.beginTime); proto.write(IntervalStatsProto.MAJOR_VERSION, stats.majorVersion); proto.write(IntervalStatsProto.MINOR_VERSION, stats.minorVersion); // String pool should be written before the rest of the usage stats - writeStringPool(proto, stats); + try { + writeStringPool(proto, stats); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write string pool to proto.", e); + } - writeCountAndTime(proto, IntervalStatsProto.INTERACTIVE, stats.interactiveTracker.count, - stats.interactiveTracker.duration); - writeCountAndTime(proto, IntervalStatsProto.NON_INTERACTIVE, - stats.nonInteractiveTracker.count, stats.nonInteractiveTracker.duration); - writeCountAndTime(proto, IntervalStatsProto.KEYGUARD_SHOWN, - stats.keyguardShownTracker.count, stats.keyguardShownTracker.duration); - writeCountAndTime(proto, IntervalStatsProto.KEYGUARD_HIDDEN, - stats.keyguardHiddenTracker.count, stats.keyguardHiddenTracker.duration); + try { + writeCountAndTime(proto, IntervalStatsProto.INTERACTIVE, stats.interactiveTracker.count, + stats.interactiveTracker.duration); + writeCountAndTime(proto, IntervalStatsProto.NON_INTERACTIVE, + stats.nonInteractiveTracker.count, stats.nonInteractiveTracker.duration); + writeCountAndTime(proto, IntervalStatsProto.KEYGUARD_SHOWN, + stats.keyguardShownTracker.count, stats.keyguardShownTracker.duration); + writeCountAndTime(proto, IntervalStatsProto.KEYGUARD_HIDDEN, + stats.keyguardHiddenTracker.count, stats.keyguardHiddenTracker.duration); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some interval stats trackers to proto.", e); + } final int statsCount = stats.packageStats.size(); for (int i = 0; i < statsCount; i++) { - writeUsageStats(proto, IntervalStatsProto.PACKAGES, stats, - stats.packageStats.valueAt(i)); + try { + writeUsageStats(proto, IntervalStatsProto.PACKAGES, stats, + stats.packageStats.valueAt(i)); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some usage stats to proto.", e); + } } final int configCount = stats.configurations.size(); for (int i = 0; i < configCount; i++) { boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i)); - writeConfigStats(proto, IntervalStatsProto.CONFIGURATIONS, stats, - stats.configurations.valueAt(i), active); + try { + writeConfigStats(proto, IntervalStatsProto.CONFIGURATIONS, stats, + stats.configurations.valueAt(i), active); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some configuration stats to proto.", e); + } } final int eventCount = stats.events.size(); for (int i = 0; i < eventCount; i++) { - writeEvent(proto, IntervalStatsProto.EVENT_LOG, stats, stats.events.get(i)); - } - - proto.flush(); - } - - // TODO: move to UsageStatsProtoV2 - static void readPendingEvents(InputStream in, List events) - throws IOException { - final ProtoInputStream proto = new ProtoInputStream(in); - final List 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; + try { + writeEvent(proto, IntervalStatsProto.EVENT_LOG, stats, stats.events.get(i)); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some events to proto.", e); } } - } - // TODO: move to UsageStatsProtoV2 - static void writePendingEvents(OutputStream out, List 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 0000000000000..7d8e430e54165 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsProtoV2.java @@ -0,0 +1,788 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.usage; + +import android.app.usage.ConfigurationStats; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; +import android.content.res.Configuration; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.LinkedList; + +/** + * 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: + try { + final long token = proto.start(UsageStatsObfuscatedProto.CHOOSER_ACTIONS); + loadChooserCounts(proto, stats); + proto.end(token); + } catch (IOException e) { + Slog.e(TAG, "Unable to read chooser counts for " + stats.mPackageToken); + } + break; + case (int) UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS: + stats.mLastTimeForegroundServiceUsed = beginTime + proto.readLong( + UsageStatsObfuscatedProto.LAST_TIME_SERVICE_USED_MS); + break; + case (int) UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS: + stats.mTotalTimeForegroundServiceUsed = proto.readLong( + UsageStatsObfuscatedProto.TOTAL_TIME_SERVICE_USED_MS); + break; + case (int) UsageStatsObfuscatedProto.LAST_TIME_VISIBLE_MS: + stats.mLastTimeVisible = beginTime + proto.readLong( + UsageStatsObfuscatedProto.LAST_TIME_VISIBLE_MS); + break; + case (int) UsageStatsObfuscatedProto.TOTAL_TIME_VISIBLE_MS: + stats.mTotalTimeVisible = proto.readLong( + UsageStatsObfuscatedProto.TOTAL_TIME_VISIBLE_MS); + break; + case ProtoInputStream.NO_MORE_FIELDS: + // 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) { + try { + final long token = proto.start(fieldId); + while (true) { + switch (proto.nextField()) { + case (int) IntervalStatsObfuscatedProto.CountAndTime.COUNT: + tracker.count = proto.readInt( + IntervalStatsObfuscatedProto.CountAndTime.COUNT); + break; + case (int) IntervalStatsObfuscatedProto.CountAndTime.TIME_MS: + tracker.duration = proto.readLong( + IntervalStatsObfuscatedProto.CountAndTime.TIME_MS); + break; + case ProtoInputStream.NO_MORE_FIELDS: + proto.end(token); + return; + } + } + } catch (IOException e) { + Slog.e(TAG, "Unable to read event tracker " + fieldId, e); + } + } + + private static void loadChooserCounts(ProtoInputStream proto, UsageStats usageStats) + throws IOException { + int actionToken; + SparseIntArray counts; + if (proto.nextField(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN)) { + // Fast path; this should work for most cases since the action token is written first + actionToken = proto.readInt(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN) - 1; + counts = usageStats.mChooserCountsObfuscated.get(actionToken); + if (counts == null) { + counts = new SparseIntArray(); + usageStats.mChooserCountsObfuscated.put(actionToken, counts); + } + } else { + counts = new SparseIntArray(); + } + + while (true) { + switch (proto.nextField()) { + case (int) UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN: + // Fast path failed for some reason, add the SparseIntArray object to usageStats + actionToken = proto.readInt( + UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN) - 1; + usageStats.mChooserCountsObfuscated.put(actionToken, counts); + break; + case (int) UsageStatsObfuscatedProto.ChooserAction.COUNTS: + final long token = proto.start(UsageStatsObfuscatedProto.ChooserAction.COUNTS); + loadCountsForAction(proto, counts); + proto.end(token); + break; + case ProtoInputStream.NO_MORE_FIELDS: + return; // if the action was never read, the loaded counts will be ignored. + } + } + } + + private static void loadCountsForAction(ProtoInputStream proto, SparseIntArray counts) + throws IOException { + int categoryToken = PackagesTokenData.UNASSIGNED_TOKEN; + int count = 0; + while (true) { + switch (proto.nextField()) { + case (int) UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN: + categoryToken = proto.readInt( + UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN) + - 1; + break; + case (int) UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT: + count = proto.readInt( + UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT); + break; + case ProtoInputStream.NO_MORE_FIELDS: + if (categoryToken != PackagesTokenData.UNASSIGNED_TOKEN) { + counts.put(categoryToken, count); + } + return; + } + } + } + + private static void loadConfigStats(ProtoInputStream proto, IntervalStats stats) + throws IOException { + boolean configActive = false; + final Configuration config = new Configuration(); + ConfigurationStats configStats = new ConfigurationStats(); + if (proto.nextField(IntervalStatsObfuscatedProto.Configuration.CONFIG)) { + // Fast path; this should work since the configuration is written first + config.readFromProto(proto, IntervalStatsObfuscatedProto.Configuration.CONFIG); + configStats = stats.getOrCreateConfigurationStats(config); + } + + while (true) { + switch (proto.nextField()) { + case (int) IntervalStatsObfuscatedProto.Configuration.CONFIG: + // Fast path failed from some reason, add ConfigStats object to statsOut now + config.readFromProto(proto, IntervalStatsObfuscatedProto.Configuration.CONFIG); + final ConfigurationStats temp = stats.getOrCreateConfigurationStats(config); + temp.mLastTimeActive = configStats.mLastTimeActive; + temp.mTotalTimeActive = configStats.mTotalTimeActive; + temp.mActivationCount = configStats.mActivationCount; + configStats = temp; + break; + case (int) IntervalStatsObfuscatedProto.Configuration.LAST_TIME_ACTIVE_MS: + configStats.mLastTimeActive = stats.beginTime + proto.readLong( + IntervalStatsObfuscatedProto.Configuration.LAST_TIME_ACTIVE_MS); + break; + case (int) IntervalStatsObfuscatedProto.Configuration.TOTAL_TIME_ACTIVE_MS: + configStats.mTotalTimeActive = proto.readLong( + IntervalStatsObfuscatedProto.Configuration.TOTAL_TIME_ACTIVE_MS); + break; + case (int) IntervalStatsObfuscatedProto.Configuration.COUNT: + configStats.mActivationCount = proto.readInt( + IntervalStatsObfuscatedProto.Configuration.COUNT); + break; + case (int) IntervalStatsObfuscatedProto.Configuration.ACTIVE: + configActive = proto.readBoolean( + IntervalStatsObfuscatedProto.Configuration.ACTIVE); + break; + case ProtoInputStream.NO_MORE_FIELDS: + // 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 IllegalArgumentException { + // 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); + try { + writeChooserCounts(proto, stats); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write chooser counts for " + stats.mPackageName, e); + } + } + + private static void writeCountAndTime(ProtoOutputStream proto, long fieldId, int count, + long time) throws IllegalArgumentException { + final long token = proto.start(fieldId); + proto.write(IntervalStatsObfuscatedProto.CountAndTime.COUNT, count); + proto.write(IntervalStatsObfuscatedProto.CountAndTime.TIME_MS, time); + proto.end(token); + } + + private static void writeChooserCounts(ProtoOutputStream proto, final UsageStats stats) + throws IllegalArgumentException { + if (stats == null || stats.mChooserCountsObfuscated.size() == 0) { + return; + } + final int chooserCountSize = stats.mChooserCountsObfuscated.size(); + for (int i = 0; i < chooserCountSize; i++) { + final int action = stats.mChooserCountsObfuscated.keyAt(i); + final SparseIntArray counts = stats.mChooserCountsObfuscated.valueAt(i); + if (counts == null || counts.size() == 0) { + continue; + } + final long token = proto.start(UsageStatsObfuscatedProto.CHOOSER_ACTIONS); + proto.write(UsageStatsObfuscatedProto.ChooserAction.ACTION_TOKEN, action + 1); + writeCountsForAction(proto, counts); + proto.end(token); + } + } + + private static void writeCountsForAction(ProtoOutputStream proto, SparseIntArray counts) + throws IllegalArgumentException { + final int countsSize = counts.size(); + for (int i = 0; i < countsSize; i++) { + final int category = counts.keyAt(i); + final int count = counts.valueAt(i); + if (count <= 0) { + continue; + } + final long token = proto.start(UsageStatsObfuscatedProto.ChooserAction.COUNTS); + proto.write(UsageStatsObfuscatedProto.ChooserAction.CategoryCount.CATEGORY_TOKEN, + category + 1); + proto.write(UsageStatsObfuscatedProto.ChooserAction.CategoryCount.COUNT, count); + proto.end(token); + } + } + + private static void writeConfigStats(ProtoOutputStream proto, final long statsBeginTime, + final ConfigurationStats configStats, boolean isActive) + throws IllegalArgumentException { + configStats.mConfiguration.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 IllegalArgumentException { + 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: + try { + 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); + } catch (IOException e) { + Slog.e(TAG, "Unable to read some usage stats from proto.", e); + } + break; + case (int) IntervalStatsObfuscatedProto.CONFIGURATIONS: + try { + final long configsToken = proto.start( + IntervalStatsObfuscatedProto.CONFIGURATIONS); + loadConfigStats(proto, stats); + proto.end(configsToken); + } catch (IOException e) { + Slog.e(TAG, "Unable to read some configuration stats from proto.", e); + } + break; + case (int) IntervalStatsObfuscatedProto.EVENT_LOG: + try { + final long eventsToken = proto.start( + IntervalStatsObfuscatedProto.EVENT_LOG); + UsageEvents.Event event = parseEvent(proto, stats.beginTime); + proto.end(eventsToken); + if (event != null) { + stats.events.insert(event); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to read some events from proto.", e); + } + break; + case ProtoInputStream.NO_MORE_FIELDS: + // 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, IllegalArgumentException { + 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); + + try { + writeCountAndTime(proto, IntervalStatsObfuscatedProto.INTERACTIVE, + stats.interactiveTracker.count, stats.interactiveTracker.duration); + writeCountAndTime(proto, IntervalStatsObfuscatedProto.NON_INTERACTIVE, + stats.nonInteractiveTracker.count, stats.nonInteractiveTracker.duration); + writeCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_SHOWN, + stats.keyguardShownTracker.count, stats.keyguardShownTracker.duration); + writeCountAndTime(proto, IntervalStatsObfuscatedProto.KEYGUARD_HIDDEN, + stats.keyguardHiddenTracker.count, stats.keyguardHiddenTracker.duration); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some interval stats trackers to proto.", e); + } + + final int statsCount = stats.packageStatsObfuscated.size(); + for (int i = 0; i < statsCount; i++) { + try { + final long token = proto.start(IntervalStatsObfuscatedProto.PACKAGES); + writeUsageStats(proto, stats.beginTime, stats.packageStatsObfuscated.valueAt(i)); + proto.end(token); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some usage stats to proto.", e); + } + } + final int configCount = stats.configurations.size(); + for (int i = 0; i < configCount; i++) { + boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i)); + try { + final long token = proto.start(IntervalStatsObfuscatedProto.CONFIGURATIONS); + writeConfigStats(proto, stats.beginTime, stats.configurations.valueAt(i), active); + proto.end(token); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some configuration stats to proto.", e); + } + } + final int eventCount = stats.events.size(); + for (int i = 0; i < eventCount; i++) { + try { + final long token = proto.start(IntervalStatsObfuscatedProto.EVENT_LOG); + writeEvent(proto, stats.beginTime, stats.events.get(i)); + proto.end(token); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some events to proto.", e); + } + } + + proto.flush(); + } + + /***** Read/Write obfuscated packages data logic. *****/ + + private static void loadPackagesMap(ProtoInputStream proto, + SparseArray> tokensToPackagesMap) throws IOException { + int key = PackagesTokenData.UNASSIGNED_TOKEN; + final ArrayList strings = new ArrayList<>(); + while (true) { + switch (proto.nextField()) { + case (int) ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN: + key = proto.readInt(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN) - 1; + break; + case (int) ObfuscatedPackagesProto.PackagesMap.STRINGS: + strings.add(proto.readString(ObfuscatedPackagesProto.PackagesMap.STRINGS)); + break; + case ProtoInputStream.NO_MORE_FIELDS: + if (key != PackagesTokenData.UNASSIGNED_TOKEN) { + tokensToPackagesMap.put(key, strings); + } + return; + } + } + } + + /** + * Populates the package mappings from the input stream given. + * + * @param in the input stream from which to read the mappings. + * @param packagesTokenData the packages data object to which the data will be read to. + */ + static void readObfuscatedData(InputStream in, PackagesTokenData packagesTokenData) + throws IOException { + final ProtoInputStream proto = new ProtoInputStream(in); + while (true) { + switch (proto.nextField()) { + case (int) ObfuscatedPackagesProto.COUNTER: + packagesTokenData.counter = proto.readInt(ObfuscatedPackagesProto.COUNTER); + break; + case (int) ObfuscatedPackagesProto.PACKAGES_MAP: + final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP); + loadPackagesMap(proto, packagesTokenData.tokensToPackagesMap); + proto.end(token); + break; + case ProtoInputStream.NO_MORE_FIELDS: + return; + } + } + } + + /** + * Writes the packages mapping data to a ProtoBuf file. + * + * @param out the output stream to which to write the mappings. + * @param packagesTokenData the packages data object holding the data to write. + */ + static void writeObfuscatedData(OutputStream out, PackagesTokenData packagesTokenData) + throws IOException, IllegalArgumentException { + final ProtoOutputStream proto = new ProtoOutputStream(out); + proto.write(ObfuscatedPackagesProto.COUNTER, packagesTokenData.counter); + + final int mapSize = packagesTokenData.tokensToPackagesMap.size(); + for (int i = 0; i < mapSize; i++) { + final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP); + int packageToken = packagesTokenData.tokensToPackagesMap.keyAt(i); + proto.write(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN, packageToken + 1); + + final ArrayList 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 events) + throws IOException { + final ProtoInputStream proto = new ProtoInputStream(in); + while (true) { + switch (proto.nextField()) { + case (int) IntervalStatsObfuscatedProto.PENDING_EVENTS: + try { + final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS); + UsageEvents.Event event = parsePendingEvent(proto); + proto.end(token); + if (event != null) { + events.add(event); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to parse some pending events from proto.", e); + } + break; + case ProtoInputStream.NO_MORE_FIELDS: + return; + } + } + } + + private static void writePendingEvent(ProtoOutputStream proto, UsageEvents.Event event) + throws IllegalArgumentException { + proto.write(PendingEventProto.PACKAGE_NAME, event.mPackage); + if (event.mClass != null) { + proto.write(PendingEventProto.CLASS_NAME, event.mClass); + } + proto.write(PendingEventProto.TIME_MS, event.mTimeStamp); + proto.write(PendingEventProto.FLAGS, event.mFlags); + proto.write(PendingEventProto.TYPE, event.mEventType); + proto.write(PendingEventProto.INSTANCE_ID, event.mInstanceId); + if (event.mTaskRootPackage != null) { + proto.write(PendingEventProto.TASK_ROOT_PACKAGE, event.mTaskRootPackage); + } + if (event.mTaskRootClass != null) { + proto.write(PendingEventProto.TASK_ROOT_CLASS, event.mTaskRootClass); + } + switch (event.mEventType) { + case UsageEvents.Event.CONFIGURATION_CHANGE: + if (event.mConfiguration != null) { + event.mConfiguration.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; + } + } + + /** + * 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 events) + throws IOException, IllegalArgumentException { + final ProtoOutputStream proto = new ProtoOutputStream(out); + final int eventCount = events.size(); + for (int i = 0; i < eventCount; i++) { + try { + final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS); + writePendingEvent(proto, events.get(i)); + proto.end(token); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Unable to write some pending events to proto.", e); + } + } + proto.flush(); + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 2cd207f72d6fa..5d03e151e993b 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -141,6 +141,7 @@ public class UsageStatsService extends SystemService implements static final int MSG_UID_STATE_CHANGED = 3; static final int MSG_REPORT_EVENT_TO_ALL_USERID = 4; static final int MSG_UNLOCKED_USER = 5; + static final int MSG_PACKAGE_REMOVED = 6; private final Object mLock = new Object(); Handler mHandler; @@ -148,7 +149,6 @@ public class UsageStatsService extends SystemService implements UserManager mUserManager; PackageManager mPackageManager; PackageManagerInternal mPackageManagerInternal; - PackageMonitor mPackageMonitor; IDeviceIdleController mDeviceIdleController; // Do not use directly. Call getDpmInternal() instead DevicePolicyManagerInternal mDpmInternal; @@ -164,6 +164,8 @@ public class UsageStatsService extends SystemService implements /** Manages app time limit observers */ AppTimeLimitController mAppTimeLimit; + private final PackageMonitor mPackageMonitor = new MyPackageMonitor(); + // A map maintaining a queue of events to be reported per user. private final SparseArray> mReportedEvents = new SparseArray<>(); final SparseArray> mUsageReporters = new SparseArray(); @@ -243,6 +245,8 @@ public class UsageStatsService extends SystemService implements mAppStandby.addListener(mStandbyChangeListener); + mPackageMonitor.register(getContext(), null, UserHandle.ALL, true); + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_STARTED); getContext().registerReceiverAsUser(new UserActionsReceiver(), UserHandle.ALL, filter, @@ -617,7 +621,7 @@ public class UsageStatsService extends SystemService implements 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 - @@ -647,11 +651,11 @@ public class UsageStatsService extends SystemService implements FileOutputStream fos = null; try { fos = af.startWrite(); - UsageStatsProto.writePendingEvents(fos, pendingEvents); + UsageStatsProtoV2.writePendingEvents(fos, pendingEvents); af.finishWrite(fos); fos = null; pendingEvents.clear(); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Slog.e(TAG, "Failed to write " + pendingEventsFile.getAbsolutePath() + " for user " + userId); } finally { @@ -842,6 +846,26 @@ public class UsageStatsService extends SystemService implements } } + /** + * Called by the Handler for message MSG_PACKAGE_REMOVED. + */ + private void onPackageRemoved(int userId, String packageName) { + synchronized (mLock) { + final long timeRemoved = System.currentTimeMillis(); + if (!mUserUnlockedStates.get(userId, false)) { + // If user is not unlocked and a package is removed for them, we will handle it + // when the user service is initialized and package manager is queried. + return; + } + final UserUsageStatsService userService = mUserState.get(userId); + if (userService == null) { + return; + } + + userService.onPackageRemoved(packageName, timeRemoved); + } + } + /** * Called by the Binder stub. */ @@ -1030,21 +1054,13 @@ public class UsageStatsService extends SystemService implements 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)) { @@ -1059,19 +1075,11 @@ public class UsageStatsService extends SystemService implements 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)) { @@ -1079,15 +1087,18 @@ public class UsageStatsService extends SystemService implements 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()); + } + 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); } - ipw.println(new File(Environment.getDataSystemCeDirectory(userId), - "usagestats").getAbsolutePath()); return; } else if (arg != null && !arg.startsWith("-")) { // Anything else that doesn't start with '-' is a pkg to filter @@ -1126,6 +1137,21 @@ public class UsageStatsService extends SystemService implements } } + 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); @@ -1157,7 +1183,9 @@ public class UsageStatsService extends SystemService implements case MSG_REMOVE_USER: onUserRemoved(msg.arg1); break; - + case MSG_PACKAGE_REMOVED: + onPackageRemoved(msg.arg1, (String) msg.obj); + break; case MSG_UID_STATE_CHANGED: { final int uid = msg.arg1; final int procState = msg.arg2; @@ -2105,4 +2133,13 @@ public class UsageStatsService extends SystemService implements return mAppTimeLimit.getAppUsageLimit(packageName, user); } } + + private class MyPackageMonitor extends PackageMonitor { + @Override + public void onPackageRemoved(String packageName, int uid) { + mHandler.obtainMessage(MSG_PACKAGE_REMOVED, getChangingUserId(), 0, packageName) + .sendToTarget(); + super.onPackageRemoved(packageName, uid); + } + } } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index 1560b9e708e3b..23df1c553de27 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -34,7 +34,10 @@ import android.app.usage.UsageEvents.Event; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; +import android.os.Process; import android.os.SystemClock; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -44,6 +47,7 @@ import android.util.Slog; import android.util.SparseIntArray; import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; import com.android.server.usage.UsageStatsDatabase.StatCombiner; import java.io.File; @@ -51,6 +55,7 @@ import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; /** @@ -108,6 +113,7 @@ class UserUsageStatsService { } void init(final long currentTimeMillis) { + readPackageMappingsLocked(); mDatabase.init(currentTimeMillis); int nullCount = 0; @@ -169,6 +175,54 @@ class UserUsageStatsService { persistActiveStats(); } + void onPackageRemoved(String packageName, long timeRemoved) { + mDatabase.onPackageRemoved(packageName, timeRemoved); + } + + private void readPackageMappingsLocked() { + mDatabase.readMappingsLocked(); + cleanUpPackageMappingsLocked(); + } + + /** + * Queries Package Manager for a list of installed packages and removes those packages from + * mPackagesTokenData which are not installed any more. + * This will only happen once per device boot, when the user is unlocked for the first time. + */ + private void cleanUpPackageMappingsLocked() { + final long timeNow = System.currentTimeMillis(); + /* + Note (b/142501248): PackageManagerInternal#getInstalledApplications is not lightweight. + Once its implementation is updated, or it's replaced with a better alternative, update + the call here to use it. For now, using the heavy #getInstalledApplications is okay since + this clean-up is only performed once every boot. + */ + final PackageManagerInternal packageManagerInternal = + LocalServices.getService(PackageManagerInternal.class); + if (packageManagerInternal == null) { + return; + } + final List installedPackages = + packageManagerInternal.getInstalledApplications(0, mUserId, Process.SYSTEM_UID); + // convert the package list to a set for easy look-ups + final HashSet packagesSet = new HashSet<>(installedPackages.size()); + for (int i = installedPackages.size() - 1; i >= 0; i--) { + packagesSet.add(installedPackages.get(i).packageName); + } + final List removedPackages = new ArrayList<>(); + // populate list of packages that are found in the mappings but not in the installed list + for (int i = mDatabase.mPackagesTokenData.packagesToTokensMap.size() - 1; i >= 0; i--) { + if (!packagesSet.contains(mDatabase.mPackagesTokenData.packagesToTokensMap.keyAt(i))) { + removedPackages.add(mDatabase.mPackagesTokenData.packagesToTokensMap.keyAt(i)); + } + } + + // remove packages in the mappings that are no longer installed + for (int i = removedPackages.size() - 1; i >= 0; i--) { + mDatabase.mPackagesTokenData.removePackage(removedPackages.get(i), timeNow); + } + } + private void onTimeChanged(long oldTime, long newTime) { persistActiveStats(); mDatabase.onTimeChanged(newTime - oldTime); @@ -400,6 +454,7 @@ class UserUsageStatsService { if (results == null) { results = new ArrayList<>(); } + mDatabase.filterStats(currentStats); combiner.combine(currentStats, true, results); } @@ -524,6 +579,8 @@ class UserUsageStatsService { 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 +757,10 @@ class UserUsageStatsService { 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 7d9d0d52b0026..7e8a13470c358 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.filters.LargeTest; 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; @@ -79,6 +80,7 @@ public class UsageStatsDatabasePerfTest { sContext = InstrumentationRegistry.getTargetContext(); mTestDir = new File(sContext.getFilesDir(), "UsageStatsDatabasePerfTest"); sUsageStatsDatabase = new UsageStatsDatabase(mTestDir); + sUsageStatsDatabase.readMappingsLocked(); sUsageStatsDatabase.init(1); } @@ -140,6 +142,37 @@ public class UsageStatsDatabasePerfTest { } } + 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); @@ -150,6 +183,16 @@ public class UsageStatsDatabasePerfTest { runPutUsageStatsTest(FEW_PKGS, LIGHT_USE); } + @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); @@ -160,6 +203,16 @@ public class UsageStatsDatabasePerfTest { runPutUsageStatsTest(FEW_PKGS, HEAVY_USE); } + @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); @@ -170,6 +223,16 @@ public class UsageStatsDatabasePerfTest { runPutUsageStatsTest(MANY_PKGS, LIGHT_USE); } + @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 +242,14 @@ public class UsageStatsDatabasePerfTest { 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); + } }