diff options
13 files changed, 413 insertions, 472 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index 9a2d503d6d52..e38ebe61ddec 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -474,12 +474,22 @@ package android.app.prediction { package android.app.role { + public interface OnRoleHoldersChangedListener { + method public void onRoleHoldersChanged(@NonNull String, @NonNull android.os.UserHandle); + } + public final class RoleManager { + method @RequiresPermission("android.permission.OBSERVE_ROLE_HOLDERS") public void addOnRoleHoldersChangedListenerAsUser(@NonNull java.util.concurrent.Executor, @NonNull android.app.role.OnRoleHoldersChangedListener, @NonNull android.os.UserHandle); method @RequiresPermission("android.permission.MANAGE_ROLE_HOLDERS") public void addRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull android.app.role.RoleManagerCallback); + method @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean addRoleHolderFromController(@NonNull String, @NonNull String); method @RequiresPermission("android.permission.MANAGE_ROLE_HOLDERS") public void clearRoleHoldersAsUser(@NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull android.app.role.RoleManagerCallback); + method @NonNull @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public java.util.List<java.lang.String> getHeldRolesFromController(@NonNull String); method @NonNull @RequiresPermission("android.permission.MANAGE_ROLE_HOLDERS") public java.util.List<java.lang.String> getRoleHolders(@NonNull String); method @NonNull @RequiresPermission("android.permission.MANAGE_ROLE_HOLDERS") public java.util.List<java.lang.String> getRoleHoldersAsUser(@NonNull String, @NonNull android.os.UserHandle); + method @RequiresPermission("android.permission.OBSERVE_ROLE_HOLDERS") public void removeOnRoleHoldersChangedListenerAsUser(@NonNull android.app.role.OnRoleHoldersChangedListener, @NonNull android.os.UserHandle); method @RequiresPermission("android.permission.MANAGE_ROLE_HOLDERS") public void removeRoleHolderAsUser(@NonNull String, @NonNull String, int, @NonNull android.os.UserHandle, @NonNull java.util.concurrent.Executor, @NonNull android.app.role.RoleManagerCallback); + method @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public boolean removeRoleHolderFromController(@NonNull String, @NonNull String); + method @RequiresPermission("com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER") public void setRoleNamesFromController(@NonNull java.util.List<java.lang.String>); } public interface RoleManagerCallback { @@ -562,6 +572,10 @@ package android.content { method public android.view.Display getDisplay(); } + public class Intent implements java.lang.Cloneable android.os.Parcelable { + field public static final String EXTRA_ROLE_NAME = "android.intent.extra.ROLE_NAME"; + } + } package android.content.pm { diff --git a/core/java/android/app/role/OnRoleHoldersChangedListener.java b/core/java/android/app/role/OnRoleHoldersChangedListener.java index 5958debc86dd..d6f76794da20 100644 --- a/core/java/android/app/role/OnRoleHoldersChangedListener.java +++ b/core/java/android/app/role/OnRoleHoldersChangedListener.java @@ -18,6 +18,7 @@ package android.app.role; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.os.UserHandle; /** @@ -26,6 +27,7 @@ import android.os.UserHandle; * @hide */ @SystemApi +@TestApi public interface OnRoleHoldersChangedListener { /** diff --git a/core/java/android/app/role/RoleManager.java b/core/java/android/app/role/RoleManager.java index 7ec21f637e0c..c665cb238028 100644 --- a/core/java/android/app/role/RoleManager.java +++ b/core/java/android/app/role/RoleManager.java @@ -467,6 +467,7 @@ public final class RoleManager { */ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS) @SystemApi + @TestApi public void addOnRoleHoldersChangedListenerAsUser(@CallbackExecutor @NonNull Executor executor, @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) { Preconditions.checkNotNull(executor, "executor cannot be null"); @@ -512,6 +513,7 @@ public final class RoleManager { */ @RequiresPermission(Manifest.permission.OBSERVE_ROLE_HOLDERS) @SystemApi + @TestApi public void removeOnRoleHoldersChangedListenerAsUser( @NonNull OnRoleHoldersChangedListener listener, @NonNull UserHandle user) { Preconditions.checkNotNull(listener, "listener cannot be null"); @@ -553,6 +555,7 @@ public final class RoleManager { */ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) @SystemApi + @TestApi public void setRoleNamesFromController(@NonNull List<String> roleNames) { Preconditions.checkNotNull(roleNames, "roleNames cannot be null"); try { @@ -583,6 +586,7 @@ public final class RoleManager { */ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) @SystemApi + @TestApi public boolean addRoleHolderFromController(@NonNull String roleName, @NonNull String packageName) { Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); @@ -615,6 +619,7 @@ public final class RoleManager { */ @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) @SystemApi + @TestApi public boolean removeRoleHolderFromController(@NonNull String roleName, @NonNull String packageName) { Preconditions.checkStringNotEmpty(roleName, "roleName cannot be null or empty"); @@ -634,9 +639,10 @@ public final class RoleManager { * * @hide */ + @NonNull @RequiresPermission(PERMISSION_MANAGE_ROLES_FROM_CONTROLLER) @SystemApi - @NonNull + @TestApi public List<String> getHeldRolesFromController(@NonNull String packageName) { Preconditions.checkStringNotEmpty(packageName, "packageName cannot be null or empty"); try { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 0589f43aaa13..d3b8e29d7399 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -27,6 +27,7 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -1902,6 +1903,7 @@ public class Intent implements Parcelable, Cloneable { * @hide */ @SystemApi + @TestApi public static final String EXTRA_ROLE_NAME = "android.intent.extra.ROLE_NAME"; /** diff --git a/core/java/android/content/rollback/RollbackManager.java b/core/java/android/content/rollback/RollbackManager.java index 2788f821e9f5..c043491e599b 100644 --- a/core/java/android/content/rollback/RollbackManager.java +++ b/core/java/android/content/rollback/RollbackManager.java @@ -187,7 +187,8 @@ public final class RollbackManager { /** * Expire the rollback data for a given package. * This API is meant to facilitate testing of rollback logic for - * expiring rollback data. + * expiring rollback data. Removes rollback data for available and + * recently committed rollbacks that contain the given package. * * @param packageName the name of the package to expire data for. * @throws SecurityException if the caller does not have the diff --git a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java index 36f18f05c23e..56e1d080a988 100644 --- a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java +++ b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java @@ -18,7 +18,6 @@ package com.android.server.rollback; import android.content.rollback.PackageRollbackInfo; import android.content.rollback.PackageRollbackInfo.RestoreInfo; -import android.content.rollback.RollbackInfo; import android.os.storage.StorageManager; import android.util.IntArray; import android.util.Log; @@ -30,9 +29,11 @@ import com.android.server.pm.Installer.InstallerException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; /** * Encapsulates the logic for initiating userdata snapshots and rollbacks via installd. @@ -153,7 +154,7 @@ public class AppDataRollbackHelper { } /** - * Computes the list of pending backups for {@code userId} given lists of available rollbacks. + * Computes the list of pending backups for {@code userId} given lists of rollbacks. * Packages pending backup for the given user are added to {@code pendingBackupPackages} along * with their corresponding {@code PackageRollbackInfo}. * @@ -162,10 +163,10 @@ public class AppDataRollbackHelper { */ private static List<RollbackData> computePendingBackups(int userId, Map<String, PackageRollbackInfo> pendingBackupPackages, - List<RollbackData> availableRollbacks) { + List<RollbackData> rollbacks) { List<RollbackData> rd = new ArrayList<>(); - for (RollbackData data : availableRollbacks) { + for (RollbackData data : rollbacks) { for (PackageRollbackInfo info : data.info.getPackages()) { final IntArray pendingBackupUsers = info.getPendingBackups(); if (pendingBackupUsers != null) { @@ -183,20 +184,20 @@ public class AppDataRollbackHelper { } /** - * Computes the list of pending restores for {@code userId} given lists of recent rollbacks. + * Computes the list of pending restores for {@code userId} given lists of rollbacks. * Packages pending restore are added to {@code pendingRestores} along with their corresponding * {@code PackageRollbackInfo}. * - * @return the list of {@code RollbackInfo} that has pending restores. Note that some of the + * @return the list of {@code RollbackData} that has pending restores. Note that some of the * restores won't be performed, because they might be counteracted by pending backups. */ - private static List<RollbackInfo> computePendingRestores(int userId, + private static List<RollbackData> computePendingRestores(int userId, Map<String, PackageRollbackInfo> pendingRestorePackages, - List<RollbackInfo> recentRollbacks) { - List<RollbackInfo> rd = new ArrayList<>(); + List<RollbackData> rollbacks) { + List<RollbackData> rd = new ArrayList<>(); - for (RollbackInfo data : recentRollbacks) { - for (PackageRollbackInfo info : data.getPackages()) { + for (RollbackData data : rollbacks) { + for (PackageRollbackInfo info : data.info.getPackages()) { final RestoreInfo ri = info.getRestoreInfo(userId); if (ri != null) { pendingRestorePackages.put(info.getPackageName(), info); @@ -215,18 +216,18 @@ public class AppDataRollbackHelper { * backups updates corresponding {@code changedRollbackData} with a mapping from {@code userId} * to a inode of theirs CE user data snapshot. * - * @return a list {@code RollbackData} that have been changed and should be stored on disk. + * @return the set of {@code RollbackData} that have been changed and should be stored on disk. */ - public List<RollbackData> commitPendingBackupAndRestoreForUser(int userId, - List<RollbackData> availableRollbacks, List<RollbackInfo> recentlyExecutedRollbacks) { + public Set<RollbackData> commitPendingBackupAndRestoreForUser(int userId, + List<RollbackData> rollbacks) { final Map<String, PackageRollbackInfo> pendingBackupPackages = new HashMap<>(); final List<RollbackData> pendingBackups = computePendingBackups(userId, - pendingBackupPackages, availableRollbacks); + pendingBackupPackages, rollbacks); final Map<String, PackageRollbackInfo> pendingRestorePackages = new HashMap<>(); - final List<RollbackInfo> pendingRestores = computePendingRestores(userId, - pendingRestorePackages, recentlyExecutedRollbacks); + final List<RollbackData> pendingRestores = computePendingRestores(userId, + pendingRestorePackages, rollbacks); // First remove unnecessary backups, i.e. when user did not unlock their phone between the // request to backup data and the request to restore it. @@ -266,13 +267,13 @@ public class AppDataRollbackHelper { } if (!pendingRestorePackages.isEmpty()) { - for (RollbackInfo data : pendingRestores) { - for (PackageRollbackInfo info : data.getPackages()) { + for (RollbackData data : pendingRestores) { + for (PackageRollbackInfo info : data.info.getPackages()) { final RestoreInfo ri = info.getRestoreInfo(userId); if (ri != null) { try { mInstaller.restoreAppDataSnapshot(info.getPackageName(), ri.appId, - ri.seInfo, userId, data.getRollbackId(), + ri.seInfo, userId, data.info.getRollbackId(), Installer.FLAG_STORAGE_CE); info.removeRestoreInfo(ri); } catch (InstallerException ie) { @@ -284,7 +285,9 @@ public class AppDataRollbackHelper { } } - return pendingBackups; + final Set<RollbackData> changed = new HashSet<>(pendingBackups); + changed.addAll(pendingRestores); + return changed; } /** diff --git a/services/core/java/com/android/server/rollback/RollbackData.java b/services/core/java/com/android/server/rollback/RollbackData.java index 655bf4ab57a7..8a95877be71d 100644 --- a/services/core/java/com/android/server/rollback/RollbackData.java +++ b/services/core/java/com/android/server/rollback/RollbackData.java @@ -16,9 +16,13 @@ package com.android.server.rollback; +import android.annotation.IntDef; +import android.annotation.NonNull; import android.content.rollback.RollbackInfo; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.time.Instant; import java.util.ArrayList; @@ -27,6 +31,30 @@ import java.util.ArrayList; * packages. */ class RollbackData { + @IntDef(flag = true, prefix = { "ROLLBACK_STATE_" }, value = { + ROLLBACK_STATE_ENABLING, + ROLLBACK_STATE_AVAILABLE, + ROLLBACK_STATE_COMMITTED, + }) + @Retention(RetentionPolicy.SOURCE) + @interface RollbackState {} + + /** + * The rollback is in the process of being enabled. It is not yet + * available for use. + */ + static final int ROLLBACK_STATE_ENABLING = 0; + + /** + * The rollback is currently available. + */ + static final int ROLLBACK_STATE_AVAILABLE = 1; + + /** + * The rollback has been committed. + */ + static final int ROLLBACK_STATE_COMMITTED = 3; + /** * The rollback info for this rollback. */ @@ -40,22 +68,23 @@ class RollbackData { /** * The time when the upgrade occurred, for purposes of expiring * rollback data. + * + * The timestamp is not applicable for all rollback states, but we make + * sure to keep it non-null to avoid potential errors there. */ - public Instant timestamp; + public @NonNull Instant timestamp; /** * The session ID for the staged session if this rollback data represents a staged session, * {@code -1} otherwise. */ - public int stagedSessionId; + public final int stagedSessionId; /** - * A flag to indicate whether the rollback should be considered available - * for use. This will always be true for rollbacks of non-staged sessions. - * For rollbacks of staged sessions, this is not set to true until after - * the staged session has been applied. + * The current state of the rollback. + * ENABLING, AVAILABLE, or COMMITTED. */ - public boolean isAvailable; + public @RollbackState int state; /** * The id of the post-reboot apk session for a staged install, if any. @@ -85,19 +114,20 @@ class RollbackData { /* committedSessionId */ -1); this.backupDir = backupDir; this.stagedSessionId = stagedSessionId; - this.isAvailable = (stagedSessionId == -1); + this.state = ROLLBACK_STATE_ENABLING; + this.timestamp = Instant.now(); } /** * Constructs a RollbackData instance with full rollback data information. */ RollbackData(RollbackInfo info, File backupDir, Instant timestamp, int stagedSessionId, - boolean isAvailable, int apkSessionId, boolean restoreUserDataInProgress) { + @RollbackState int state, int apkSessionId, boolean restoreUserDataInProgress) { this.info = info; this.backupDir = backupDir; this.timestamp = timestamp; this.stagedSessionId = stagedSessionId; - this.isAvailable = isAvailable; + this.state = state; this.apkSessionId = apkSessionId; this.restoreUserDataInProgress = restoreUserDataInProgress; } diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index 52d441255da6..f52f3a3627ad 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -64,6 +64,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -75,7 +76,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { private static final String TAG = "RollbackManager"; // Rollbacks expire after 48 hours. - // TODO: How to test rollback expiration works properly? private static final long DEFAULT_ROLLBACK_LIFETIME_DURATION_MILLIS = TimeUnit.HOURS.toMillis(48); @@ -106,15 +106,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { @GuardedBy("mLock") private final Map<Integer, Integer> mChildSessions = new HashMap<>(); - // Package rollback data available to be used for rolling back a package. + // The list of all rollbacks, including available and committed rollbacks. // This list is null until the rollback data has been loaded. @GuardedBy("mLock") - private List<RollbackData> mAvailableRollbacks; - - // The list of recently executed rollbacks. - // This list is null until the rollback data has been loaded. - @GuardedBy("mLock") - private List<RollbackInfo> mRecentlyExecutedRollbacks; + private List<RollbackData> mRollbacks; private final RollbackStore mRollbackStore; @@ -176,17 +171,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } }, filter, null, getHandler()); - // NOTE: A new intent filter is being created here because this broadcast - // doesn't use a data scheme ("package") like above. - IntentFilter sessionUpdatedFilter = new IntentFilter(); - sessionUpdatedFilter.addAction(PackageInstaller.ACTION_SESSION_UPDATED); - mContext.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - onStagedSessionUpdated(intent); - } - }, sessionUpdatedFilter, null, getHandler()); - IntentFilter enableRollbackFilter = new IntentFilter(); enableRollbackFilter.addAction(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK); try { @@ -240,9 +224,9 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { synchronized (mLock) { ensureRollbackDataLoadedLocked(); List<RollbackInfo> rollbacks = new ArrayList<>(); - for (int i = 0; i < mAvailableRollbacks.size(); ++i) { - RollbackData data = mAvailableRollbacks.get(i); - if (data.isAvailable) { + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); + if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE) { rollbacks.add(data.info); } } @@ -258,7 +242,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { synchronized (mLock) { ensureRollbackDataLoadedLocked(); - List<RollbackInfo> rollbacks = new ArrayList<>(mRecentlyExecutedRollbacks); + List<RollbackInfo> rollbacks = new ArrayList<>(); + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); + if (data.state == RollbackData.ROLLBACK_STATE_COMMITTED) { + rollbacks.add(data.info); + } + } return new ParceledListSlice<>(rollbacks); } } @@ -290,21 +280,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { synchronized (mLock) { ensureRollbackDataLoadedLocked(); - Iterator<RollbackData> iter = mAvailableRollbacks.iterator(); + Iterator<RollbackData> iter = mRollbacks.iterator(); while (iter.hasNext()) { RollbackData data = iter.next(); - data.timestamp = data.timestamp.plusMillis(timeDifference); - try { - mRollbackStore.saveRollbackData(data); - } catch (IOException ioe) { - // TODO: figure out the right way to deal with this, especially if - // it fails for some data and succeeds for others. - Log.e(TAG, "Unable to save rollback info for : " - + data.info.getRollbackId(), ioe); - } + saveRollbackData(data); } - } } }; @@ -328,18 +309,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { Log.i(TAG, "Initiating rollback"); RollbackData data = getRollbackForId(rollbackId); - if (data == null) { + if (data == null || data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) { sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE, "Rollback unavailable"); return; } - if (data.restoreUserDataInProgress) { - sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE, - "Rollback for package is already in progress."); - return; - } - // Verify the RollbackData is up to date with what's installed on // device. // TODO: We assume that between now and the time we commit the @@ -438,13 +413,25 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { final LocalIntentReceiver receiver = new LocalIntentReceiver( (Intent result) -> { getHandler().post(() -> { - // We've now completed the rollback, so we mark it as no longer in - // progress. - data.restoreUserDataInProgress = false; int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); if (status != PackageInstaller.STATUS_SUCCESS) { + // Committing the rollback failed, but we + // still have all the info we need to try + // rolling back again, so restore the rollback + // state to how it was before we tried + // committing. + // TODO: Should we just kill this rollback if + // commit failed? Why would we expect commit + // not to fail again? + synchronized (mLock) { + // TODO: Could this cause a rollback to be + // resurrected if it should otherwise have + // expired by now? + data.state = RollbackData.ROLLBACK_STATE_AVAILABLE; + data.restoreUserDataInProgress = false; + } sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_INSTALL, "Rollback downgrade install failed: " + result.getStringExtra( @@ -452,9 +439,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { return; } - data.info.setCommittedSessionId(parentSessionId); - data.info.getCausePackages().addAll(causePackages); - addRecentlyExecutedRollback(data.info); + synchronized (mLock) { + if (!data.isStaged()) { + // All calls to restoreUserData should have + // completed by now for a non-staged install. + data.restoreUserDataInProgress = false; + } + + data.info.setCommittedSessionId(parentSessionId); + data.info.getCausePackages().addAll(causePackages); + } + mRollbackStore.deletePackageCodePaths(data); + saveRollbackData(data); + sendSuccess(statusReceiver); Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED); @@ -468,7 +465,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } ); - data.restoreUserDataInProgress = true; + synchronized (mLock) { + data.state = RollbackData.ROLLBACK_STATE_COMMITTED; + data.restoreUserDataInProgress = true; + } parentSession.commit(receiver.getIntentSender()); } catch (IOException e) { Log.e(TAG, "Rollback failed", e); @@ -485,8 +485,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { "reloadPersistedData"); synchronized (mLock) { - mAvailableRollbacks = null; - mRecentlyExecutedRollbacks = null; + mRollbacks = null; } getHandler().post(() -> { updateRollbackLifetimeDurationInMillis(); @@ -499,14 +498,9 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.MANAGE_ROLLBACKS, "expireRollbackForPackage"); - - // TODO: Should this take a package version number in addition to - // package name? For now, just remove all rollbacks matching the - // package name. This method is only currently used to facilitate - // testing anyway. synchronized (mLock) { ensureRollbackDataLoadedLocked(); - Iterator<RollbackData> iter = mAvailableRollbacks.iterator(); + Iterator<RollbackData> iter = mRollbacks.iterator(); while (iter.hasNext()) { RollbackData data = iter.next(); for (PackageRollbackInfo info : data.info.getPackages()) { @@ -522,29 +516,16 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { void onUnlockUser(int userId) { getHandler().post(() -> { - final List<RollbackData> availableRollbacks; - final List<RollbackInfo> recentlyExecutedRollbacks; + final List<RollbackData> rollbacks; synchronized (mLock) { - ensureRollbackDataLoadedLocked(); - availableRollbacks = new ArrayList<>(mAvailableRollbacks); - recentlyExecutedRollbacks = new ArrayList<>(mRecentlyExecutedRollbacks); + rollbacks = new ArrayList<>(mRollbacks); } - final List<RollbackData> changed = - mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId, - availableRollbacks, recentlyExecutedRollbacks); + final Set<RollbackData> changed = + mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId, rollbacks); for (RollbackData rd : changed) { - try { - mRollbackStore.saveRollbackData(rd); - } catch (IOException ioe) { - Log.e(TAG, "Unable to save rollback info for : " - + rd.info.getRollbackId(), ioe); - } - } - - synchronized (mLock) { - mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks); + saveRollbackData(rd); } }); } @@ -569,42 +550,55 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { scheduleExpiration(0); getHandler().post(() -> { - // Check to see if any staged sessions with rollback enabled have - // been applied. - List<RollbackData> staged = new ArrayList<>(); + // Check to see if any rollback-enabled staged sessions or staged + // rollback sessions been applied. + List<RollbackData> enabling = new ArrayList<>(); + List<RollbackData> restoreInProgress = new ArrayList<>(); synchronized (mLock) { ensureRollbackDataLoadedLocked(); - for (RollbackData data : mAvailableRollbacks) { - if (!data.isAvailable && data.isStaged()) { - staged.add(data); + for (RollbackData data : mRollbacks) { + if (data.isStaged()) { + if (data.state == RollbackData.ROLLBACK_STATE_ENABLING) { + enabling.add(data); + } else if (data.restoreUserDataInProgress) { + restoreInProgress.add(data); + } } } } - for (RollbackData data : staged) { + for (RollbackData data : enabling) { PackageInstaller installer = mContext.getPackageManager().getPackageInstaller(); PackageInstaller.SessionInfo session = installer.getSessionInfo( data.stagedSessionId); + // TODO: What if session is null? if (session != null) { if (session.isStagedSessionApplied()) { - synchronized (mLock) { - data.isAvailable = true; - } - try { - mRollbackStore.saveRollbackData(data); - } catch (IOException ioe) { - Log.e(TAG, "Unable to save rollback info for : " - + data.info.getRollbackId(), ioe); - } + makeRollbackAvailable(data); } else if (session.isStagedSessionFailed()) { // TODO: Do we need to remove this from - // mAvailableRollbacks, or is it okay to leave as + // mRollbacks, or is it okay to leave as // unavailable until the next reboot when it will go // away on its own? deleteRollback(data); } } } + + for (RollbackData data : restoreInProgress) { + PackageInstaller installer = mContext.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionInfo session = installer.getSessionInfo( + data.stagedSessionId); + // TODO: What if session is null? + if (session != null) { + if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) { + synchronized (mLock) { + data.restoreUserDataInProgress = false; + } + saveRollbackData(data); + } + } + } }); } @@ -621,12 +615,11 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { /** * Load rollback data from storage if it has not already been loaded. - * After calling this function, mAvailableRollbacks and - * mRecentlyExecutedRollbacks will be non-null. + * After calling this function, mRollbacks will be non-null. */ @GuardedBy("mLock") private void ensureRollbackDataLoadedLocked() { - if (mAvailableRollbacks == null) { + if (mRollbacks == null) { loadAllRollbackDataLocked(); } } @@ -639,15 +632,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { */ @GuardedBy("mLock") private void loadAllRollbackDataLocked() { - mAvailableRollbacks = mRollbackStore.loadAvailableRollbacks(); - for (RollbackData data : mAvailableRollbacks) { + mRollbacks = mRollbackStore.loadAllRollbackData(); + for (RollbackData data : mRollbacks) { mAllocatedRollbackIds.put(data.info.getRollbackId(), true); } - - mRecentlyExecutedRollbacks = mRollbackStore.loadRecentlyExecutedRollbacks(); - for (RollbackInfo info : mRecentlyExecutedRollbacks) { - mAllocatedRollbackIds.put(info.getRollbackId(), true); - } } /** @@ -662,17 +650,21 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { synchronized (mLock) { ensureRollbackDataLoadedLocked(); - Iterator<RollbackData> iter = mAvailableRollbacks.iterator(); + Iterator<RollbackData> iter = mRollbacks.iterator(); while (iter.hasNext()) { RollbackData data = iter.next(); - for (PackageRollbackInfo info : data.info.getPackages()) { - if (info.getPackageName().equals(packageName) - && !packageVersionsEqual( - info.getVersionRolledBackFrom(), - installedVersion)) { - iter.remove(); - deleteRollback(data); - break; + // TODO: Should we remove rollbacks in the ENABLING state here? + if (data.state == RollbackData.ROLLBACK_STATE_AVAILABLE + || data.state == RollbackData.ROLLBACK_STATE_ENABLING) { + for (PackageRollbackInfo info : data.info.getPackages()) { + if (info.getPackageName().equals(packageName) + && !packageVersionsEqual( + info.getVersionRolledBackFrom(), + installedVersion)) { + iter.remove(); + deleteRollback(data); + break; + } } } } @@ -685,53 +677,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { */ private void onPackageFullyRemoved(String packageName) { expireRollbackForPackage(packageName); - - synchronized (mLock) { - ensureRollbackDataLoadedLocked(); - Iterator<RollbackInfo> iter = mRecentlyExecutedRollbacks.iterator(); - boolean changed = false; - while (iter.hasNext()) { - RollbackInfo rollback = iter.next(); - for (PackageRollbackInfo info : rollback.getPackages()) { - if (packageName.equals(info.getPackageName())) { - iter.remove(); - changed = true; - break; - } - } - } - - if (changed) { - mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks); - } - } - } - - /** - * Records that the given package has been recently rolled back. - */ - private void addRecentlyExecutedRollback(RollbackInfo rollback) { - // TODO: if the list of rollbacks gets too big, trim it to only those - // that are necessary to keep track of. - synchronized (mLock) { - ensureRollbackDataLoadedLocked(); - - // This should never happen because we can't have any pending backups left after - // a rollback has been executed. See AppDataRollbackHelper#restoreAppData where we - // clear all pending backups at the point of restore because they're guaranteed to be - // no-ops. - // - // We may, however, have one or more pending restores left to handle. - for (PackageRollbackInfo target : rollback.getPackages()) { - if (target.getPendingBackups().size() > 0) { - Log.e(TAG, "No backups allowed to be pending for: " + target); - target.getPendingBackups().clear(); - } - } - - mRecentlyExecutedRollbacks.add(rollback); - mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks); - } } /** @@ -768,17 +713,16 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { // Check to see if anything needs expiration, and if so, expire it. // Schedules future expiration as appropriate. - // TODO: Handle cases where the user changes time on the device. private void runExpiration() { Instant now = Instant.now(); Instant oldest = null; synchronized (mLock) { ensureRollbackDataLoadedLocked(); - Iterator<RollbackData> iter = mAvailableRollbacks.iterator(); + Iterator<RollbackData> iter = mRollbacks.iterator(); while (iter.hasNext()) { RollbackData data = iter.next(); - if (!data.isAvailable) { + if (data.state != RollbackData.ROLLBACK_STATE_AVAILABLE) { continue; } if (!now.isBefore(data.timestamp.plusMillis(mRollbackLifetimeDurationInMillis))) { @@ -876,8 +820,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { RollbackData rd = null; synchronized (mLock) { ensureRollbackDataLoadedLocked(); - for (int i = 0; i < mAvailableRollbacks.size(); ++i) { - RollbackData data = mAvailableRollbacks.get(i); + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); if (data.apkSessionId == parentSessionId) { rd = data; break; @@ -901,16 +845,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { if (info.getPackageName().equals(packageName)) { info.getInstalledUsers().addAll(IntArray.wrap(installedUsers)); mAppDataRollbackHelper.snapshotAppData(rd.info.getRollbackId(), info); - try { - mRollbackStore.saveRollbackData(rd); - } catch (IOException ioe) { - // TODO: Hopefully this is okay because we will try - // again to save the rollback when the staged session - // is applied. Just so long as the device doesn't - // reboot before then. - Log.e(TAG, "Unable to save rollback info for : " - + rd.info.getRollbackId(), ioe); - } + saveRollbackData(rd); return true; } } @@ -1039,32 +974,33 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { private void restoreUserDataInternal(String packageName, int[] userIds, int appId, long ceDataInode, String seInfo, int token) { - final RollbackData rollbackData = getRollbackForPackage(packageName); - if (rollbackData == null) { - return; + PackageRollbackInfo info = null; + RollbackData rollbackData = null; + synchronized (mLock) { + ensureRollbackDataLoadedLocked(); + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); + if (data.restoreUserDataInProgress) { + info = getPackageRollbackInfo(data, packageName); + if (info != null) { + rollbackData = data; + break; + } + } + } } - if (!rollbackData.restoreUserDataInProgress) { - Log.e(TAG, "Request to restore userData for: " + packageName - + ", but no rollback in progress."); + if (rollbackData == null) { return; } for (int userId : userIds) { - final PackageRollbackInfo info = getPackageRollbackInfo(rollbackData, packageName); final boolean changedRollbackData = mAppDataRollbackHelper.restoreAppData( rollbackData.info.getRollbackId(), info, userId, appId, seInfo); // We've updated metadata about this rollback, so save it to flash. if (changedRollbackData) { - try { - mRollbackStore.saveRollbackData(rollbackData); - } catch (IOException ioe) { - // TODO(narayan): What is the right thing to do here ? This isn't a fatal - // error, since it will only result in us trying to restore data again, - // which will be a no-op if there's no data available. - Log.e(TAG, "Unable to save available rollback: " + packageName, ioe); - } + saveRollbackData(rollbackData); } } } @@ -1108,6 +1044,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } } + completeEnableRollback(sessionId, true); result.offer(true); }); @@ -1125,8 +1062,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { RollbackData rd = null; synchronized (mLock) { ensureRollbackDataLoadedLocked(); - for (int i = 0; i < mAvailableRollbacks.size(); ++i) { - RollbackData data = mAvailableRollbacks.get(i); + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); if (data.stagedSessionId == originalSessionId) { data.apkSessionId = apkSessionId; rd = data; @@ -1136,12 +1073,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } if (rd != null) { - try { - mRollbackStore.saveRollbackData(rd); - } catch (IOException ioe) { - Log.e(TAG, "Unable to save rollback info for : " - + rd.info.getRollbackId(), ioe); - } + saveRollbackData(rd); } }); } @@ -1183,21 +1115,21 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { @Override public void onFinished(int sessionId, boolean success) { - // If sessionId refers to a staged session, we can't deal with it here since the - // session might take an unbounded amount of time to become "ready" after the package - // installer session is committed. In those cases, we respond to it in response to - // a session ready broadcast. - PackageInstaller packageInstaller = mContext.getPackageManager().getPackageInstaller(); - PackageInstaller.SessionInfo si = packageInstaller.getSessionInfo(sessionId); - if (si != null && si.isStaged()) { - return; + RollbackData rollback = completeEnableRollback(sessionId, success); + if (rollback != null && !rollback.isStaged()) { + makeRollbackAvailable(rollback); } - - completeEnableRollback(sessionId, success); } } - private void completeEnableRollback(int sessionId, boolean success) { + /** + * Add a rollback to the list of rollbacks. + * This should be called after rollback has been enabled for all packages + * in the rollback. It does not make the rollback available yet. + * + * @return the rollback data for a successfully enable-completed rollback. + */ + private RollbackData completeEnableRollback(int sessionId, boolean success) { RollbackData data = null; synchronized (mLock) { Integer parentSessionId = mChildSessions.remove(sessionId); @@ -1208,107 +1140,71 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { data = mPendingRollbacks.remove(sessionId); } - if (data != null) { - if (success) { - try { - data.timestamp = Instant.now(); - - mRollbackStore.saveRollbackData(data); - synchronized (mLock) { - // Note: There is a small window of time between when - // the session has been committed by the package - // manager and when we make the rollback available - // here. Presumably the window is small enough that - // nobody will want to roll back the newly installed - // package before we make the rollback available. - // TODO: We'll lose the rollback data if the - // device reboots between when the session is - // committed and this point. Revisit this after - // adding support for rollback of staged installs. - ensureRollbackDataLoadedLocked(); - mAvailableRollbacks.add(data); - } - // TODO(zezeozue): Provide API to explicitly start observing instead - // of doing this for all rollbacks. If we do this for all rollbacks, - // should document in PackageInstaller.SessionParams#setEnableRollback - // After enabling and commiting any rollback, observe packages and - // prepare to rollback if packages crashes too frequently. - List<String> packages = new ArrayList<>(); - for (int i = 0; i < data.info.getPackages().size(); i++) { - packages.add(data.info.getPackages().get(i).getPackageName()); - } - mPackageHealthObserver.startObservingHealth(packages, - mRollbackLifetimeDurationInMillis); - scheduleExpiration(mRollbackLifetimeDurationInMillis); - } catch (IOException e) { - Log.e(TAG, "Unable to enable rollback", e); - deleteRollback(data); - } - } else { - // The install session was aborted, clean up the pending - // install. - deleteRollback(data); - } + if (data == null) { + return null; } - } - private void onStagedSessionUpdated(Intent intent) { - PackageInstaller.SessionInfo pi = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); - if (pi == null) { - Log.e(TAG, "Missing intent extra: " + PackageInstaller.EXTRA_SESSION); - return; + if (!success) { + // The install session was aborted, clean up the pending install. + deleteRollback(data); + return null; } - if (pi.isStaged()) { - if (!pi.isStagedSessionFailed()) { - // TODO: The session really isn't "enabled" at this point, since more work might - // be required post reboot. - // TODO: We need to make this case consistent with the call from onFinished. - // Ideally, we'd call completeEnableRollback excatly once per multi-package session - // with the parentSessionId only. - completeEnableRollback(pi.sessionId, pi.isStagedSessionReady()); - } else { - // TODO: Clean up the saved rollback when the session fails. This may need to be - // unified with the case where things fail post reboot. - } - } else { - Log.e(TAG, "Received onStagedSessionUpdated for: " + pi.sessionId - + ", which isn't staged"); + saveRollbackData(data); + synchronized (mLock) { + // Note: There is a small window of time between when + // the session has been committed by the package + // manager and when we make the rollback available + // here. Presumably the window is small enough that + // nobody will want to roll back the newly installed + // package before we make the rollback available. + // TODO: We'll lose the rollback data if the + // device reboots between when the session is + // committed and this point. Revisit this after + // adding support for rollback of staged installs. + ensureRollbackDataLoadedLocked(); + mRollbacks.add(data); } + + return data; } - /* - * Returns the RollbackData, if any, for an available rollback that would - * roll back the given package. Note: This assumes we have at most one - * available rollback for a given package at any one time. - */ - private RollbackData getRollbackForPackage(String packageName) { + private void makeRollbackAvailable(RollbackData data) { + // TODO: What if the rollback has since been expired, for example due + // to a new package being installed. Won't this revive an expired + // rollback? Consider adding a ROLLBACK_STATE_EXPIRED to address this. synchronized (mLock) { - // TODO: Have ensureRollbackDataLoadedLocked return the list of - // available rollbacks, to hopefully avoid forgetting to call it? - ensureRollbackDataLoadedLocked(); - for (int i = 0; i < mAvailableRollbacks.size(); ++i) { - RollbackData data = mAvailableRollbacks.get(i); - if (data.isAvailable && getPackageRollbackInfo(data, packageName) != null) { - return data; - } - } + data.state = RollbackData.ROLLBACK_STATE_AVAILABLE; + data.timestamp = Instant.now(); } - return null; + saveRollbackData(data); + + // TODO(zezeozue): Provide API to explicitly start observing instead + // of doing this for all rollbacks. If we do this for all rollbacks, + // should document in PackageInstaller.SessionParams#setEnableRollback + // After enabling and commiting any rollback, observe packages and + // prepare to rollback if packages crashes too frequently. + List<String> packages = new ArrayList<>(); + for (int i = 0; i < data.info.getPackages().size(); i++) { + packages.add(data.info.getPackages().get(i).getPackageName()); + } + mPackageHealthObserver.startObservingHealth(packages, + mRollbackLifetimeDurationInMillis); + scheduleExpiration(mRollbackLifetimeDurationInMillis); } /* - * Returns the RollbackData, if any, for an available rollback with the - * given rollbackId. + * Returns the RollbackData, if any, for a rollback with the given + * rollbackId. */ private RollbackData getRollbackForId(int rollbackId) { synchronized (mLock) { // TODO: Have ensureRollbackDataLoadedLocked return the list of // available rollbacks, to hopefully avoid forgetting to call it? ensureRollbackDataLoadedLocked(); - for (int i = 0; i < mAvailableRollbacks.size(); ++i) { - RollbackData data = mAvailableRollbacks.get(i); - if (data.isAvailable && data.info.getRollbackId() == rollbackId) { + for (int i = 0; i < mRollbacks.size(); ++i) { + RollbackData data = mRollbacks.get(i); + if (data.info.getRollbackId() == rollbackId) { return data; } } @@ -1358,4 +1254,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } mRollbackStore.deleteRollbackData(rollbackData); } + + /** + * Saves rollback data, swallowing any IOExceptions. + * For those times when it's not obvious what to do about the IOException. + * TODO: Double check we can't do a better job handling the IOException in + * a cases where this method is called. + */ + private void saveRollbackData(RollbackData rollbackData) { + try { + mRollbackStore.saveRollbackData(rollbackData); + } catch (IOException ioe) { + Log.e(TAG, "Unable to save rollback info for: " + + rollbackData.info.getRollbackId(), ioe); + } + } } diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index ecdb2ccd872b..4f8f6856bb6f 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -35,6 +35,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; +import java.text.ParseException; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.ArrayList; @@ -47,60 +48,44 @@ class RollbackStore { private static final String TAG = "RollbackManager"; // Assuming the rollback data directory is /data/rollback, we use the - // following directory structure to store persisted data for available and - // recently executed rollbacks: + // following directory structure to store persisted data for rollbacks: // /data/rollback/ - // available/ - // XXX/ - // rollback.json - // com.package.A/ - // base.apk - // com.package.B/ - // base.apk - // YYY/ - // rollback.json - // com.package.C/ - // base.apk - // recently_executed.json + // XXX/ + // rollback.json + // com.package.A/ + // base.apk + // com.package.B/ + // base.apk + // YYY/ + // rollback.json // // * XXX, YYY are the rollbackIds for the corresponding rollbacks. - // * rollback.json contains all relevant metadata for the rollback. This - // file is not written until the rollback is made available. + // * rollback.json contains all relevant metadata for the rollback. // // TODO: Use AtomicFile for all the .json files? private final File mRollbackDataDir; - private final File mAvailableRollbacksDir; - private final File mRecentlyExecutedRollbacksFile; RollbackStore(File rollbackDataDir) { mRollbackDataDir = rollbackDataDir; - mAvailableRollbacksDir = new File(mRollbackDataDir, "available"); - mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json"); } /** - * Reads the list of available rollbacks from persistent storage. + * Reads the rollback data from persistent storage. */ - List<RollbackData> loadAvailableRollbacks() { - List<RollbackData> availableRollbacks = new ArrayList<>(); - mAvailableRollbacksDir.mkdirs(); - for (File rollbackDir : mAvailableRollbacksDir.listFiles()) { + List<RollbackData> loadAllRollbackData() { + List<RollbackData> rollbacks = new ArrayList<>(); + mRollbackDataDir.mkdirs(); + for (File rollbackDir : mRollbackDataDir.listFiles()) { if (rollbackDir.isDirectory()) { try { - RollbackData data = loadRollbackData(rollbackDir); - availableRollbacks.add(data); + rollbacks.add(loadRollbackData(rollbackDir)); } catch (IOException e) { - // Note: Deleting the rollbackDir here will cause pending - // rollbacks to be deleted. This should only ever happen - // if reloadPersistedData is called while there are - // pending rollbacks. The reloadPersistedData method is - // currently only for testing, so that should be okay. Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e); removeFile(rollbackDir); } } } - return availableRollbacks; + return rollbacks; } /** @@ -203,37 +188,11 @@ class RollbackStore { } /** - * Reads the list of recently executed rollbacks from persistent storage. - */ - List<RollbackInfo> loadRecentlyExecutedRollbacks() { - List<RollbackInfo> recentlyExecutedRollbacks = new ArrayList<>(); - if (mRecentlyExecutedRollbacksFile.exists()) { - try { - // TODO: How to cope with changes to the format of this file from - // when RollbackStore is updated in the future? - String jsonString = IoUtils.readFileAsString( - mRecentlyExecutedRollbacksFile.getAbsolutePath()); - JSONObject object = new JSONObject(jsonString); - JSONArray array = object.getJSONArray("recentlyExecuted"); - for (int i = 0; i < array.length(); ++i) { - recentlyExecutedRollbacks.add(rollbackInfoFromJson(array.getJSONObject(i))); - } - } catch (IOException | JSONException e) { - // TODO: What to do here? Surely we shouldn't just forget about - // everything after the point of exception? - Log.e(TAG, "Failed to read recently executed rollbacks", e); - } - } - - return recentlyExecutedRollbacks; - } - - /** * Creates a new RollbackData instance for a non-staged rollback with * backupDir assigned. */ RollbackData createNonStagedRollback(int rollbackId) throws IOException { - File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId)); + File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId)); return new RollbackData(rollbackId, backupDir, -1); } @@ -243,7 +202,7 @@ class RollbackStore { */ RollbackData createStagedRollback(int rollbackId, int stagedSessionId) throws IOException { - File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId)); + File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId)); return new RollbackData(rollbackId, backupDir, stagedSessionId); } @@ -277,6 +236,17 @@ class RollbackStore { } /** + * Deletes all backed up apks and apex files associated with the given + * rollback. + */ + static void deletePackageCodePaths(RollbackData data) { + for (PackageRollbackInfo info : data.info.getPackages()) { + File targetDir = new File(data.backupDir, info.getPackageName()); + removeFile(targetDir); + } + } + + /** * Saves the rollback data to persistent storage. */ void saveRollbackData(RollbackData data) throws IOException { @@ -285,7 +255,7 @@ class RollbackStore { dataJson.put("info", rollbackInfoToJson(data.info)); dataJson.put("timestamp", data.timestamp.toString()); dataJson.put("stagedSessionId", data.stagedSessionId); - dataJson.put("isAvailable", data.isAvailable); + dataJson.put("state", rollbackStateToString(data.state)); dataJson.put("apkSessionId", data.apkSessionId); dataJson.put("restoreUserDataInProgress", data.restoreUserDataInProgress); @@ -305,29 +275,6 @@ class RollbackStore { } /** - * Writes the list of recently executed rollbacks to storage. - */ - void saveRecentlyExecutedRollbacks(List<RollbackInfo> recentlyExecutedRollbacks) { - try { - JSONObject json = new JSONObject(); - JSONArray array = new JSONArray(); - json.put("recentlyExecuted", array); - - for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) { - RollbackInfo rollback = recentlyExecutedRollbacks.get(i); - array.put(rollbackInfoToJson(rollback)); - } - - PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile); - pw.println(json.toString()); - pw.close(); - } catch (IOException | JSONException e) { - // TODO: What to do here? - Log.e(TAG, "Failed to save recently executed rollbacks", e); - } - } - - /** * Reads the metadata for a rollback from the given directory. * @throws IOException in case of error reading the data. */ @@ -342,10 +289,10 @@ class RollbackStore { backupDir, Instant.parse(dataJson.getString("timestamp")), dataJson.getInt("stagedSessionId"), - dataJson.getBoolean("isAvailable"), + rollbackStateFromString(dataJson.getString("state")), dataJson.getInt("apkSessionId"), dataJson.getBoolean("restoreUserDataInProgress")); - } catch (JSONException | DateTimeParseException e) { + } catch (JSONException | DateTimeParseException | ParseException e) { throw new IOException(e); } } @@ -444,7 +391,7 @@ class RollbackStore { * If the file is a directory, its contents are deleted as well. * Has no effect if the directory does not exist. */ - private void removeFile(File file) { + private static void removeFile(File file) { if (file.isDirectory()) { for (File child : file.listFiles()) { removeFile(child); @@ -454,4 +401,23 @@ class RollbackStore { file.delete(); } } + + private static String rollbackStateToString(@RollbackData.RollbackState int state) { + switch (state) { + case RollbackData.ROLLBACK_STATE_ENABLING: return "enabling"; + case RollbackData.ROLLBACK_STATE_AVAILABLE: return "available"; + case RollbackData.ROLLBACK_STATE_COMMITTED: return "committed"; + } + throw new AssertionError("Invalid rollback state: " + state); + } + + private static @RollbackData.RollbackState int rollbackStateFromString(String state) + throws ParseException { + switch (state) { + case "enabling": return RollbackData.ROLLBACK_STATE_ENABLING; + case "available": return RollbackData.ROLLBACK_STATE_AVAILABLE; + case "committed": return RollbackData.ROLLBACK_STATE_COMMITTED; + } + throw new ParseException("Invalid rollback state: " + state, 0); + } } diff --git a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java index fc7ccc576abb..798605568138 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.when; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.content.rollback.PackageRollbackInfo.RestoreInfo; -import android.content.rollback.RollbackInfo; import android.util.IntArray; import android.util.SparseLongArray; @@ -46,8 +45,7 @@ import org.mockito.Mockito; import java.io.File; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.Set; @RunWith(JUnit4.class) public class AppDataRollbackHelperTest { @@ -247,13 +245,13 @@ public class AppDataRollbackHelperTest { -1); dataForDifferentUser.info.getPackages().add(ignoredInfo); - RollbackInfo rollbackInfo = new RollbackInfo(17239, - Arrays.asList(pendingRestore, wasRecentlyRestored), false, - new ArrayList<>(), -1); + RollbackData dataForRestore = new RollbackData(17239, new File("/does/not/exist"), -1); + dataForRestore.info.getPackages().add(pendingRestore); + dataForRestore.info.getPackages().add(wasRecentlyRestored); - List<RollbackData> changed = helper.commitPendingBackupAndRestoreForUser(37, - Arrays.asList(dataWithPendingBackup, dataWithRecentRestore, dataForDifferentUser), - Collections.singletonList(rollbackInfo)); + Set<RollbackData> changed = helper.commitPendingBackupAndRestoreForUser(37, + Arrays.asList(dataWithPendingBackup, dataWithRecentRestore, dataForDifferentUser, + dataForRestore)); InOrder inOrder = Mockito.inOrder(installer); // Check that pending backup and restore for the same package mutually destroyed each other. @@ -267,9 +265,10 @@ public class AppDataRollbackHelperTest { assertEquals(53, pendingBackup.getCeSnapshotInodes().get(37)); // Check that changed returns correct RollbackData. - assertEquals(2, changed.size()); - assertEquals(dataWithPendingBackup, changed.get(0)); - assertEquals(dataWithRecentRestore, changed.get(1)); + assertEquals(3, changed.size()); + assertTrue(changed.contains(dataWithPendingBackup)); + assertTrue(changed.contains(dataWithRecentRestore)); + assertTrue(changed.contains(dataForRestore)); // Check that restore was performed. inOrder.verify(installer).restoreAppDataSnapshot( diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index 7be83edd91b9..a6054e8f41d0 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -19,19 +19,17 @@ package com.android.tests.rollback; import static com.android.tests.rollback.RollbackTestUtils.assertPackageRollbackInfoEquals; import static com.android.tests.rollback.RollbackTestUtils.assertRollbackInfoEquals; import static com.android.tests.rollback.RollbackTestUtils.getUniqueRollbackInfoForPackage; +import static com.android.tests.rollback.RollbackTestUtils.processUserData; import android.Manifest; import android.app.ActivityManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.VersionedPackage; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; -import android.os.Handler; -import android.os.HandlerThread; import android.provider.DeviceConfig; import android.support.test.InstrumentationRegistry; import android.util.Log; @@ -47,7 +45,6 @@ import org.junit.runners.JUnit4; import java.util.Collections; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; @@ -427,52 +424,6 @@ public class RollbackTest { } } - private static final String NO_RESPONSE = "NO RESPONSE"; - - // Calls into the test app to process user data. - // Asserts if the user data could not be processed or was version - // incompatible with the previously processed user data. - private void processUserData(String packageName) throws Exception { - Intent intent = new Intent(); - intent.setComponent(new ComponentName(packageName, - "com.android.tests.rollback.testapp.ProcessUserData")); - Context context = InstrumentationRegistry.getContext(); - - HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); - handlerThread.start(); - - // It can sometimes take a while after rollback before the app will - // receive this broadcast, so try a few times in a loop. - String result = NO_RESPONSE; - for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) { - BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>(); - context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (getResultCode() == 1) { - resultQueue.add("OK"); - } else { - // If the test app doesn't receive the broadcast or - // fails to set the result data, then getResultData - // here returns the initial NO_RESPONSE data passed to - // the sendOrderedBroadcast call. - resultQueue.add(getResultData()); - } - } - }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null); - - result = resultQueue.poll(10, TimeUnit.SECONDS); - if (result == null) { - result = "ProcessUserData broadcast timed out"; - } - } - - handlerThread.quit(); - if (!"OK".equals(result)) { - fail(result); - } - } - /** * Test that app user data is rolled back. */ diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java index f28714c87125..2f989f316ea6 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java @@ -17,6 +17,7 @@ package com.android.tests.rollback; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -27,12 +28,15 @@ import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; +import android.os.Handler; +import android.os.HandlerThread; import android.support.test.InstrumentationRegistry; import android.util.Log; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import java.io.IOException; import java.io.InputStream; @@ -393,4 +397,53 @@ class RollbackTestUtils { throw new AssertionError(e); } } + + private static final String NO_RESPONSE = "NO RESPONSE"; + + /** + * Calls into the test app to process user data. + * Asserts if the user data could not be processed or was version + * incompatible with the previously processed user data. + */ + static void processUserData(String packageName) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(packageName, + "com.android.tests.rollback.testapp.ProcessUserData")); + Context context = InstrumentationRegistry.getContext(); + + HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); + handlerThread.start(); + + // It can sometimes take a while after rollback before the app will + // receive this broadcast, so try a few times in a loop. + String result = NO_RESPONSE; + for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) { + BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>(); + context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getResultCode() == 1) { + resultQueue.add("OK"); + } else { + // If the test app doesn't receive the broadcast or + // fails to set the result data, then getResultData + // here returns the initial NO_RESPONSE data passed to + // the sendOrderedBroadcast call. + resultQueue.add(getResultData()); + } + } + }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null); + + try { + result = resultQueue.take(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + handlerThread.quit(); + if (!"OK".equals(result)) { + fail(result); + } + } } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index b65917b2ae5b..59ae8d9deaeb 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -84,6 +84,7 @@ public class StagedRollbackTest { RollbackTestUtils.install("RollbackTestAppAv1.apk", false); assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); RollbackTestUtils.installStaged(true, "RollbackTestAppAv2.apk"); @@ -98,6 +99,7 @@ public class StagedRollbackTest { @Test public void testApkOnlyCommitRollback() throws Exception { assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); RollbackManager rm = RollbackTestUtils.getRollbackManager(); RollbackInfo rollback = getUniqueRollbackInfoForPackage( @@ -129,6 +131,7 @@ public class StagedRollbackTest { @Test public void testApkOnlyConfirmRollback() throws Exception { assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); RollbackManager rm = RollbackTestUtils.getRollbackManager(); RollbackInfo rollback = getUniqueRollbackInfoForPackage( |