Merge changes from topic "usagestats-obfuscation"

* changes:
  Do not retain UsageStats for uninstalled packages.
  Catch exceptions in UsageStatsService on bad data.
  Obfuscate usage stats data stored on disk.
This commit is contained in:
TreeHugger Robot
2019-10-21 17:53:38 +00:00
committed by Android (Google) Code Review
15 changed files with 2201 additions and 144 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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<String, ArrayMap<String, Integer>> mChooserCounts = new ArrayMap<>();
/**
* {@hide}
*/
public SparseArray<SparseIntArray> mChooserCountsObfuscated = new SparseArray<>();
/**
* {@hide}
*/

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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<IntervalStats> 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<IntervalStats> stats = newDB.queryUsageStats(interval, 0, mEndTime,
mIntervalStatsVerifier);
assertEquals(1, stats.size());
// The written and read IntervalStats should match
compareIntervalStats(mIntervalStats, stats.get(0), 5);
}
@Test
public void testObfuscation() throws IOException {
compareObfuscatedData(UsageStatsManager.INTERVAL_DAILY);
compareObfuscatedData(UsageStatsManager.INTERVAL_WEEKLY);
compareObfuscatedData(UsageStatsManager.INTERVAL_MONTHLY);
compareObfuscatedData(UsageStatsManager.INTERVAL_YEARLY);
}
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<IntervalStats> 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);
}
}

View File

@@ -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<String, UsageStats> packageStats = new ArrayMap<>();
/** @hide */
public final SparseArray<UsageStats> packageStatsObfuscated = new SparseArray<>();
public final ArrayMap<Configuration, ConfigurationStats> configurations = new ArrayMap<>();
public Configuration activeConfiguration;
public final EventList events = new EventList();
@@ -436,4 +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<String, Integer> 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<String, Integer> categoriesMap =
usageStats.mChooserCounts.valueAt(actionIndex);
if (categoriesMap == null) {
continue;
}
final SparseIntArray categoryCounts = new SparseIntArray();
final int categoriesSize = categoriesMap.size();
for (int categoryIndex = 0; categoryIndex < categoriesSize; categoryIndex++) {
String category = categoriesMap.keyAt(categoryIndex);
int categoryToken = packagesTokenData.getTokenOrAdd(packageToken, packageName,
category);
categoryCounts.put(categoryToken, categoriesMap.valueAt(categoryIndex));
}
int actionToken = packagesTokenData.getTokenOrAdd(packageToken, packageName,
action);
usageStats.mChooserCountsObfuscated.put(actionToken, categoryCounts);
}
packageStatsObfuscated.put(packageToken, usageStats);
}
}
/**
* Obfuscates certain strings within an event such as the package name, the class name,
* task root package and class names, and shortcut and notification channel ids.
*/
private void obfuscateEventsData(PackagesTokenData packagesTokenData) {
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);
}
}

View File

@@ -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<ArrayList<String>> tokensToPackagesMap = new SparseArray<>();
/**
* Stores a hierarchy of strings to token mappings for each package. This is simply an inverse
* map of the {@code tokenToPackagesMap} in this class, mainly for an O(1) access to the tokens.
*/
public final ArrayMap<String, ArrayMap<String, Integer>> packagesToTokensMap = new ArrayMap<>();
/**
* Stores a map of packages that were removed and when they were removed.
*/
public final ArrayMap<String, Long> 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<String, Integer> packageTokensMap = packagesToTokensMap.get(packageName);
if (packageTokensMap == null) {
packageTokensMap = new ArrayMap<>();
packagesToTokensMap.put(packageName, packageTokensMap);
}
int token = packageTokensMap.getOrDefault(packageName, UNASSIGNED_TOKEN);
if (token == UNASSIGNED_TOKEN) {
token = counter++;
// package name should always be at index 0 in the sub-mapping
ArrayList<String> tokenPackages = new ArrayList<>();
tokenPackages.add(packageName);
packageTokensMap.put(packageName, token);
tokensToPackagesMap.put(token, tokenPackages);
}
return token;
}
/**
* Fetches the token mapped to the given key within the package's context. If there is no
* mapping, a new token is created and the relevant mappings are updated.
*
* @param packageToken the package token for which the given key belongs to
* @param packageName the package name for which the given key belongs to
* @param key the key whose token is being fetched
* @return the mapped token
*/
public int getTokenOrAdd(int packageToken, String packageName, String key) {
if (packageName.equals(key)) {
return PACKAGE_NAME_INDEX;
}
int token = packagesToTokensMap.get(packageName).getOrDefault(key, UNASSIGNED_TOKEN);
if (token == UNASSIGNED_TOKEN) {
token = tokensToPackagesMap.get(packageToken).size();
packagesToTokensMap.get(packageName).put(key, token);
tokensToPackagesMap.get(packageToken).add(key);
}
return token;
}
/**
* Fetches the 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<String> 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);
}
}

View File

@@ -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<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
// filter out package usage stats
final int removedPackagesSize = removedPackagesMap.size();
for (int i = 0; i < removedPackagesSize; i++) {
final String removedPackage = removedPackagesMap.keyAt(i);
final UsageStats usageStats = stats.packageStats.get(removedPackage);
if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
stats.packageStats.remove(removedPackage);
}
}
// filter out events
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<ArrayList<String>> tokensToPackagesMap =
mPackagesTokenData.tokensToPackagesMap;
final int tokensToPackagesMapSize = tokensToPackagesMap.size();
for (int i = 0; i < tokensToPackagesMapSize; i++) {
final int packageToken = tokensToPackagesMap.keyAt(i);
final ArrayList<String> tokensMap = tokensToPackagesMap.valueAt(i);
final ArrayMap<String, Integer> packageStringsMap = new ArrayMap<>();
final int tokensMapSize = tokensMap.size();
// package name will always be at index 0 but its token should not be 0
packageStringsMap.put(tokensMap.get(0), packageToken);
for (int j = 1; j < tokensMapSize; j++) {
packageStringsMap.put(tokensMap.get(j), j);
}
mPackagesTokenData.packagesToTokensMap.put(tokensMap.get(0), packageStringsMap);
}
}
void writeMappingsLocked() throws IOException {
final AtomicFile file = new AtomicFile(mPackageMappingsFile);
FileOutputStream fos = file.startWrite();
try {
UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData);
file.finishWrite(fos);
fos = null;
} catch (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<AtomicFile> 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();

View File

@@ -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<String, Integer> counts) throws IOException {
ArrayMap<String, Integer> 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<UsageEvents.Event> events)
throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
final List<String> stringPool = new ArrayList<>();
final IntervalStats tmpStatsObj = new IntervalStats();
while (true) {
switch (proto.nextField()) {
case (int) IntervalStatsProto.PENDING_EVENTS:
loadEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, stringPool);
break;
case ProtoInputStream.NO_MORE_FIELDS:
final int eventCount = tmpStatsObj.events.size();
for (int i = 0; i < eventCount; i++) {
events.add(tmpStatsObj.events.get(i));
}
return;
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<UsageEvents.Event> events)
throws IOException {
final ProtoOutputStream proto = new ProtoOutputStream(out);
final IntervalStats tmpStatsObj = new IntervalStats();
final int eventCount = events.size();
for (int i = 0; i < eventCount; i++) {
writeEvent(proto, IntervalStatsProto.PENDING_EVENTS, tmpStatsObj, events.get(i));
}
proto.flush();
}
}

View File

@@ -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<ArrayList<String>> tokensToPackagesMap) throws IOException {
int key = PackagesTokenData.UNASSIGNED_TOKEN;
final ArrayList<String> strings = new ArrayList<>();
while (true) {
switch (proto.nextField()) {
case (int) ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN:
key = proto.readInt(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN) - 1;
break;
case (int) ObfuscatedPackagesProto.PackagesMap.STRINGS:
strings.add(proto.readString(ObfuscatedPackagesProto.PackagesMap.STRINGS));
break;
case ProtoInputStream.NO_MORE_FIELDS:
if (key != PackagesTokenData.UNASSIGNED_TOKEN) {
tokensToPackagesMap.put(key, strings);
}
return;
}
}
}
/**
* Populates the package mappings from the input stream given.
*
* @param in the input stream from which to read the mappings.
* @param packagesTokenData the packages data object to which the data will be read to.
*/
static void readObfuscatedData(InputStream in, PackagesTokenData packagesTokenData)
throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
while (true) {
switch (proto.nextField()) {
case (int) ObfuscatedPackagesProto.COUNTER:
packagesTokenData.counter = proto.readInt(ObfuscatedPackagesProto.COUNTER);
break;
case (int) ObfuscatedPackagesProto.PACKAGES_MAP:
final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP);
loadPackagesMap(proto, packagesTokenData.tokensToPackagesMap);
proto.end(token);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
}
}
}
/**
* Writes the packages mapping data to a ProtoBuf file.
*
* @param out the output stream to which to write the mappings.
* @param packagesTokenData the packages data object holding the data to write.
*/
static void writeObfuscatedData(OutputStream out, PackagesTokenData packagesTokenData)
throws IOException, IllegalArgumentException {
final ProtoOutputStream proto = new ProtoOutputStream(out);
proto.write(ObfuscatedPackagesProto.COUNTER, packagesTokenData.counter);
final int mapSize = packagesTokenData.tokensToPackagesMap.size();
for (int i = 0; i < mapSize; i++) {
final long token = proto.start(ObfuscatedPackagesProto.PACKAGES_MAP);
int packageToken = packagesTokenData.tokensToPackagesMap.keyAt(i);
proto.write(ObfuscatedPackagesProto.PackagesMap.PACKAGE_TOKEN, packageToken + 1);
final ArrayList<String> strings = packagesTokenData.tokensToPackagesMap.valueAt(i);
final int listSize = strings.size();
for (int j = 0; j < listSize; j++) {
proto.write(ObfuscatedPackagesProto.PackagesMap.STRINGS, strings.get(j));
}
proto.end(token);
}
proto.flush();
}
/***** Read/Write pending events logic. *****/
private static UsageEvents.Event parsePendingEvent(ProtoInputStream proto) throws IOException {
final UsageEvents.Event event = new UsageEvents.Event();
while (true) {
switch (proto.nextField()) {
case (int) PendingEventProto.PACKAGE_NAME:
event.mPackage = proto.readString(PendingEventProto.PACKAGE_NAME);
break;
case (int) PendingEventProto.CLASS_NAME:
event.mClass = proto.readString(PendingEventProto.CLASS_NAME);
break;
case (int) PendingEventProto.TIME_MS:
event.mTimeStamp = proto.readLong(PendingEventProto.TIME_MS);
break;
case (int) PendingEventProto.FLAGS:
event.mFlags = proto.readInt(PendingEventProto.FLAGS);
break;
case (int) PendingEventProto.TYPE:
event.mEventType = proto.readInt(PendingEventProto.TYPE);
break;
case (int) PendingEventProto.CONFIG:
event.mConfiguration = new Configuration();
event.mConfiguration.readFromProto(proto, PendingEventProto.CONFIG);
break;
case (int) PendingEventProto.SHORTCUT_ID:
event.mShortcutId = proto.readString(PendingEventProto.SHORTCUT_ID);
break;
case (int) PendingEventProto.STANDBY_BUCKET:
event.mBucketAndReason = proto.readInt(PendingEventProto.STANDBY_BUCKET);
break;
case (int) PendingEventProto.NOTIFICATION_CHANNEL_ID:
event.mNotificationChannelId = proto.readString(
PendingEventProto.NOTIFICATION_CHANNEL_ID);
break;
case (int) PendingEventProto.INSTANCE_ID:
event.mInstanceId = proto.readInt(PendingEventProto.INSTANCE_ID);
break;
case (int) PendingEventProto.TASK_ROOT_PACKAGE:
event.mTaskRootPackage = proto.readString(PendingEventProto.TASK_ROOT_PACKAGE);
break;
case (int) PendingEventProto.TASK_ROOT_CLASS:
event.mTaskRootClass = proto.readString(PendingEventProto.TASK_ROOT_CLASS);
break;
case ProtoInputStream.NO_MORE_FIELDS:
// Handle default values for certain events types
switch (event.mEventType) {
case UsageEvents.Event.CONFIGURATION_CHANGE:
if (event.mConfiguration == null) {
event.mConfiguration = new Configuration();
}
break;
case UsageEvents.Event.SHORTCUT_INVOCATION:
if (event.mShortcutId == null) {
event.mShortcutId = "";
}
break;
case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
if (event.mNotificationChannelId == null) {
event.mNotificationChannelId = "";
}
break;
}
return event.mPackage == null ? null : event;
}
}
}
/**
* Populates the list of pending events from the input stream given.
*
* @param in the input stream from which to read the pending events.
* @param events the list of pending events to populate.
*/
static void readPendingEvents(InputStream in, LinkedList<UsageEvents.Event> events)
throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
while (true) {
switch (proto.nextField()) {
case (int) IntervalStatsObfuscatedProto.PENDING_EVENTS:
try {
final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
UsageEvents.Event event = parsePendingEvent(proto);
proto.end(token);
if (event != null) {
events.add(event);
}
} catch (IOException e) {
Slog.e(TAG, "Unable to parse some pending events from proto.", e);
}
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
}
}
}
private static void writePendingEvent(ProtoOutputStream proto, UsageEvents.Event event)
throws IllegalArgumentException {
proto.write(PendingEventProto.PACKAGE_NAME, event.mPackage);
if (event.mClass != null) {
proto.write(PendingEventProto.CLASS_NAME, event.mClass);
}
proto.write(PendingEventProto.TIME_MS, event.mTimeStamp);
proto.write(PendingEventProto.FLAGS, event.mFlags);
proto.write(PendingEventProto.TYPE, event.mEventType);
proto.write(PendingEventProto.INSTANCE_ID, event.mInstanceId);
if (event.mTaskRootPackage != null) {
proto.write(PendingEventProto.TASK_ROOT_PACKAGE, event.mTaskRootPackage);
}
if (event.mTaskRootClass != null) {
proto.write(PendingEventProto.TASK_ROOT_CLASS, event.mTaskRootClass);
}
switch (event.mEventType) {
case UsageEvents.Event.CONFIGURATION_CHANGE:
if (event.mConfiguration != null) {
event.mConfiguration.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<UsageEvents.Event> events)
throws IOException, IllegalArgumentException {
final ProtoOutputStream proto = new ProtoOutputStream(out);
final int eventCount = events.size();
for (int i = 0; i < eventCount; i++) {
try {
final long token = proto.start(IntervalStatsObfuscatedProto.PENDING_EVENTS);
writePendingEvent(proto, events.get(i));
proto.end(token);
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Unable to write some pending events to proto.", e);
}
}
proto.flush();
}
}

View File

@@ -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<LinkedList<Event>> mReportedEvents = new SparseArray<>();
final SparseArray<ArraySet<String>> 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);
}
}
}

View File

@@ -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<ApplicationInfo> installedPackages =
packageManagerInternal.getInstalledApplications(0, mUserId, Process.SYSTEM_UID);
// convert the package list to a set for easy look-ups
final HashSet<String> packagesSet = new HashSet<>(installedPackages.size());
for (int i = installedPackages.size() - 1; i >= 0; i--) {
packagesSet.add(installedPackages.get(i).packageName);
}
final List<String> 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

View File

@@ -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);
}
}