From 807128ead99b00b03643ad451bb2d3c6f7b61bdf Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Wed, 7 Dec 2022 13:01:29 -0800 Subject: [PATCH] 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 --- core/java/android/os/UserHandle.java | 3 + .../app/usage/UsageStatsManagerInternal.java | 4 +- .../server/backup/SystemBackupAgent.java | 4 +- .../server/backup/UsageStatsBackupHelper.java | 22 ++++-- .../android/server/pm/UserManagerService.java | 2 +- .../server/usage/UsageStatsIdleService.java | 69 +++++++++++-------- .../server/usage/UsageStatsService.java | 69 ++++++++----------- 7 files changed, 94 insertions(+), 79 deletions(-) diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index dca722e649bcf..4ce9184f0b969 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -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 diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java index a35aa7c74ee5a..70eeb7fecc8fa 100644 --- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -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. diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index 59d8afabbbe1f..fda4407c7cfd2 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -85,7 +85,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final Set 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)); diff --git a/services/core/java/com/android/server/backup/UsageStatsBackupHelper.java b/services/core/java/com/android/server/backup/UsageStatsBackupHelper.java index d6a70d369cdc6..4098c1a9cff91 100644 --- a/services/core/java/com/android/server/backup/UsageStatsBackupHelper.java +++ b/services/core/java/com/android/server/backup/UsageStatsBackupHelper.java @@ -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); } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a8cf8cb2b0348..51546c1cde020 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -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 diff --git a/services/usage/java/com/android/server/usage/UsageStatsIdleService.java b/services/usage/java/com/android/server/usage/UsageStatsIdleService.java index 3163820286777..20f03d8c1b2aa 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsIdleService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsIdleService.java @@ -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; } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index b3a1f2b85f638..7ff5b4a28f1bd 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -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 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 installedPkgs = getInstalledPackages(UserHandle.USER_SYSTEM); + final HashMap 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 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 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); } /**