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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user