Revert "Add a job to prune UsageStats data on package removals."

This reverts commit ece1fa0ba1.

Reason for revert: b/144982898

Change-Id: I95834b3e6b28d569a28088357ab6bfb4d3a0c7ff
This commit is contained in:
Marissa Wall
2019-11-22 20:29:34 +00:00
parent 7172000c75
commit 1a1c61b23e
9 changed files with 28 additions and 381 deletions

View File

@@ -5045,10 +5045,6 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<service android:name="com.android.server.usage.UsageStatsIdleService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<service android:name="com.android.server.net.watchlist.ReportWatchlistJobService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>

View File

@@ -281,13 +281,4 @@ public abstract class UsageStatsManagerInternal {
return mUsageRemaining;
}
}
/**
* Called by {@link com.android.server.usage.UsageStatsIdleService} when the device is idle to
* prune usage stats data for uninstalled packages.
*
* @param userId the user associated with the job
* @return {@code true} if the pruning was successful, {@code false} otherwise
*/
public abstract boolean pruneUninstalledPackagesData(@UserIdInt int userId);
}

View File

@@ -45,7 +45,6 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -94,8 +93,6 @@ public class UsageStatsDatabaseTest {
for (File f : usageFiles) {
f.delete();
}
} else {
intervalDir.delete();
}
}
}
@@ -590,7 +587,6 @@ public class UsageStatsDatabaseTest {
db.readMappingsLocked();
db.init(1);
db.putUsageStats(interval, mIntervalStats);
db.writeMappingsLocked();
final String removedPackage = "fake.package.name0";
// invoke handler call directly from test to remove package
@@ -598,19 +594,19 @@ public class UsageStatsDatabaseTest {
List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
mIntervalStatsVerifier);
assertEquals(1, stats.size(),
"Only one interval stats object should exist for the given time range.");
final IntervalStats stat = stats.get(0);
if (stat.packageStats.containsKey(removedPackage)) {
fail("Found removed package " + removedPackage + " in package stats.");
return;
}
for (int i = 0; i < stat.events.size(); i++) {
final Event event = stat.events.get(i);
if (removedPackage.equals(event.mPackage)) {
fail("Found an event from removed package " + removedPackage);
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;
}
}
}
}
@@ -621,90 +617,4 @@ public class UsageStatsDatabaseTest {
verifyPackageNotRetained(UsageStatsManager.INTERVAL_MONTHLY);
verifyPackageNotRetained(UsageStatsManager.INTERVAL_YEARLY);
}
private void verifyPackageDataIsRemoved(UsageStatsDatabase db, int interval,
String removedPackage) {
List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
mIntervalStatsVerifier);
assertEquals(1, stats.size(),
"Only one interval stats object should exist for the given time range.");
final IntervalStats stat = stats.get(0);
if (stat.packageStats.containsKey(removedPackage)) {
fail("Found removed package " + removedPackage + " in package stats.");
return;
}
for (int i = 0; i < stat.events.size(); i++) {
final Event event = stat.events.get(i);
if (removedPackage.equals(event.mPackage)) {
fail("Found an event from removed package " + removedPackage);
return;
}
}
}
private void verifyPackageDataIsNotRemoved(UsageStatsDatabase db, int interval,
Set<String> installedPackages) {
List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
mIntervalStatsVerifier);
assertEquals(1, stats.size(),
"Only one interval stats object should exist for the given time range.");
final IntervalStats stat = stats.get(0);
if (!stat.packageStats.containsAll(installedPackages)) {
fail("Could not find some installed packages in package stats.");
return;
}
// attempt to find an event from each installed package
for (String installedPackage : installedPackages) {
for (int i = 0; i < stat.events.size(); i++) {
if (installedPackage.equals(stat.events.get(i).mPackage)) {
break;
}
if (i == stat.events.size() - 1) {
fail("Could not find any event for: " + installedPackage);
return;
}
}
}
}
@Test
public void testPackageDataIsRemoved() throws IOException {
UsageStatsDatabase db = new UsageStatsDatabase(mTestDir);
db.readMappingsLocked();
db.init(1);
// write stats to disk for each interval
db.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats);
db.putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, mIntervalStats);
db.putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, mIntervalStats);
db.putUsageStats(UsageStatsManager.INTERVAL_YEARLY, mIntervalStats);
db.writeMappingsLocked();
final Set<String> installedPackages = mIntervalStats.packageStats.keySet();
final String removedPackage = installedPackages.iterator().next();
installedPackages.remove(removedPackage);
// mimic a package uninstall
db.onPackageRemoved(removedPackage, System.currentTimeMillis());
// mimic the idle prune job being triggered
db.pruneUninstalledPackagesData();
// read data from disk into a new db instance
UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir);
newDB.readMappingsLocked();
newDB.init(mEndTime);
// query data for each interval and ensure data for package doesn't exist
verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_DAILY, removedPackage);
verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_WEEKLY, removedPackage);
verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_MONTHLY, removedPackage);
verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_YEARLY, removedPackage);
// query data for each interval and ensure some data for installed packages exists
verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_DAILY, installedPackages);
verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_WEEKLY, installedPackages);
verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_MONTHLY, installedPackages);
verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_YEARLY, installedPackages);
}
}

View File

@@ -448,11 +448,8 @@ public class IntervalStats {
/**
* 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.
*
* @return {@code true} if any stats were omitted while deobfuscating, {@code false} otherwise.
*/
private boolean deobfuscateUsageStats(PackagesTokenData packagesTokenData) {
boolean dataOmitted = false;
private void deobfuscateUsageStats(PackagesTokenData packagesTokenData) {
final int usageStatsSize = packageStatsObfuscated.size();
for (int statsIndex = 0; statsIndex < usageStatsSize; statsIndex++) {
final int packageToken = packageStatsObfuscated.keyAt(statsIndex);
@@ -460,7 +457,6 @@ public class IntervalStats {
usageStats.mPackageName = packagesTokenData.getPackageString(packageToken);
if (usageStats.mPackageName == null) {
Slog.e(TAG, "Unable to parse usage stats package " + packageToken);
dataOmitted = true;
continue;
}
@@ -493,18 +489,14 @@ public class IntervalStats {
}
packageStats.put(usageStats.mPackageName, usageStats);
}
return dataOmitted;
}
/**
* 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.
*
* @return {@code true} if any events were omitted while deobfuscating, {@code false} otherwise.
*/
private boolean deobfuscateEvents(PackagesTokenData packagesTokenData) {
boolean dataOmitted = false;
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;
@@ -512,7 +504,6 @@ public class IntervalStats {
if (event.mPackage == null) {
Slog.e(TAG, "Unable to parse event package " + packageToken);
this.events.remove(i);
dataOmitted = true;
continue;
}
@@ -552,7 +543,6 @@ public class IntervalStats {
Slog.e(TAG, "Unable to parse shortcut " + event.mShortcutIdToken
+ " for package " + packageToken);
this.events.remove(i);
dataOmitted = true;
continue;
}
break;
@@ -564,25 +554,21 @@ public class IntervalStats {
+ event.mNotificationChannelIdToken + " for package "
+ packageToken);
this.events.remove(i);
dataOmitted = true;
continue;
}
break;
}
}
return dataOmitted;
}
/**
* Parses the obfuscated tokenized data held in this interval stats object.
*
* @return {@code true} if any data was omitted while deobfuscating, {@code false} otherwise.
* @hide
*/
public boolean deobfuscateData(PackagesTokenData packagesTokenData) {
final boolean statsOmitted = deobfuscateUsageStats(packagesTokenData);
final boolean eventsOmitted = deobfuscateEvents(packagesTokenData);
return statsOmitted || eventsOmitted;
public void deobfuscateData(PackagesTokenData packagesTokenData) {
deobfuscateUsageStats(packagesTokenData);
deobfuscateEvents(packagesTokenData);
}
/**

View File

@@ -162,18 +162,15 @@ public final class PackagesTokenData {
*
* @param packageName the package to be removed
* @param timeRemoved the time stamp of when the package was removed
* @return the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN}
* if not mapped
*/
public int removePackage(String packageName, long timeRemoved) {
public void removePackage(String packageName, long timeRemoved) {
removedPackagesMap.put(packageName, timeRemoved);
if (!packagesToTokensMap.containsKey(packageName)) {
return UNASSIGNED_TOKEN;
return;
}
final int packageToken = packagesToTokensMap.get(packageName).get(packageName);
packagesToTokensMap.remove(packageName);
tokensToPackagesMap.delete(packageToken);
return packageToken;
}
}

View File

@@ -545,52 +545,9 @@ public class UsageStatsDatabase {
}
}
/**
* Returns the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN}
* if not mapped.
*/
int onPackageRemoved(String packageName, long timeRemoved) {
void onPackageRemoved(String packageName, long timeRemoved) {
synchronized (mLock) {
return mPackagesTokenData.removePackage(packageName, timeRemoved);
}
}
/**
* Reads all the usage stats data on disk and rewrites it with any data related to uninstalled
* packages omitted.
*/
boolean pruneUninstalledPackagesData() {
synchronized (mLock) {
for (int i = 0; i < mIntervalDirs.length; i++) {
final File[] files = mIntervalDirs[i].listFiles();
if (files == null) {
continue;
}
for (int j = 0; j < files.length; j++) {
try {
final IntervalStats stats = new IntervalStats();
final AtomicFile atomicFile = new AtomicFile(files[j]);
if (!readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData)) {
continue; // no data was omitted when read so no need to rewrite
}
// Any data related to packages that have been removed would have failed
// the deobfuscation step on read so the IntervalStats object here only
// contains data for packages that are currently installed - all we need
// to do here is write the data back to disk.
writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
} catch (Exception e) {
Slog.e(TAG, "Failed to prune data from: " + files[j].toString());
return false;
}
}
}
try {
writeMappingsLocked();
} catch (IOException e) {
Slog.e(TAG, "Failed to write package mappings after pruning data.");
return false;
}
return true;
mPackagesTokenData.removePackage(packageName, timeRemoved);
}
}
@@ -688,6 +645,7 @@ public class UsageStatsDatabase {
}
// 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);
@@ -984,17 +942,13 @@ public class UsageStatsDatabase {
readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData);
}
/**
* Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
*/
private static boolean readLocked(AtomicFile file, IntervalStats statsOut, int version,
private static void readLocked(AtomicFile file, IntervalStats statsOut, int version,
PackagesTokenData packagesTokenData) throws IOException {
boolean dataOmitted = false;
try {
FileInputStream in = file.openRead();
try {
statsOut.beginTime = parseBeginTime(file);
dataOmitted = readLocked(in, statsOut, version, packagesTokenData);
readLocked(in, statsOut, version, packagesTokenData);
statsOut.lastTimeSaved = file.getLastModifiedTime();
} finally {
try {
@@ -1007,15 +961,10 @@ public class UsageStatsDatabase {
Slog.e(TAG, "UsageStatsDatabase", e);
throw e;
}
return dataOmitted;
}
/**
* Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
*/
private static boolean readLocked(InputStream in, IntervalStats statsOut, int version,
private static void readLocked(InputStream in, IntervalStats statsOut, int version,
PackagesTokenData packagesTokenData) throws IOException {
boolean dataOmitted = false;
switch (version) {
case 1:
case 2:
@@ -1040,14 +989,14 @@ public class UsageStatsDatabase {
} catch (IOException e) {
Slog.e(TAG, "Unable to read interval stats from proto.", e);
}
dataOmitted = statsOut.deobfuscateData(packagesTokenData);
statsOut.deobfuscateData(packagesTokenData);
break;
default:
throw new RuntimeException(
"Unhandled UsageStatsDatabase version: " + Integer.toString(version)
+ " on read.");
}
return dataOmitted;
}
/**

View File

@@ -1,121 +0,0 @@
/*
* 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.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.os.AsyncTask;
import android.os.PersistableBundle;
import com.android.server.LocalServices;
/**
* JobService used to do any work for UsageStats while the device is idle.
*/
public class UsageStatsIdleService extends JobService {
/**
* Base job ID for the pruning job - must be unique within the system server uid.
*/
private static final int PRUNE_JOB_ID = 546357475;
private static final String USER_ID_KEY = "user_id";
private static final String REMOVED_TOKENS_KEY = "removed_tokens";
static void scheduleJob(Context context, int userId, int token) {
final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
final ComponentName component = new ComponentName(context.getPackageName(),
UsageStatsIdleService.class.getName());
final PersistableBundle bundle = new PersistableBundle();
bundle.putInt(USER_ID_KEY, userId);
bundle.putIntArray(REMOVED_TOKENS_KEY,
getOrCreateRemovedTokens(fetchRemovedTokens(context, userId), token));
final JobInfo pruneJob = new JobInfo.Builder(userJobId, component)
.setRequiresDeviceIdle(true)
.setExtras(bundle)
.setPersisted(true)
.build();
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
jobScheduler.schedule(pruneJob);
}
static void cancelJob(Context context, int userId) {
final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
jobScheduler.cancel(userJobId);
}
/**
* Fetches an array of removed tokens from previous prune jobs, if any.
*/
static int[] fetchRemovedTokens(Context context, int userId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
final JobInfo pendingJob = jobScheduler.getPendingJob(userJobId);
if (pendingJob != null) {
final PersistableBundle bundle = pendingJob.getExtras();
return bundle.getIntArray(REMOVED_TOKENS_KEY);
}
return null;
}
@Override
public boolean onStartJob(JobParameters params) {
final PersistableBundle bundle = params.getExtras();
final int userId = bundle.getInt(USER_ID_KEY, -1);
if (userId == -1) {
return false;
}
AsyncTask.execute(() -> {
final UsageStatsManagerInternal usageStatsManagerInternal = LocalServices.getService(
UsageStatsManagerInternal.class);
final boolean pruned = usageStatsManagerInternal.pruneUninstalledPackagesData(userId);
jobFinished(params, !pruned); // reschedule if data was not pruned
});
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
// Since the pruning job isn't a heavy job, we don't want to cancel it's execution midway.
return false;
}
/**
* Helper method to create a cloned array of removed tokens from previous jobs (if any) with
* the newly removed token at index 0. If there are no removed tokens from previous jobs, it
* simply returns a new array containing the current token.
*/
private static int[] getOrCreateRemovedTokens(int[] previousRemovedTokens, int token) {
final int[] removedTokens;
if (previousRemovedTokens == null) {
removedTokens = new int[1];
} else {
removedTokens = new int[previousRemovedTokens.length + 1];
System.arraycopy(previousRemovedTokens, 0, removedTokens, 1,
previousRemovedTokens.length);
}
removedTokens[0] = token;
return removedTokens;
}
}

View File

@@ -846,8 +846,6 @@ public class UsageStatsService extends SystemService implements
mUserState.remove(userId);
mAppStandby.onUserRemoved(userId);
mAppTimeLimit.onUserRemoved(userId);
// Cancel any scheduled jobs for this user since the user is being removed.
UsageStatsIdleService.cancelJob(getContext(), userId);
}
}
@@ -857,7 +855,7 @@ public class UsageStatsService extends SystemService implements
private void onPackageRemoved(int userId, String packageName) {
synchronized (mLock) {
final long timeRemoved = System.currentTimeMillis();
if (!mUserUnlockedStates.get(userId)) {
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;
@@ -871,24 +869,6 @@ public class UsageStatsService extends SystemService implements
}
}
/**
* Called by the Binder stub.
*/
private boolean pruneUninstalledPackagesData(int userId) {
synchronized (mLock) {
if (!mUserUnlockedStates.get(userId)) {
return false; // user is no longer unlocked
}
final UserUsageStatsService userService = mUserState.get(userId);
if (userService == null) {
return false; // user was stopped or removed
}
return userService.pruneUninstalledPackagesData();
}
}
/**
* Called by the Binder stub.
*/
@@ -2144,11 +2124,6 @@ public class UsageStatsService extends SystemService implements
public AppUsageLimitData getAppUsageLimit(String packageName, UserHandle user) {
return mAppTimeLimit.getAppUsageLimit(packageName, user);
}
@Override
public boolean pruneUninstalledPackagesData(int userId) {
return UsageStatsService.this.pruneUninstalledPackagesData(userId);
}
}
private class MyPackageMonitor extends PackageMonitor {

View File

@@ -175,46 +175,14 @@ class UserUsageStatsService {
}
void onPackageRemoved(String packageName, long timeRemoved) {
final int token = mDatabase.onPackageRemoved(packageName, timeRemoved);
if (token != PackagesTokenData.UNASSIGNED_TOKEN) {
UsageStatsIdleService.scheduleJob(mContext, mUserId, token);
}
mDatabase.onPackageRemoved(packageName, timeRemoved);
}
private void readPackageMappingsLocked() {
mDatabase.readMappingsLocked();
updatePackageMappingsLocked();
cleanUpPackageMappingsLocked();
}
/**
* Queries Job Scheduler for any pending data prune jobs and if any exist, it updates the
* package mappings in memory by removing those tokens.
* This will only happen once per device boot, when the user is unlocked for the first time.
*/
private void updatePackageMappingsLocked() {
final long timeNow = System.currentTimeMillis();
final int[] removedTokens = UsageStatsIdleService.fetchRemovedTokens(mContext, mUserId);
if (removedTokens == null) {
return;
}
for (int i = removedTokens.length - 1; i >= 0; i--) {
final String packageName =
mDatabase.mPackagesTokenData.getPackageString(removedTokens[i]);
if (packageName == null) {
continue;
}
/*
Note: in most cases, packageName returned will be null since the package mappings file
on disk should have been updated when it was last persisted. This is to handle the rare
case of system crashing after a package was removed but the package mappings file
wasn't persisted to disk.
*/
mDatabase.mPackagesTokenData.removePackage(packageName, timeNow);
}
}
/**
* Queries Package Manager for a list of installed packages and removes those packages from
* mPackagesTokenData which are not installed any more.
@@ -254,10 +222,6 @@ class UserUsageStatsService {
}
}
boolean pruneUninstalledPackagesData() {
return mDatabase.pruneUninstalledPackagesData();
}
private void onTimeChanged(long oldTime, long newTime) {
persistActiveStats();
mDatabase.onTimeChanged(newTime - oldTime);