Delay updating of usage stats package mappings.

Do not update package mappings for the system user when the user is
first unlocked. Instead, schedule a job to be executed after 24 to 48
hours from when the system user is unlocked. This makes the service
initialization phase for the system user a little quicker since their
data is not likely to be stale. Additionally, this also ensures that
restored data is not pruned by mistake if there is a device restart
before restore is completed. The updating of the mappings occurs
normally for other users, on user service initialization.

Bug: 155209652
Test: manually ensure job is skipped for system user
Test: atest android.app.usage.cts.UsageStatsTest
Change-Id: I2c03a1a05246d6b454569c4569813e90bede3693
This commit is contained in:
Varun Shah
2020-05-04 19:36:54 -07:00
parent 0728a4fe50
commit a7f9c762cb
4 changed files with 109 additions and 16 deletions

View File

@@ -315,4 +315,15 @@ public abstract class UsageStatsManagerInternal {
* @return {@code true} if the pruning was successful, {@code false} otherwise
*/
public abstract boolean pruneUninstalledPackagesData(@UserIdInt int userId);
/**
* Called by {@link com.android.server.usage.UsageStatsIdleService} between 24 to 48 hours of
* when the user is first unlocked to update the usage stats package mappings data that might
* be stale or have existed from a restore and belongs to packages that are not installed for
* this user anymore.
* Note: this is only executed for the system user.
*
* @return {@code true} if the updating was successful, {@code false} otherwise
*/
public abstract boolean updatePackageMappingsData();
}

View File

@@ -27,6 +27,8 @@ import android.os.PersistableBundle;
import com.android.server.LocalServices;
import java.util.concurrent.TimeUnit;
/**
* JobService used to do any work for UsageStats while the device is idle.
*/
@@ -36,6 +38,11 @@ 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;
/**
* Job ID for the update mappings job - must be unique within the system server uid.
* Incrementing PRUNE_JOB_ID by 21475 (MAX_USER_ID) to ensure there is no overlap in job ids.
*/
private static final int UPDATE_MAPPINGS_JOB_ID = 546378950;
private static final String USER_ID_KEY = "user_id";
@@ -51,35 +58,65 @@ public class UsageStatsIdleService extends JobService {
.setPersisted(true)
.build();
scheduleJobInternal(context, pruneJob, userJobId);
}
static void scheduleUpdateMappingsJob(Context context) {
final ComponentName component = new ComponentName(context.getPackageName(),
UsageStatsIdleService.class.getName());
final JobInfo updateMappingsJob = new JobInfo.Builder(UPDATE_MAPPINGS_JOB_ID, component)
.setPersisted(true)
.setMinimumLatency(TimeUnit.DAYS.toMillis(1))
.setOverrideDeadline(TimeUnit.DAYS.toMillis(2))
.build();
scheduleJobInternal(context, updateMappingsJob, UPDATE_MAPPINGS_JOB_ID);
}
private static void scheduleJobInternal(Context context, JobInfo pruneJob, int jobId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
final JobInfo pendingPruneJob = jobScheduler.getPendingJob(userJobId);
final JobInfo pendingPruneJob = jobScheduler.getPendingJob(jobId);
// only schedule a new prune job if one doesn't exist already for this user
if (!pruneJob.equals(pendingPruneJob)) {
jobScheduler.cancel(userJobId); // cancel any previously scheduled prune job
jobScheduler.cancel(jobId); // cancel any previously scheduled prune job
jobScheduler.schedule(pruneJob);
}
}
static void cancelJob(Context context, int userId) {
final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
cancelJobInternal(context, PRUNE_JOB_ID + userId);
}
static void cancelUpdateMappingsJob(Context context) {
cancelJobInternal(context, UPDATE_MAPPINGS_JOB_ID);
}
private static void cancelJobInternal(Context context, int jobId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
jobScheduler.cancel(userJobId);
if (jobScheduler != null) {
jobScheduler.cancel(jobId);
}
}
@Override
public boolean onStartJob(JobParameters params) {
final PersistableBundle bundle = params.getExtras();
final int userId = bundle.getInt(USER_ID_KEY, -1);
if (userId == -1) {
if (userId == -1 && params.getJobId() != UPDATE_MAPPINGS_JOB_ID) {
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
if (params.getJobId() == UPDATE_MAPPINGS_JOB_ID) {
final boolean jobFinished = usageStatsManagerInternal.updatePackageMappingsData();
jobFinished(params, !jobFinished); // reschedule if data was not updated
} else {
final boolean jobFinished =
usageStatsManagerInternal.pruneUninstalledPackagesData(userId);
jobFinished(params, !jobFinished); // reschedule if data was not pruned
}
});
return true;
}

View File

@@ -332,6 +332,11 @@ public class UsageStatsService extends SystemService implements
private void onUserUnlocked(int userId) {
// fetch the installed packages outside the lock so it doesn't block package manager.
final HashMap<String, Long> installedPackages = getInstalledPackages(userId);
// delay updating of package mappings for user 0 since their data is not likely to be stale.
// this also makes it less likely for restored data to be erased on unexpected reboots.
if (userId == UserHandle.USER_SYSTEM) {
UsageStatsIdleService.scheduleUpdateMappingsJob(getContext());
}
synchronized (mLock) {
// Create a user unlocked event to report
final Event unlockEvent = new Event(USER_UNLOCKED, SystemClock.elapsedRealtime());
@@ -543,8 +548,8 @@ public class UsageStatsService extends SystemService implements
* Initializes the given user's usage stats service - this should ideally only be called once,
* when the user is initially unlocked.
*/
private void initializeUserUsageStatsServiceLocked(int userId,
long currentTimeMillis, HashMap<String, Long> installedPackages) {
private void initializeUserUsageStatsServiceLocked(int userId, long currentTimeMillis,
HashMap<String, Long> installedPackages) {
final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId),
"usagestats");
final UserUsageStatsService service = new UserUsageStatsService(getContext(), userId,
@@ -931,6 +936,7 @@ public class UsageStatsService extends SystemService implements
}
// Cancel any scheduled jobs for this user since the user is being removed.
UsageStatsIdleService.cancelJob(getContext(), userId);
UsageStatsIdleService.cancelUpdateMappingsJob(getContext());
}
/**
@@ -977,6 +983,26 @@ public class UsageStatsService extends SystemService implements
}
}
/**
* Called by the Binder stub.
*/
private boolean updatePackageMappingsData() {
// fetch the installed packages outside the lock so it doesn't block package manager.
final HashMap<String, Long> installedPkgs = getInstalledPackages(UserHandle.USER_SYSTEM);
synchronized (mLock) {
if (!mUserUnlockedStates.get(UserHandle.USER_SYSTEM)) {
return false; // user is no longer unlocked
}
final UserUsageStatsService userService = mUserState.get(UserHandle.USER_SYSTEM);
if (userService == null) {
return false; // user was stopped or removed
}
return userService.updatePackageMappingsLocked(installedPkgs);
}
}
/**
* Called by the Binder stub.
*/
@@ -2137,6 +2163,9 @@ public class UsageStatsService extends SystemService implements
}
// Check to ensure that only user 0's data is b/r for now
// Note: if backup and restore is enabled for users other than the system user, the
// #onUserUnlocked logic, specifically when the update mappings job is scheduled via
// UsageStatsIdleService.scheduleUpdateMappingsJob, will have to be updated.
if (user == UserHandle.USER_SYSTEM) {
final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(user);
if (userStats == null) {
@@ -2229,6 +2258,11 @@ public class UsageStatsService extends SystemService implements
public boolean pruneUninstalledPackagesData(int userId) {
return UsageStatsService.this.pruneUninstalledPackagesData(userId);
}
@Override
public boolean updatePackageMappingsData() {
return UsageStatsService.this.updatePackageMappingsData();
}
}
private class MyPackageMonitor extends PackageMonitor {

View File

@@ -40,6 +40,7 @@ import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.res.Configuration;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -181,19 +182,27 @@ class UserUsageStatsService {
private void readPackageMappingsLocked(HashMap<String, Long> installedPackages) {
mDatabase.readMappingsLocked();
updatePackageMappingsLocked(installedPackages);
// Package mappings for the system user are updated after 24 hours via a job scheduled by
// UsageStatsIdleService to ensure restored data is not lost on first boot. Additionally,
// this makes user service initialization a little quicker on subsequent boots.
if (mUserId != UserHandle.USER_SYSTEM) {
updatePackageMappingsLocked(installedPackages);
}
}
/**
* Queries Job Scheduler for any pending data prune jobs and if any exist, it updates the
* package mappings in memory by removing those tokens.
* Compares the package mappings on disk with the ones currently installed and removes the
* mappings for those packages that have been uninstalled.
* This will only happen once per device boot, when the user is unlocked for the first time.
* If the user is the system user (user 0), this is delayed to ensure data for packages
* that were restored isn't removed before the restore is complete.
*
* @param installedPackages map of installed packages (package_name:package_install_time)
* @return {@code true} on a successful mappings update, {@code false} otherwise.
*/
private void updatePackageMappingsLocked(HashMap<String, Long> installedPackages) {
boolean updatePackageMappingsLocked(HashMap<String, Long> installedPackages) {
if (ArrayUtils.isEmpty(installedPackages)) {
return;
return true;
}
final long timeNow = System.currentTimeMillis();
@@ -206,7 +215,7 @@ class UserUsageStatsService {
}
}
if (removedPackages.isEmpty()) {
return;
return true;
}
// remove packages in the mappings that are no longer installed and persist to disk
@@ -217,7 +226,9 @@ class UserUsageStatsService {
mDatabase.writeMappingsLocked();
} catch (Exception e) {
Slog.w(TAG, "Unable to write updated package mappings file on service initialization.");
return false;
}
return true;
}
boolean pruneUninstalledPackagesData() {