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:
committed by
Android (Google) Code Review
commit
5df072b6ae
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
135
core/proto/android/server/usagestatsservice_v2.proto
Normal file
135
core/proto/android/server/usagestatsservice_v2.proto
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user