Allow any user's UsageStats to be backed up and restored

Refactor update mappings jobs and prune jobs to use the
user id as the job id and use job namespaces.

Plumb the userId through from the system backup agent

Put 0 in the userId field of the payload to ensure that it
can restore on a single-user device even if backed up from
a secondary user. Henceforth the userId in the payload should
be ignored

Test: Unlock user and dumpsys jobscheduler to look for update job
      Uninstall an app and look for prune job
      Manually force a backup from HSUM device main user and
      restore the dataset on a non-HSUM device

Bug: 260831016

Change-Id: I64904664eb0c949415a9a05eb919674d74081aba
This commit is contained in:
Amith Yamasani
2022-12-07 13:01:29 -08:00
parent dc0f1da44f
commit 807128ead9
7 changed files with 94 additions and 79 deletions

View File

@@ -123,6 +123,9 @@ public final class UserHandle implements Parcelable {
@TestApi
public static final int MIN_SECONDARY_USER_ID = 10;
/** @hide */
public static final int MAX_SECONDARY_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;
/**
* (Arbitrary) user handle cache size.
* {@link #CACHED_USER_HANDLES} caches user handles in the range of

View File

@@ -329,11 +329,11 @@ public abstract class UsageStatsManagerInternal {
* 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.
*
* @param userId The user to update
* @return {@code true} if the updating was successful, {@code false} otherwise
*/
public abstract boolean updatePackageMappingsData();
public abstract boolean updatePackageMappingsData(@UserIdInt int userId);
/**
* Listener interface for usage events.

View File

@@ -85,7 +85,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final Set<String> sEligibleForMultiUser = Sets.newArraySet(
PERMISSION_HELPER, NOTIFICATION_HELPER, SYNC_SETTINGS_HELPER, APP_LOCALES_HELPER,
ACCOUNT_MANAGER_HELPER);
ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER);
private int mUserId = UserHandle.USER_SYSTEM;
@@ -99,7 +99,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper(mUserId));
addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(mUserId));
addHelper(PERMISSION_HELPER, new PermissionBackupHelper(mUserId));
addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this));
addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(mUserId));
addHelper(SHORTCUT_MANAGER_HELPER, new ShortcutBackupHelper());
addHelper(ACCOUNT_MANAGER_HELPER, new AccountManagerBackupHelper(mUserId));
addHelper(SLICES_HELPER, new SliceBackupHelper(this));

View File

@@ -1,9 +1,9 @@
package com.android.server.backup;
import android.annotation.UserIdInt;
import android.app.backup.BlobBackupHelper;
import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.os.UserHandle;
import android.util.Log;
@@ -26,8 +26,16 @@ public class UsageStatsBackupHelper extends BlobBackupHelper {
// same as UsageStatsDatabase.KEY_USAGE_STATS
static final String KEY_USAGE_STATS = "usage_stats";
public UsageStatsBackupHelper(Context context) {
private final @UserIdInt int mUserId;
/**
* Marshall/unmarshall the usagestats data for the given user
*
* @param userId The userId to backup/restore
*/
public UsageStatsBackupHelper(@UserIdInt int userId) {
super(BLOB_VERSION, KEY_USAGE_STATS);
mUserId = userId;
}
@Override
@@ -38,8 +46,11 @@ public class UsageStatsBackupHelper extends BlobBackupHelper {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
try {
// Note: Write 0 here deliberately so that a backup from a secondary user
// can still be restored to an older OS where the restore was always to user 0
// Writing the actual userId here would result in restores not working on pre-U.
out.writeInt(UserHandle.USER_SYSTEM);
out.write(localUsageStatsManager.getBackupPayload(UserHandle.USER_SYSTEM, key));
out.write(localUsageStatsManager.getBackupPayload(mUserId, key));
} catch (IOException ioe) {
if (DEBUG) Log.e(TAG, "Failed to backup Usage Stats", ioe);
baos.reset();
@@ -49,7 +60,6 @@ public class UsageStatsBackupHelper extends BlobBackupHelper {
return null;
}
@Override
protected void applyRestoredPayload(String key, byte[] payload) {
if (KEY_USAGE_STATS.equals(key)) {
@@ -57,10 +67,10 @@ public class UsageStatsBackupHelper extends BlobBackupHelper {
LocalServices.getService(UsageStatsManagerInternal.class);
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
try {
int user = in.readInt();
in.readInt(); // Legacy userId parameter, read and ignore
byte[] restoreData = new byte[payload.length - 4];
in.read(restoreData, 0, restoreData.length);
localUsageStatsManager.applyRestoredPayload(user, key, restoreData);
localUsageStatsManager.applyRestoredPayload(mUserId, key, restoreData);
} catch (IOException ioe) {
if (DEBUG) Log.e(TAG, "Failed to restore Usage Stats", ioe);
}

View File

@@ -261,7 +261,7 @@ public class UserManagerService extends IUserManager.Stub {
// We need to keep process uid within Integer.MAX_VALUE.
@VisibleForTesting
static final int MAX_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;
static final int MAX_USER_ID = UserHandle.MAX_SECONDARY_USER_ID;
// Max size of the queue of recently removed users
@VisibleForTesting

View File

@@ -15,6 +15,7 @@
*/
package com.android.server.usage;
import android.annotation.UserIdInt;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
@@ -35,65 +36,71 @@ import java.util.concurrent.TimeUnit;
public class UsageStatsIdleService extends JobService {
/**
* Base job ID for the pruning job - must be unique within the system server uid.
* Namespace for prune job
*/
private static final int PRUNE_JOB_ID = 546357475;
private static final String PRUNE_JOB_NS = "usagestats_prune";
/**
* 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.
* Namespace for update mappings job
*/
private static final int UPDATE_MAPPINGS_JOB_ID = 546378950;
private static final String UPDATE_MAPPINGS_JOB_NS = "usagestats_mapping";
private static final String USER_ID_KEY = "user_id";
static void scheduleJob(Context context, int userId) {
final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user
/** Schedule a prune job */
static void schedulePruneJob(Context context, @UserIdInt int userId) {
final ComponentName component = new ComponentName(context.getPackageName(),
UsageStatsIdleService.class.getName());
final PersistableBundle bundle = new PersistableBundle();
bundle.putInt(USER_ID_KEY, userId);
final JobInfo pruneJob = new JobInfo.Builder(userJobId, component)
final JobInfo pruneJob = new JobInfo.Builder(userId, component)
.setRequiresDeviceIdle(true)
.setExtras(bundle)
.setPersisted(true)
.build();
scheduleJobInternal(context, pruneJob, userJobId);
scheduleJobInternal(context, pruneJob, PRUNE_JOB_NS, userId);
}
static void scheduleUpdateMappingsJob(Context context) {
static void scheduleUpdateMappingsJob(Context context, @UserIdInt int userId) {
final ComponentName component = new ComponentName(context.getPackageName(),
UsageStatsIdleService.class.getName());
final JobInfo updateMappingsJob = new JobInfo.Builder(UPDATE_MAPPINGS_JOB_ID, component)
final PersistableBundle bundle = new PersistableBundle();
bundle.putInt(USER_ID_KEY, userId);
final JobInfo updateMappingsJob = new JobInfo.Builder(userId, component)
.setPersisted(true)
.setMinimumLatency(TimeUnit.DAYS.toMillis(1))
.setOverrideDeadline(TimeUnit.DAYS.toMillis(2))
.setExtras(bundle)
.build();
scheduleJobInternal(context, updateMappingsJob, UPDATE_MAPPINGS_JOB_ID);
scheduleJobInternal(context, updateMappingsJob, UPDATE_MAPPINGS_JOB_NS, userId);
}
private static void scheduleJobInternal(Context context, JobInfo pruneJob, int jobId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
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(jobId); // cancel any previously scheduled prune job
jobScheduler.schedule(pruneJob);
private static void scheduleJobInternal(Context context, JobInfo jobInfo,
String namespace, int jobId) {
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
jobScheduler = jobScheduler.forNamespace(namespace);
final JobInfo pendingJob = jobScheduler.getPendingJob(jobId);
// only schedule a new job if one doesn't exist already for this user
if (!jobInfo.equals(pendingJob)) {
jobScheduler.cancel(jobId); // cancel any previously scheduled job
jobScheduler.schedule(jobInfo);
}
}
static void cancelJob(Context context, int userId) {
cancelJobInternal(context, PRUNE_JOB_ID + userId);
static void cancelPruneJob(Context context, @UserIdInt int userId) {
cancelJobInternal(context, PRUNE_JOB_NS, userId);
}
static void cancelUpdateMappingsJob(Context context) {
cancelJobInternal(context, UPDATE_MAPPINGS_JOB_ID);
static void cancelUpdateMappingsJob(Context context, @UserIdInt int userId) {
cancelJobInternal(context, UPDATE_MAPPINGS_JOB_NS, userId);
}
private static void cancelJobInternal(Context context, int jobId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
private static void cancelJobInternal(Context context, String namespace, int jobId) {
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler != null) {
jobScheduler = jobScheduler.forNamespace(namespace);
jobScheduler.cancel(jobId);
}
}
@@ -102,15 +109,19 @@ public class UsageStatsIdleService extends JobService {
public boolean onStartJob(JobParameters params) {
final PersistableBundle bundle = params.getExtras();
final int userId = bundle.getInt(USER_ID_KEY, -1);
if (userId == -1 && params.getJobId() != UPDATE_MAPPINGS_JOB_ID) {
if (userId == -1) { // legacy job
return false;
}
// Do async
AsyncTask.execute(() -> {
final UsageStatsManagerInternal usageStatsManagerInternal = LocalServices.getService(
UsageStatsManagerInternal.class);
if (params.getJobId() == UPDATE_MAPPINGS_JOB_ID) {
final boolean jobFinished = usageStatsManagerInternal.updatePackageMappingsData();
final String jobNs = params.getJobNamespace();
if (UPDATE_MAPPINGS_JOB_NS.equals(jobNs)) {
final boolean jobFinished =
usageStatsManagerInternal.updatePackageMappingsData(userId);
jobFinished(params, !jobFinished); // reschedule if data was not updated
} else {
final boolean jobFinished =
@@ -118,6 +129,8 @@ public class UsageStatsIdleService extends JobService {
jobFinished(params, !jobFinished); // reschedule if data was not pruned
}
});
// Job is running asynchronously
return true;
}

View File

@@ -433,11 +433,9 @@ 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());
}
UsageStatsIdleService.scheduleUpdateMappingsJob(getContext(), userId);
final boolean deleteObsoleteData = shouldDeleteObsoleteData(UserHandle.of(userId));
synchronized (mLock) {
// This should be safe to add this early. Other than reportEventOrAddToQueue and
@@ -1261,8 +1259,8 @@ public class UsageStatsService extends SystemService implements
}
mAppStandby.onUserRemoved(userId);
// Cancel any scheduled jobs for this user since the user is being removed.
UsageStatsIdleService.cancelJob(getContext(), userId);
UsageStatsIdleService.cancelUpdateMappingsJob(getContext());
UsageStatsIdleService.cancelPruneJob(getContext(), userId);
UsageStatsIdleService.cancelUpdateMappingsJob(getContext(), userId);
}
/**
@@ -1300,7 +1298,7 @@ public class UsageStatsService extends SystemService implements
// Schedule a job to prune any data related to this package.
if (tokenRemoved != PackagesTokenData.UNASSIGNED_TOKEN) {
UsageStatsIdleService.scheduleJob(getContext(), userId);
UsageStatsIdleService.schedulePruneJob(getContext(), userId);
}
}
@@ -1325,19 +1323,19 @@ public class UsageStatsService extends SystemService implements
/**
* Called by the Binder stub.
*/
private boolean updatePackageMappingsData() {
private boolean updatePackageMappingsData(@UserIdInt int userId) {
// don't update the mappings if a profile user is defined
if (!shouldDeleteObsoleteData(UserHandle.SYSTEM)) {
if (!shouldDeleteObsoleteData(UserHandle.of(userId))) {
return true; // return true so job scheduler doesn't reschedule the job
}
// fetch the installed packages outside the lock so it doesn't block package manager.
final HashMap<String, Long> installedPkgs = getInstalledPackages(UserHandle.USER_SYSTEM);
final HashMap<String, Long> installedPkgs = getInstalledPackages(userId);
synchronized (mLock) {
if (!mUserUnlockedStates.contains(UserHandle.USER_SYSTEM)) {
if (!mUserUnlockedStates.contains(userId)) {
return false; // user is no longer unlocked
}
final UserUsageStatsService userService = mUserState.get(UserHandle.USER_SYSTEM);
final UserUsageStatsService userService = mUserState.get(userId);
if (userService == null) {
return false; // user was stopped or removed
}
@@ -3055,44 +3053,35 @@ public class UsageStatsService extends SystemService implements
}
@Override
public byte[] getBackupPayload(int user, String key) {
if (!mUserUnlockedStates.contains(user)) {
Slog.w(TAG, "Failed to get backup payload for locked user " + user);
public byte[] getBackupPayload(@UserIdInt int userId, String key) {
if (!mUserUnlockedStates.contains(userId)) {
Slog.w(TAG, "Failed to get backup payload for locked user " + userId);
return null;
}
synchronized (mLock) {
// 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) {
return null; // user was stopped or removed
}
return userStats.getBackupPayload(key);
} else {
return null;
final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(userId);
if (userStats == null) {
return null; // user was stopped or removed
}
Slog.i(TAG, "Returning backup payload for u=" + userId);
return userStats.getBackupPayload(key);
}
}
@Override
public void applyRestoredPayload(int user, String key, byte[] payload) {
public void applyRestoredPayload(@UserIdInt int userId, String key, byte[] payload) {
synchronized (mLock) {
if (!mUserUnlockedStates.contains(user)) {
Slog.w(TAG, "Failed to apply restored payload for locked user " + user);
if (!mUserUnlockedStates.contains(userId)) {
Slog.w(TAG, "Failed to apply restored payload for locked user " + userId);
return;
}
if (user == UserHandle.USER_SYSTEM) {
final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(user);
if (userStats == null) {
return; // user was stopped or removed
}
final Set<String> restoredApps = userStats.applyRestoredPayload(key, payload);
mAppStandby.restoreAppsToRare(restoredApps, user);
final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(userId);
if (userStats == null) {
return; // user was stopped or removed
}
final Set<String> restoredApps = userStats.applyRestoredPayload(key, payload);
mAppStandby.restoreAppsToRare(restoredApps, userId);
}
}
@@ -3165,8 +3154,8 @@ public class UsageStatsService extends SystemService implements
}
@Override
public boolean updatePackageMappingsData() {
return UsageStatsService.this.updatePackageMappingsData();
public boolean updatePackageMappingsData(@UserIdInt int userId) {
return UsageStatsService.this.updatePackageMappingsData(userId);
}
/**