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

This commit is contained in:
Amith Yamasani
2023-01-21 00:16:47 +00:00
committed by Android (Google) Code Review
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

@@ -86,7 +86,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;
@@ -100,7 +100,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

@@ -262,7 +262,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);
}
/**