Add RollbackManagerService
This change adds RollbackManagerService as a new system service for
managing apk level rollbacks.
To work properly this requires additional selinux policy changes. Fails
gracefully in case of selinux denials, until we have a chance to sort
out the proper selinux policy.
Bug: 112431924
Bug: 116512606
Test: atest RollbackTest, with selinux enforcement off.
Test: atest CtsPermission2TestCases:PermissionPolicyTest
Change-Id: Id72aae9c4d8da9aaab3922ec9233ba335bc0198f
diff --git a/Android.bp b/Android.bp
index 4e7a7b4..340f3b1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -151,6 +151,7 @@
"core/java/android/content/pm/dex/IArtManager.aidl",
"core/java/android/content/pm/dex/ISnapshotRuntimeProfileCallback.aidl",
"core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl",
+ "core/java/android/content/rollback/IRollbackManager.aidl",
"core/java/android/database/IContentObserver.aidl",
"core/java/android/debug/IAdbManager.aidl",
"core/java/android/debug/IAdbTransport.aidl",
diff --git a/api/test-current.txt b/api/test-current.txt
index 71a06f1..c1e4162 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -329,6 +329,11 @@
method public android.os.UserHandle getUser();
method public int getUserId();
method public void setAutofillCompatibilityEnabled(boolean);
+ field public static final java.lang.String ROLLBACK_SERVICE = "rollback";
+ }
+
+ public class Intent implements java.lang.Cloneable android.os.Parcelable {
+ field public static final java.lang.String ACTION_PACKAGE_ROLLBACK_EXECUTED = "android.intent.action.PACKAGE_ROLLBACK_EXECUTED";
}
}
@@ -351,6 +356,10 @@
ctor public LauncherApps(android.content.Context);
}
+ public static class PackageInstaller.SessionParams implements android.os.Parcelable {
+ method public void setEnableRollback();
+ }
+
public abstract class PackageManager {
method public abstract boolean arePermissionsIndividuallyControlled();
method public abstract java.lang.String getDefaultBrowserPackageNameAsUser(int);
@@ -398,6 +407,42 @@
}
+package android.content.rollback {
+
+ public final class PackageRollbackInfo implements android.os.Parcelable {
+ ctor public PackageRollbackInfo(java.lang.String, android.content.rollback.PackageRollbackInfo.PackageVersion, android.content.rollback.PackageRollbackInfo.PackageVersion);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.rollback.PackageRollbackInfo> CREATOR;
+ field public final android.content.rollback.PackageRollbackInfo.PackageVersion higherVersion;
+ field public final android.content.rollback.PackageRollbackInfo.PackageVersion lowerVersion;
+ field public final java.lang.String packageName;
+ }
+
+ public static class PackageRollbackInfo.PackageVersion {
+ ctor public PackageRollbackInfo.PackageVersion(long);
+ field public final long versionCode;
+ }
+
+ public final class RollbackInfo implements android.os.Parcelable {
+ ctor public RollbackInfo(android.content.rollback.PackageRollbackInfo);
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.rollback.RollbackInfo> CREATOR;
+ field public final android.content.rollback.PackageRollbackInfo targetPackage;
+ }
+
+ public final class RollbackManager {
+ method public void executeRollback(android.content.rollback.RollbackInfo, android.content.IntentSender);
+ method public void expireRollbackForPackage(java.lang.String);
+ method public android.content.rollback.RollbackInfo getAvailableRollback(java.lang.String);
+ method public java.util.List<java.lang.String> getPackagesWithAvailableRollbacks();
+ method public java.util.List<android.content.rollback.RollbackInfo> getRecentlyExecutedRollbacks();
+ method public void reloadPersistedData();
+ }
+
+}
+
package android.database.sqlite {
public class SQLiteCompatibilityWalFlags {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 45e87e0..75da1646 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -49,6 +49,8 @@
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
import android.content.res.Resources;
+import android.content.rollback.IRollbackManager;
+import android.content.rollback.RollbackManager;
import android.debug.AdbManager;
import android.debug.IAdbManager;
import android.hardware.ConsumerIrManager;
@@ -1161,6 +1163,16 @@
throws ServiceNotFoundException {
return new RoleManager(ctx.getOuterContext());
}});
+
+ registerService(Context.ROLLBACK_SERVICE, RollbackManager.class,
+ new CachedServiceFetcher<RollbackManager>() {
+ @Override
+ public RollbackManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(Context.ROLLBACK_SERVICE);
+ return new RollbackManager(ctx.getOuterContext(),
+ IRollbackManager.Stub.asInterface(b));
+ }});
//CHECKSTYLE:ON IndentationCheck
}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6f12cad..6d9facb 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3122,6 +3122,7 @@
APPWIDGET_SERVICE,
//@hide: VOICE_INTERACTION_MANAGER_SERVICE,
//@hide: BACKUP_SERVICE,
+ ROLLBACK_SERVICE,
DROPBOX_SERVICE,
//@hide: DEVICE_IDLE_CONTROLLER,
DEVICE_POLICY_SERVICE,
@@ -3993,6 +3994,17 @@
public static final String BACKUP_SERVICE = "backup";
/**
+ * Use with {@link #getSystemService(String)} to retrieve an
+ * {@link android.content.rollback.RollbackManager} for communicating
+ * with the rollback manager
+ *
+ * @see #getSystemService(String)
+ * @hide TODO(ruhler): hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+ @TestApi
+ public static final String ROLLBACK_SERVICE = "rollback";
+
+ /**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.DropBoxManager} instance for recording
* diagnostic logs.
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index d5c6c63..1e3908c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -27,6 +27,7 @@
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;
@@ -2248,6 +2249,32 @@
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_PACKAGE_CHANGED = "android.intent.action.PACKAGE_CHANGED";
/**
+ * Broadcast Action: Sent to the system rollback manager when a package
+ * needs to have rollback enabled.
+ * <p class="note">
+ * This is a protected intent that can only be sent by the system.
+ * </p>
+ *
+ * @hide This broadcast is used internally by the system.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_ENABLE_ROLLBACK =
+ "android.intent.action.PACKAGE_ENABLE_ROLLBACK";
+ /**
+ * Broadcast Action: An existing version of an application package has been
+ * rolled back to a previous version.
+ * The data contains the name of the package.
+ *
+ * <p class="note">This is a protected intent that can only be sent
+ * by the system.
+ *
+ * @hide TODO: hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+ @TestApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_ROLLBACK_EXECUTED =
+ "android.intent.action.PACKAGE_ROLLBACK_EXECUTED";
+ /**
* @hide
* Broadcast Action: Ask system services if there is any reason to
* restart the given package. The data contains the name of the
@@ -10343,6 +10370,7 @@
case ACTION_MEDIA_SCANNER_SCAN_FILE:
case ACTION_PACKAGE_NEEDS_VERIFICATION:
case ACTION_PACKAGE_VERIFIED:
+ case ACTION_PACKAGE_ENABLE_ROLLBACK:
// Ignore legacy actions
break;
default:
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index f06df3d..a2fd83f 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -24,6 +24,7 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityManager;
import android.app.AppGlobals;
@@ -1425,6 +1426,15 @@
this.grantedRuntimePermissions = permissions;
}
+ /**
+ * Request that rollbacks be enabled for the given upgrade.
+ * @hide TODO: hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+ @TestApi
+ public void setEnableRollback() {
+ installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK;
+ }
+
/** {@hide} */
@SystemApi
public void setAllowDowngrade(boolean allowDowngrade) {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 9d604bb..2608796 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -720,6 +720,9 @@
INSTALL_FORCE_SDK,
INSTALL_FULL_APP,
INSTALL_ALLOCATE_AGGRESSIVE,
+ INSTALL_VIRTUAL_PRELOAD,
+ INSTALL_APEX,
+ INSTALL_ENABLE_ROLLBACK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface InstallFlags {}
@@ -857,6 +860,14 @@
*/
public static final int INSTALL_APEX = 0x00020000;
+ /**
+ * Flag parameter for {@link #installPackage} to indicate that rollback
+ * should be enabled for this install.
+ *
+ * @hide
+ */
+ public static final int INSTALL_ENABLE_ROLLBACK = 0x00040000;
+
/** @hide */
@IntDef(flag = true, prefix = { "DONT_KILL_APP" }, value = {
DONT_KILL_APP
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index 5db9f50..83979e9 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -759,4 +759,48 @@
/** Returns whether the given package is enabled for the given user */
public abstract @PackageManager.EnabledState int getApplicationEnabledState(
String packageName, int userId);
+
+ /**
+ * Extra field name for the token of a request to enable rollback for a
+ * package.
+ */
+ public static final String EXTRA_ENABLE_ROLLBACK_TOKEN =
+ "android.content.pm.extra.ENABLE_ROLLBACK_TOKEN";
+
+ /**
+ * Extra field name for the installFlags of a request to enable rollback
+ * for a package.
+ */
+ public static final String EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS =
+ "android.content.pm.extra.ENABLE_ROLLBACK_INSTALL_FLAGS";
+
+ /**
+ * Used as the {@code enableRollbackCode} argument for
+ * {@link PackageManagerInternal#setEnableRollbackCode} to indicate that
+ * enabling rollback succeeded.
+ */
+ public static final int ENABLE_ROLLBACK_SUCCEEDED = 1;
+
+ /**
+ * Used as the {@code enableRollbackCode} argument for
+ * {@link PackageManagerInternal#setEnableRollbackCode} to indicate that
+ * enabling rollback failed.
+ */
+ public static final int ENABLE_ROLLBACK_FAILED = -1;
+
+ /**
+ * Allows the rollback manager listening to the
+ * {@link Intent#ACTION_PACKAGE_ENABLE_ROLLBACK enable rollback broadcast}
+ * to respond to the package manager. The response must include the
+ * {@code enableRollbackCode} which is one of
+ * {@link PackageManager#ENABLE_ROLLBACK_SUCCEEDED} or
+ * {@link PackageManager#ENABLE_ROLLBACK_FAILED}.
+ *
+ * @param token pending package identifier as passed via the
+ * {@link PackageManager#EXTRA_ENABLE_ROLLBACK_TOKEN} Intent extra.
+ * @param enableRollbackCode the status code result of enabling rollback
+ * @throws SecurityException if the caller does not have the
+ * PACKAGE_ROLLBACK_AGENT permission.
+ */
+ public abstract void setEnableRollbackCode(int token, int enableRollbackCode);
}
diff --git a/core/java/android/content/rollback/IRollbackManager.aidl b/core/java/android/content/rollback/IRollbackManager.aidl
new file mode 100644
index 0000000..7f557cd
--- /dev/null
+++ b/core/java/android/content/rollback/IRollbackManager.aidl
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.rollback;
+
+import android.content.pm.ParceledListSlice;
+import android.content.pm.StringParceledListSlice;
+import android.content.rollback.RollbackInfo;
+import android.content.IntentSender;
+
+/** {@hide} */
+interface IRollbackManager {
+
+ RollbackInfo getAvailableRollback(String packageName);
+
+ StringParceledListSlice getPackagesWithAvailableRollbacks();
+
+ ParceledListSlice getRecentlyExecutedRollbacks();
+
+ void executeRollback(in RollbackInfo rollback, String callerPackageName,
+ in IntentSender statusReceiver);
+
+ // Exposed for test purposes only.
+ void reloadPersistedData();
+
+ // Exposed for test purposes only.
+ void expireRollbackForPackage(String packageName);
+}
diff --git a/core/java/android/content/rollback/PackageRollbackInfo.aidl b/core/java/android/content/rollback/PackageRollbackInfo.aidl
new file mode 100644
index 0000000..9cb52c9
--- /dev/null
+++ b/core/java/android/content/rollback/PackageRollbackInfo.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.rollback;
+
+parcelable PackageRollbackInfo;
diff --git a/core/java/android/content/rollback/PackageRollbackInfo.java b/core/java/android/content/rollback/PackageRollbackInfo.java
new file mode 100644
index 0000000..0c05765
--- /dev/null
+++ b/core/java/android/content/rollback/PackageRollbackInfo.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.rollback;
+
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Information about a rollback available for a particular package.
+ *
+ * @hide TODO: hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+@TestApi
+public final class PackageRollbackInfo implements Parcelable {
+ /**
+ * The name of a package being rolled back.
+ */
+ public final String packageName;
+
+ /**
+ * The version the package was rolled back from.
+ */
+ public final PackageVersion higherVersion;
+
+ /**
+ * The version the package was rolled back to.
+ */
+ public final PackageVersion lowerVersion;
+
+ /**
+ * Represents a version of a package.
+ */
+ public static class PackageVersion {
+ public final long versionCode;
+
+ // TODO(b/120200473): Include apk sha or some other way to distinguish
+ // between two different apks with the same version code.
+ public PackageVersion(long versionCode) {
+ this.versionCode = versionCode;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof PackageVersion) {
+ PackageVersion otherVersion = (PackageVersion) other;
+ return versionCode == otherVersion.versionCode;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(versionCode);
+ }
+ }
+
+ public PackageRollbackInfo(String packageName,
+ PackageVersion higherVersion, PackageVersion lowerVersion) {
+ this.packageName = packageName;
+ this.higherVersion = higherVersion;
+ this.lowerVersion = lowerVersion;
+ }
+
+ private PackageRollbackInfo(Parcel in) {
+ this.packageName = in.readString();
+ this.higherVersion = new PackageVersion(in.readLong());
+ this.lowerVersion = new PackageVersion(in.readLong());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(packageName);
+ out.writeLong(higherVersion.versionCode);
+ out.writeLong(lowerVersion.versionCode);
+ }
+
+ public static final Parcelable.Creator<PackageRollbackInfo> CREATOR =
+ new Parcelable.Creator<PackageRollbackInfo>() {
+ public PackageRollbackInfo createFromParcel(Parcel in) {
+ return new PackageRollbackInfo(in);
+ }
+
+ public PackageRollbackInfo[] newArray(int size) {
+ return new PackageRollbackInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/content/rollback/RollbackInfo.aidl b/core/java/android/content/rollback/RollbackInfo.aidl
new file mode 100644
index 0000000..a9dc5cd
--- /dev/null
+++ b/core/java/android/content/rollback/RollbackInfo.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.rollback;
+
+parcelable RollbackInfo;
diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java
new file mode 100644
index 0000000..5fa4e57
--- /dev/null
+++ b/core/java/android/content/rollback/RollbackInfo.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.rollback;
+
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information about a set of packages that can be, or already have been
+ * rolled back together.
+ *
+ * @hide TODO: hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+@TestApi
+public final class RollbackInfo implements Parcelable {
+
+ /**
+ * The package that needs to be rolled back.
+ */
+ public final PackageRollbackInfo targetPackage;
+
+ // TODO: Add a list of additional packages rolled back due to atomic
+ // install dependencies when rollback of atomic installs is supported.
+ // TODO: Add a flag to indicate if reboot is required, when rollback of
+ // staged installs is supported.
+
+ public RollbackInfo(PackageRollbackInfo targetPackage) {
+ this.targetPackage = targetPackage;
+ }
+
+ private RollbackInfo(Parcel in) {
+ this.targetPackage = PackageRollbackInfo.CREATOR.createFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ targetPackage.writeToParcel(out, flags);
+ }
+
+ public static final Parcelable.Creator<RollbackInfo> CREATOR =
+ new Parcelable.Creator<RollbackInfo>() {
+ public RollbackInfo createFromParcel(Parcel in) {
+ return new RollbackInfo(in);
+ }
+
+ public RollbackInfo[] newArray(int size) {
+ return new RollbackInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/content/rollback/RollbackManager.java b/core/java/android/content/rollback/RollbackManager.java
new file mode 100644
index 0000000..294151a
--- /dev/null
+++ b/core/java/android/content/rollback/RollbackManager.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.rollback;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.IntentSender;
+import android.os.RemoteException;
+
+import java.util.List;
+
+/**
+ * Offers the ability to rollback packages after upgrade.
+ * <p>
+ * For packages installed with rollbacks enabled, the RollbackManager can be
+ * used to initiate rollback of those packages for a limited time period after
+ * upgrade.
+ *
+ * TODO: Require an appropriate permission for apps to use these APIs.
+ *
+ * @see PackageInstaller.SessionParams#setEnableRollback()
+ * @hide TODO: hidden, @TestApi until we decide on public vs. @SystemApi.
+ */
+@TestApi
+@SystemService(Context.ROLLBACK_SERVICE)
+public final class RollbackManager {
+ private final String mCallerPackageName;
+ private final IRollbackManager mBinder;
+
+ /** {@hide} */
+ public RollbackManager(Context context, IRollbackManager binder) {
+ mCallerPackageName = context.getPackageName();
+ mBinder = binder;
+ }
+
+ /**
+ * Returns the rollback currently available to be executed for the given
+ * package.
+ * <p>
+ * The returned RollbackInfo describes what packages would be rolled back,
+ * including package version codes before and after rollback. The rollback
+ * can be initiated using {@link #executeRollback(RollbackInfo,IntentSender)}.
+ * <p>
+ * TODO: What if there is no package installed on device for packageName?
+ *
+ * @param packageName name of the package to get the availble RollbackInfo for.
+ * @return the rollback available for the package, or null if no rollback
+ * is available for the package.
+ */
+ public @Nullable RollbackInfo getAvailableRollback(@NonNull String packageName) {
+ try {
+ return mBinder.getAvailableRollback(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the names of packages that are available for rollback.
+ * Call {@link #getAvailableRollback(String)} to get more information
+ * about the rollback available for a particular package.
+ *
+ * @return the names of packages that are available for rollback.
+ */
+ public @NonNull List<String> getPackagesWithAvailableRollbacks() {
+ try {
+ return mBinder.getPackagesWithAvailableRollbacks().getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+
+ /**
+ * Gets the list of all recently executed rollbacks.
+ * This is for the purposes of preventing re-install of a bad version of a
+ * package.
+ * <p>
+ * Returns an empty list if there are no recently executed rollbacks.
+ * <p>
+ * To avoid having to keep around complete rollback history forever on a
+ * device, the returned list of rollbacks is only guaranteed to include
+ * rollbacks that are still relevant. A rollback is no longer considered
+ * relevant if the package is subsequently uninstalled or upgraded
+ * (without the possibility of rollback) to a higher version code than was
+ * rolled back from.
+ *
+ * @return the recently executed rollbacks
+ */
+ public @NonNull List<RollbackInfo> getRecentlyExecutedRollbacks() {
+ try {
+ return mBinder.getRecentlyExecutedRollbacks().getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Execute the given rollback, rolling back all versions of the packages
+ * to the last good versions previously installed on the device as
+ * specified in the given rollback object. The rollback will fail if any
+ * of the installed packages or available rollbacks are inconsistent with
+ * the versions specified in the given rollback object, which can happen
+ * if a package has been updated or a rollback expired since the rollback
+ * object was retrieved from {@link #getAvailableRollback(String)}.
+ * <p>
+ * TODO: Specify the returns status codes.
+ * TODO: What happens in case reboot is required for the rollback to take
+ * effect for staged installs?
+ *
+ * @param rollback to execute
+ * @param statusReceiver where to deliver the results
+ */
+ public void executeRollback(@NonNull RollbackInfo rollback,
+ @NonNull IntentSender statusReceiver) {
+ try {
+ mBinder.executeRollback(rollback, mCallerPackageName, statusReceiver);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Reload all persisted rollback data from device storage.
+ * This API is meant to test that rollback state is properly preserved
+ * across device reboot, by simulating what happens on reboot without
+ * actually rebooting the device.
+ *
+ * @hide
+ */
+ @TestApi
+ public void reloadPersistedData() {
+ try {
+ mBinder.reloadPersistedData();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Expire the rollback data for a given package.
+ * This API is meant to facilitate testing of rollback logic for
+ * expiring rollback data.
+ *
+ * @param packageName the name of the package to expire data for.
+ *
+ * @hide
+ */
+ @TestApi
+ public void expireRollbackForPackage(@NonNull String packageName) {
+ try {
+ mBinder.expireRollbackForPackage(packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c6343a8..b16c16d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -42,6 +42,8 @@
<protected-broadcast android:name="android.intent.action.PACKAGE_REMOVED" />
<protected-broadcast android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<protected-broadcast android:name="android.intent.action.PACKAGE_CHANGED" />
+ <protected-broadcast android:name="android.intent.action.PACKAGE_ENABLE_ROLLBACK" />
+ <protected-broadcast android:name="android.intent.action.PACKAGE_ROLLBACK_EXECUTED" />
<protected-broadcast android:name="android.intent.action.PACKAGE_RESTARTED" />
<protected-broadcast android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
<protected-broadcast android:name="android.intent.action.PACKAGE_FIRST_LAUNCH" />
@@ -3846,6 +3848,12 @@
<permission android:name="android.permission.BIND_PACKAGE_VERIFIER"
android:protectionLevel="signature" />
+ <!-- @hide Rollback manager needs to have this permission before the PackageManager will
+ trust it to enable rollback.
+ -->
+ <permission android:name="android.permission.PACKAGE_ROLLBACK_AGENT"
+ android:protectionLevel="signature" />
+
<!-- @SystemApi @hide Allows an application to mark other applications as harmful -->
<permission android:name="android.permission.SET_HARMFUL_APP_WARNINGS"
android:protectionLevel="signature|verifier" />
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 35ffe8d..f320afe 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -699,9 +699,11 @@
switch (Binder.getCallingUid()) {
case android.os.Process.SHELL_UID:
case android.os.Process.ROOT_UID:
+ case android.os.Process.SYSTEM_UID:
break;
default:
- throw new SecurityException("Reverse mode only supported from shell");
+ throw new SecurityException(
+ "Reverse mode only supported from shell or system");
}
// In "reverse" mode, we're streaming data ourselves from the
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 0bde80a..178725d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -545,6 +545,12 @@
private static final long DEFAULT_VERIFICATION_TIMEOUT = 10 * 1000;
/**
+ * The default duration to wait for rollback to be enabled in
+ * milliseconds.
+ */
+ private static final long DEFAULT_ENABLE_ROLLBACK_TIMEOUT = 10 * 1000;
+
+ /**
* The default response for package verification timeout.
*
* This can be either PackageManager.VERIFICATION_ALLOW or
@@ -864,6 +870,9 @@
/** List of packages waiting for verification. */
final SparseArray<PackageVerificationState> mPendingVerification = new SparseArray<>();
+ /** List of packages waiting for rollback to be enabled. */
+ final SparseArray<InstallParams> mPendingEnableRollback = new SparseArray<>();
+
final PackageInstallerService mInstallerService;
final ArtManagerService mArtManagerService;
@@ -884,6 +893,9 @@
/** Token for keys in mPendingVerification. */
private int mPendingVerificationToken = 0;
+ /** Token for keys in mPendingEnableRollback. */
+ private int mPendingEnableRollbackToken = 0;
+
volatile boolean mSystemReady;
volatile boolean mSafeMode;
volatile boolean mHasSystemUidErrors;
@@ -1255,6 +1267,8 @@
static final int INTENT_FILTER_VERIFIED = 18;
static final int WRITE_PACKAGE_LIST = 19;
static final int INSTANT_APP_RESOLUTION_PHASE_TWO = 20;
+ static final int ENABLE_ROLLBACK_STATUS = 21;
+ static final int ENABLE_ROLLBACK_TIMEOUT = 22;
static final int WRITE_SETTINGS_DELAY = 10*1000; // 10 seconds
@@ -1478,14 +1492,13 @@
final PackageVerificationState state = mPendingVerification.get(verificationId);
if ((state != null) && !state.timeoutExtended()) {
- final InstallArgs args = state.getInstallArgs();
+ final InstallParams params = state.getInstallParams();
+ final InstallArgs args = params.mArgs;
final Uri originUri = Uri.fromFile(args.origin.resolvedFile);
Slog.i(TAG, "Verification timed out for " + originUri);
mPendingVerification.remove(verificationId);
- int ret = PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE;
-
final UserHandle user = args.getUser();
if (getDefaultVerificationResponse(user)
== PackageManager.VERIFICATION_ALLOW) {
@@ -1494,16 +1507,16 @@
PackageManager.VERIFICATION_ALLOW_WITHOUT_SUFFICIENT);
broadcastPackageVerified(verificationId, originUri,
PackageManager.VERIFICATION_ALLOW, user);
- ret = args.copyApk();
} else {
broadcastPackageVerified(verificationId, originUri,
PackageManager.VERIFICATION_REJECT, user);
+ params.setReturnCode(
+ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE);
}
Trace.asyncTraceEnd(
TRACE_TAG_PACKAGE_MANAGER, "verification", verificationId);
-
- processPendingInstall(args, ret);
+ params.handleVerificationFinished();
}
break;
}
@@ -1523,22 +1536,22 @@
if (state.isVerificationComplete()) {
mPendingVerification.remove(verificationId);
- final InstallArgs args = state.getInstallArgs();
+ final InstallParams params = state.getInstallParams();
+ final InstallArgs args = params.mArgs;
final Uri originUri = Uri.fromFile(args.origin.resolvedFile);
- int ret;
if (state.isInstallAllowed()) {
broadcastPackageVerified(verificationId, originUri,
- response.code, state.getInstallArgs().getUser());
- ret = args.copyApk();
+ response.code, args.getUser());
} else {
- ret = PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE;
+ params.setReturnCode(
+ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE);
}
Trace.asyncTraceEnd(
TRACE_TAG_PACKAGE_MANAGER, "verification", verificationId);
- processPendingInstall(args, ret);
+ params.handleVerificationFinished();
}
break;
@@ -1598,6 +1611,49 @@
(InstantAppRequest) msg.obj,
mInstantAppInstallerActivity,
mHandler);
+ break;
+ }
+ case ENABLE_ROLLBACK_STATUS: {
+ final int enableRollbackToken = msg.arg1;
+ final int enableRollbackCode = msg.arg2;
+ InstallParams params = mPendingEnableRollback.get(enableRollbackToken);
+ if (params == null) {
+ Slog.w(TAG, "Invalid rollback enabled token "
+ + enableRollbackToken + " received");
+ break;
+ }
+
+ mPendingEnableRollback.remove(enableRollbackToken);
+
+ if (enableRollbackCode != PackageManagerInternal.ENABLE_ROLLBACK_SUCCEEDED) {
+ final InstallArgs args = params.mArgs;
+ final Uri originUri = Uri.fromFile(args.origin.resolvedFile);
+ Slog.w(TAG, "Failed to enable rollback for " + originUri);
+ Slog.w(TAG, "Continuing with installation of " + originUri);
+ }
+
+ Trace.asyncTraceEnd(
+ TRACE_TAG_PACKAGE_MANAGER, "enable_rollback", enableRollbackToken);
+
+ params.handleRollbackEnabled();
+ break;
+ }
+ case ENABLE_ROLLBACK_TIMEOUT: {
+ final int enableRollbackToken = msg.arg1;
+ final InstallParams params = mPendingEnableRollback.get(enableRollbackToken);
+ if (params != null) {
+ final InstallArgs args = params.mArgs;
+ final Uri originUri = Uri.fromFile(args.origin.resolvedFile);
+
+ Slog.w(TAG, "Enable rollback timed out for " + originUri);
+ mPendingEnableRollback.remove(enableRollbackToken);
+
+ Slog.w(TAG, "Continuing with installation of " + originUri);
+ Trace.asyncTraceEnd(
+ TRACE_TAG_PACKAGE_MANAGER, "enable_rollback", enableRollbackToken);
+ params.handleRollbackEnabled();
+ }
+ break;
}
}
}
@@ -13220,6 +13276,13 @@
}
}
+ private void setEnableRollbackCode(int token, int enableRollbackCode) {
+ final Message msg = mHandler.obtainMessage(ENABLE_ROLLBACK_STATUS);
+ msg.arg1 = token;
+ msg.arg2 = enableRollbackCode;
+ mHandler.sendMessage(msg);
+ }
+
@Override
public void finishPackageInstall(int token, boolean didLaunch) {
enforceSystemOrRoot("Only the system is allowed to finish installs");
@@ -13871,7 +13934,7 @@
@NonNull
private final ArrayList<InstallParams> mChildParams;
@NonNull
- private final Map<InstallArgs, Integer> mVerifiedState;
+ private final Map<InstallArgs, Integer> mCurrentState;
MultiPackageInstallParams(
@NonNull UserHandle user,
@@ -13887,7 +13950,7 @@
childParams.mParentInstallParams = this;
this.mChildParams.add(childParams);
}
- this.mVerifiedState = new ArrayMap<>(mChildParams.size());
+ this.mCurrentState = new ArrayMap<>(mChildParams.size());
}
@Override
@@ -13913,12 +13976,12 @@
}
void tryProcessInstallRequest(InstallArgs args, int currentStatus) {
- mVerifiedState.put(args, currentStatus);
+ mCurrentState.put(args, currentStatus);
boolean success = true;
- if (mVerifiedState.size() != mChildParams.size()) {
+ if (mCurrentState.size() != mChildParams.size()) {
return;
}
- for (Integer status : mVerifiedState.values()) {
+ for (Integer status : mCurrentState.values()) {
if (status == PackageManager.INSTALL_UNKNOWN) {
return;
} else if (status != PackageManager.INSTALL_SUCCEEDED) {
@@ -13926,8 +13989,8 @@
break;
}
}
- final List<InstallRequest> installRequests = new ArrayList<>(mVerifiedState.size());
- for (Map.Entry<InstallArgs, Integer> entry : mVerifiedState.entrySet()) {
+ final List<InstallRequest> installRequests = new ArrayList<>(mCurrentState.size());
+ for (Map.Entry<InstallArgs, Integer> entry : mCurrentState.entrySet()) {
installRequests.add(new InstallRequest(entry.getKey(),
createPackageInstalledInfo(entry.getValue())));
}
@@ -13944,6 +14007,8 @@
int installFlags;
final String installerPackageName;
final String volumeUuid;
+ private boolean mVerificationCompleted;
+ private boolean mEnableRollbackCompleted;
private InstallArgs mArgs;
int mRet;
final String packageAbiOverride;
@@ -14193,6 +14258,8 @@
}
final InstallArgs args = createInstallArgs(this);
+ mVerificationCompleted = true;
+ mEnableRollbackCompleted = true;
mArgs = args;
if (ret == PackageManager.INSTALL_SUCCEEDED) {
@@ -14272,7 +14339,7 @@
}
final PackageVerificationState verificationState = new PackageVerificationState(
- requiredUid, args);
+ requiredUid, this);
mPendingVerification.append(verificationId, verificationState);
@@ -14334,25 +14401,80 @@
/*
* We don't want the copy to proceed until verification
- * succeeds, so null out this field.
+ * succeeds.
*/
- mArgs = null;
+ mVerificationCompleted = false;
}
- } else {
- /*
- * No package verification is enabled, so immediately start
- * the remote call to initiate copy using temporary file.
- */
- ret = args.copyApk();
+ }
+
+ if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
+ // TODO(ruhler) b/112431924: Don't do this in case of 'move'?
+ final int enableRollbackToken = mPendingEnableRollbackToken++;
+ Trace.asyncTraceBegin(
+ TRACE_TAG_PACKAGE_MANAGER, "enable_rollback", enableRollbackToken);
+ mPendingEnableRollback.append(enableRollbackToken, this);
+
+ // TODO(ruhler) b/112431924: What user? Test for multi-user.
+ Intent enableRollbackIntent = new Intent(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK);
+ enableRollbackIntent.putExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN,
+ enableRollbackToken);
+ enableRollbackIntent.putExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS,
+ installFlags);
+ enableRollbackIntent.setDataAndType(Uri.fromFile(new File(origin.resolvedPath)),
+ PACKAGE_MIME_TYPE);
+ enableRollbackIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ mContext.sendOrderedBroadcastAsUser(enableRollbackIntent, getUser(),
+ android.Manifest.permission.PACKAGE_ROLLBACK_AGENT,
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // TODO(ruhler) b/112431924 Have a configurable setting to
+ // allow changing the timeout and fall back to the default
+ // if no such specified.
+ final Message msg = mHandler.obtainMessage(
+ ENABLE_ROLLBACK_TIMEOUT);
+ msg.arg1 = enableRollbackToken;
+ mHandler.sendMessageDelayed(msg,
+ DEFAULT_ENABLE_ROLLBACK_TIMEOUT);
+ }
+ }, null, 0, null, null);
+
+ mEnableRollbackCompleted = false;
}
}
mRet = ret;
}
+ void setReturnCode(int ret) {
+ if (mRet == PackageManager.INSTALL_SUCCEEDED) {
+ // Only update mRet if it was previously INSTALL_SUCCEEDED to
+ // ensure we do not overwrite any previous failure results.
+ mRet = ret;
+ }
+ }
+
+ void handleVerificationFinished() {
+ mVerificationCompleted = true;
+ handleReturnCode();
+ }
+
+ void handleRollbackEnabled() {
+ // TODO(ruhler) b/112431924: Consider halting the install if we
+ // couldn't enable rollback.
+ mEnableRollbackCompleted = true;
+ handleReturnCode();
+ }
+
@Override
void handleReturnCode() {
- if (mArgs != null) {
+ if (mVerificationCompleted && mEnableRollbackCompleted) {
+ if (mRet == PackageManager.INSTALL_SUCCEEDED) {
+ mRet = mArgs.copyApk();
+ }
processPendingInstall(mArgs, mRet);
}
}
@@ -23450,6 +23572,11 @@
return setting.getEnabled(userId);
}
}
+
+ @Override
+ public void setEnableRollbackCode(int token, int enableRollbackCode) {
+ PackageManagerService.this.setEnableRollbackCode(token, enableRollbackCode);
+ }
}
@GuardedBy("mPackages")
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 357872e..ac9c6ef 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2300,6 +2300,9 @@
case "--staged":
sessionParams.setStaged();
break;
+ case "--enable-rollback":
+ sessionParams.installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK;
+ break;
default:
throw new IllegalArgumentException("Unknown option " + opt);
}
@@ -2833,6 +2836,7 @@
pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]");
pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]");
pw.println(" [--preload] [--instantapp] [--full] [--dont-kill]");
+ pw.println(" [--enable-rollback]");
pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES] [--apex]");
pw.println(" [PATH|-]");
pw.println(" Install an application. Must provide the apk data to install, either as a");
diff --git a/services/core/java/com/android/server/pm/PackageVerificationState.java b/services/core/java/com/android/server/pm/PackageVerificationState.java
index 3214e88..c50bf59 100644
--- a/services/core/java/com/android/server/pm/PackageVerificationState.java
+++ b/services/core/java/com/android/server/pm/PackageVerificationState.java
@@ -16,11 +16,11 @@
package com.android.server.pm;
-import com.android.server.pm.PackageManagerService.InstallArgs;
-
import android.content.pm.PackageManager;
import android.util.SparseBooleanArray;
+import com.android.server.pm.PackageManagerService.InstallParams;
+
/**
* Tracks the package verification state for a particular package. Each package
* verification has a required verifier and zero or more sufficient verifiers.
@@ -29,7 +29,7 @@
* then package verification is considered complete.
*/
class PackageVerificationState {
- private final InstallArgs mArgs;
+ private final InstallParams mParams;
private final SparseBooleanArray mSufficientVerifierUids;
@@ -53,15 +53,15 @@
* @param requiredVerifierUid user ID of required package verifier
* @param args
*/
- public PackageVerificationState(int requiredVerifierUid, InstallArgs args) {
+ PackageVerificationState(int requiredVerifierUid, InstallParams params) {
mRequiredVerifierUid = requiredVerifierUid;
- mArgs = args;
+ mParams = params;
mSufficientVerifierUids = new SparseBooleanArray();
mExtendedTimeout = false;
}
- public InstallArgs getInstallArgs() {
- return mArgs;
+ InstallParams getInstallParams() {
+ return mParams;
}
/**
@@ -69,7 +69,7 @@
*
* @param uid user ID of sufficient verifier
*/
- public void addSufficientVerifier(int uid) {
+ void addSufficientVerifier(int uid) {
mSufficientVerifierUids.put(uid, true);
}
@@ -80,7 +80,7 @@
* @param uid user ID of the verifying agent
* @return {@code true} if the verifying agent actually exists in our list
*/
- public boolean setVerifierResponse(int uid, int code) {
+ boolean setVerifierResponse(int uid, int code) {
if (uid == mRequiredVerifierUid) {
mRequiredVerificationComplete = true;
switch (code) {
@@ -120,7 +120,7 @@
*
* @return {@code true} when verification is considered complete
*/
- public boolean isVerificationComplete() {
+ boolean isVerificationComplete() {
if (!mRequiredVerificationComplete) {
return false;
}
@@ -138,7 +138,7 @@
*
* @return {@code true} if installation should be allowed
*/
- public boolean isInstallAllowed() {
+ boolean isInstallAllowed() {
if (!mRequiredVerificationPassed) {
return false;
}
@@ -153,7 +153,7 @@
/**
* Extend the timeout for this Package to be verified.
*/
- public void extendTimeout() {
+ void extendTimeout() {
if (!mExtendedTimeout) {
mExtendedTimeout = true;
}
@@ -164,7 +164,7 @@
*
* @return {@code true} if a timeout was already extended.
*/
- public boolean timeoutExtended() {
+ boolean timeoutExtended() {
return mExtendedTimeout;
}
}
diff --git a/services/core/java/com/android/server/rollback/PackageRollbackData.java b/services/core/java/com/android/server/rollback/PackageRollbackData.java
new file mode 100644
index 0000000..15d1242
--- /dev/null
+++ b/services/core/java/com/android/server/rollback/PackageRollbackData.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.rollback;
+
+import android.content.rollback.PackageRollbackInfo;
+
+import java.io.File;
+import java.time.Instant;
+
+/**
+ * Information about a rollback available for a particular package.
+ * This is similar to {@link PackageRollbackInfo}, but extended with
+ * additional information for internal bookkeeping.
+ */
+class PackageRollbackData {
+ public final PackageRollbackInfo info;
+
+ /**
+ * The directory where the apk backup is stored.
+ */
+ public final File backupDir;
+
+ /**
+ * The time when the upgrade occurred, for purposes of expiring
+ * rollback data.
+ */
+ public final Instant timestamp;
+
+ PackageRollbackData(PackageRollbackInfo info,
+ File backupDir, Instant timestamp) {
+ this.info = info;
+ this.backupDir = backupDir;
+ this.timestamp = timestamp;
+ }
+}
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerService.java b/services/core/java/com/android/server/rollback/RollbackManagerService.java
new file mode 100644
index 0000000..5bf2040
--- /dev/null
+++ b/services/core/java/com/android/server/rollback/RollbackManagerService.java
@@ -0,0 +1,846 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.rollback;
+
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageParser;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.StringParceledListSlice;
+import android.content.rollback.IRollbackManager;
+import android.content.rollback.PackageRollbackInfo;
+import android.content.rollback.RollbackInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SELinux;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.pm.PackageManagerServiceUtils;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Service that manages APK level rollbacks.
+ *
+ * TODO: Make RollbackManagerService extend SystemService and move the
+ * IRollbackManager.Stub implementation to a new private
+ * RollbackManagerServiceImpl class.
+ *
+ * @hide
+ */
+public class RollbackManagerService 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 ROLLBACK_LIFETIME_DURATION_MILLIS = 48 * 60 * 60 * 1000;
+
+ // Lock used to synchronize accesses to in-memory rollback data
+ // structures. By convention, methods with the suffix "Locked" require
+ // mLock is held when they are called.
+ private final Object mLock = new Object();
+
+ // Package rollback data available to be used for rolling back a package.
+ // This list is null until the rollback data has been loaded.
+ @GuardedBy("mLock")
+ private List<PackageRollbackData> mAvailableRollbacks;
+
+ // The list of recently executed rollbacks.
+ // This list is null until the rollback data has been loaded.
+ @GuardedBy("mLock")
+ private List<RollbackInfo> mRecentlyExecutedRollbacks;
+
+ // Data for available rollbacks and recently executed rollbacks is
+ // persisted in storage. Assuming the rollback data directory is
+ // /data/rollback, we use the following directory structure
+ // to store this data:
+ // /data/rollback/
+ // available/
+ // com.package.A-XXX/
+ // base.apk
+ // rollback.json
+ // com.package.B-YYY/
+ // base.apk
+ // rollback.json
+ // recently_executed.json
+ // TODO: Use AtomicFile for rollback.json and recently_executed.json.
+ private final File mRollbackDataDir;
+ private final File mAvailableRollbacksDir;
+ private final File mRecentlyExecutedRollbacksFile;
+
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+
+ RollbackManagerService(Context context) {
+ mContext = context;
+ mHandlerThread = new HandlerThread("RollbackManagerServiceHandler");
+ mHandlerThread.start();
+
+ mRollbackDataDir = new File(Environment.getDataDirectory(), "rollback");
+ mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
+ mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
+
+ // Kick off loading of the rollback data from strorage in a background
+ // thread.
+ // TODO: Consider loading the rollback data directly here instead, to
+ // avoid the need to call ensureRollbackDataLoaded every time before
+ // accessing the rollback data?
+ // TODO: Test that this kicks off initial scheduling of rollback
+ // expiration.
+ getHandler().post(() -> ensureRollbackDataLoaded());
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ onPackageReplaced(packageName);
+ }
+ if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ onPackageFullyRemoved(packageName);
+ }
+ }
+ }, filter, null, getHandler());
+
+ IntentFilter enableRollbackFilter = new IntentFilter();
+ enableRollbackFilter.addAction(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK);
+ try {
+ enableRollbackFilter.addDataType("application/vnd.android.package-archive");
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.e(TAG, "addDataType", e);
+ }
+
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_PACKAGE_ENABLE_ROLLBACK.equals(intent.getAction())) {
+ int token = intent.getIntExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN, -1);
+ int installFlags = intent.getIntExtra(
+ PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS, 0);
+ File newPackageCodePath = new File(intent.getData().getPath());
+
+ getHandler().post(() -> {
+ boolean success = enableRollback(installFlags, newPackageCodePath);
+ int ret = PackageManagerInternal.ENABLE_ROLLBACK_SUCCEEDED;
+ if (!success) {
+ ret = PackageManagerInternal.ENABLE_ROLLBACK_FAILED;
+ }
+
+ PackageManagerInternal pm = LocalServices.getService(
+ PackageManagerInternal.class);
+ pm.setEnableRollbackCode(token, ret);
+ });
+
+ // We're handling the ordered broadcast. Abort the
+ // broadcast because there is no need for it to go to
+ // anyone else.
+ abortBroadcast();
+ }
+ }
+ }, enableRollbackFilter, null, getHandler());
+ }
+
+ @Override
+ public RollbackInfo getAvailableRollback(String packageName) {
+ PackageRollbackInfo.PackageVersion installedVersion =
+ getInstalledPackageVersion(packageName);
+ if (installedVersion == null) {
+ return null;
+ }
+
+ 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) {
+ PackageRollbackData data = mAvailableRollbacks.get(i);
+ if (data.info.higherVersion.equals(installedVersion)) {
+ // TODO: For atomic installs, check all dependent packages
+ // for available rollbacks and include that info here.
+ return new RollbackInfo(data.info);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public StringParceledListSlice getPackagesWithAvailableRollbacks() {
+ // TODO: This may return packages whose rollback is out of date or
+ // expired. Presumably that's okay because the package rollback could
+ // be expired anyway between when the caller calls this method and
+ // when the caller calls getAvailableRollback for more details.
+ final Set<String> packageNames = new HashSet<>();
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
+ PackageRollbackData data = mAvailableRollbacks.get(i);
+ packageNames.add(data.info.packageName);
+ }
+ }
+ return new StringParceledListSlice(new ArrayList<>(packageNames));
+ }
+
+ @Override
+ public ParceledListSlice<RollbackInfo> getRecentlyExecutedRollbacks() {
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ List<RollbackInfo> rollbacks = new ArrayList<>(mRecentlyExecutedRollbacks);
+ return new ParceledListSlice<>(rollbacks);
+ }
+ }
+
+ @Override
+ public void executeRollback(RollbackInfo rollback, String callerPackageName,
+ IntentSender statusReceiver) {
+ final int callingUid = Binder.getCallingUid();
+ if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {
+ AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
+ appOps.checkPackage(callingUid, callerPackageName);
+ }
+
+ getHandler().post(() ->
+ executeRollbackInternal(rollback, callerPackageName, statusReceiver));
+ }
+
+ /**
+ * Performs the actual work to execute a rollback.
+ * The work is done on the current thread. This may be a long running
+ * operation.
+ */
+ private void executeRollbackInternal(RollbackInfo rollback,
+ String callerPackageName, IntentSender statusReceiver) {
+ String packageName = rollback.targetPackage.packageName;
+ Log.i(TAG, "Initiating rollback of " + packageName);
+
+ PackageRollbackInfo.PackageVersion installedVersion =
+ getInstalledPackageVersion(packageName);
+ if (installedVersion == null) {
+ // TODO: Test this case
+ sendFailure(statusReceiver, "Target package to roll back is not installed");
+ return;
+ }
+
+ if (!rollback.targetPackage.higherVersion.equals(installedVersion)) {
+ // TODO: Test this case
+ sendFailure(statusReceiver, "Target package version to roll back not installed.");
+ return;
+ }
+
+ // TODO: We assume that between now and the time we commit the
+ // downgrade install, the currently installed package version does not
+ // change. This is not safe to assume, particularly in the case of a
+ // rollback racing with a roll-forward fix of a buggy package.
+ // Figure out how to ensure we don't commit the rollback if
+ // roll forward happens at the same time.
+ PackageRollbackData data = null;
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
+ PackageRollbackData available = mAvailableRollbacks.get(i);
+ // TODO: Check if available.info.lowerVersion matches
+ // rollback.targetPackage.lowerVersion?
+ if (available.info.higherVersion.equals(installedVersion)) {
+ data = available;
+ break;
+ }
+ }
+ }
+
+ if (data == null) {
+ sendFailure(statusReceiver, "Rollback not available");
+ return;
+ }
+
+ // Get a context for the caller to use to install the downgraded
+ // version of the package.
+ Context context = null;
+ try {
+ context = mContext.createPackageContext(callerPackageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ sendFailure(statusReceiver, "Invalid callerPackageName");
+ return;
+ }
+
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInstaller.Session session = null;
+
+ PackageInstaller packageInstaller = pm.getPackageInstaller();
+ PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ params.setAllowDowngrade(true);
+ int sessionId = packageInstaller.createSession(params);
+ session = packageInstaller.openSession(sessionId);
+
+ // TODO: Will it always be called "base.apk"? What about splits?
+ File baseApk = new File(data.backupDir, "base.apk");
+ try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(baseApk,
+ ParcelFileDescriptor.MODE_READ_ONLY)) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ session.write("base.apk", 0, baseApk.length(), fd);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ final LocalIntentReceiver receiver = new LocalIntentReceiver();
+ session.commit(receiver.getIntentSender());
+
+ Intent result = receiver.getResult();
+ int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ if (status != PackageInstaller.STATUS_SUCCESS) {
+ sendFailure(statusReceiver, "Rollback downgrade install failed: "
+ + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE));
+ return;
+ }
+
+ addRecentlyExecutedRollback(rollback);
+ sendSuccess(statusReceiver);
+
+ // TODO: Restrict permissions for who can listen for this
+ // broadcast?
+ Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
+ Uri.fromParts("package", packageName, null));
+
+ // TODO: This call emits the warning "Calling a method in the
+ // system process without a qualified user". Fix that.
+ mContext.sendBroadcast(broadcast);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to roll back " + packageName, e);
+ sendFailure(statusReceiver, "IOException: " + e.toString());
+ return;
+ }
+ }
+
+ @Override
+ public void reloadPersistedData() {
+ synchronized (mLock) {
+ mAvailableRollbacks = null;
+ mRecentlyExecutedRollbacks = null;
+ }
+ getHandler().post(() -> ensureRollbackDataLoaded());
+ }
+
+ @Override
+ public void expireRollbackForPackage(String packageName) {
+ // 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<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ while (iter.hasNext()) {
+ PackageRollbackData data = iter.next();
+ if (data.info.packageName.equals(packageName)) {
+ iter.remove();
+ removeFile(data.backupDir);
+ }
+ }
+ }
+ }
+
+ /**
+ * Load rollback data from storage if it has not already been loaded.
+ * After calling this funciton, mAvailableRollbacks and
+ * mRecentlyExecutedRollbacks will be non-null.
+ */
+ private void ensureRollbackDataLoaded() {
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ }
+ }
+
+ /**
+ * Load rollback data from storage if it has not already been loaded.
+ * After calling this function, mAvailableRollbacks and
+ * mRecentlyExecutedRollbacks will be non-null.
+ */
+ @GuardedBy("mLock")
+ private void ensureRollbackDataLoadedLocked() {
+ if (mAvailableRollbacks == null) {
+ loadRollbackDataLocked();
+ }
+ }
+
+ /**
+ * Load rollback data from storage.
+ * Note: We do potentially heavy IO here while holding mLock, because we
+ * have to have the rollback data loaded before we can do anything else
+ * meaningful.
+ */
+ @GuardedBy("mLock")
+ private void loadRollbackDataLocked() {
+ mAvailableRollbacksDir.mkdirs();
+ mAvailableRollbacks = new ArrayList<>();
+ for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
+ if (rollbackDir.isDirectory()) {
+ // TODO: How to detect and clean up an invalid rollback
+ // directory? We don't know if it's invalid because something
+ // went wrong, or if it's only temporarily invalid because
+ // it's in the process of being created.
+ try {
+ File jsonFile = new File(rollbackDir, "rollback.json");
+ String jsonString = IoUtils.readFileAsString(jsonFile.getAbsolutePath());
+ JSONObject jsonObject = new JSONObject(jsonString);
+ String packageName = jsonObject.getString("packageName");
+ long higherVersionCode = jsonObject.getLong("higherVersionCode");
+ long lowerVersionCode = jsonObject.getLong("lowerVersionCode");
+ Instant timestamp = Instant.parse(jsonObject.getString("timestamp"));
+ PackageRollbackData data = new PackageRollbackData(
+ new PackageRollbackInfo(packageName,
+ new PackageRollbackInfo.PackageVersion(higherVersionCode),
+ new PackageRollbackInfo.PackageVersion(lowerVersionCode)),
+ rollbackDir, timestamp);
+ mAvailableRollbacks.add(data);
+ } catch (IOException | JSONException | DateTimeParseException e) {
+ Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
+ }
+ }
+ }
+
+ mRecentlyExecutedRollbacks = 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) {
+ JSONObject element = array.getJSONObject(i);
+ String packageName = element.getString("packageName");
+ long higherVersionCode = element.getLong("higherVersionCode");
+ long lowerVersionCode = element.getLong("lowerVersionCode");
+ PackageRollbackInfo target = new PackageRollbackInfo(packageName,
+ new PackageRollbackInfo.PackageVersion(higherVersionCode),
+ new PackageRollbackInfo.PackageVersion(lowerVersionCode));
+ RollbackInfo rollback = new RollbackInfo(target);
+ mRecentlyExecutedRollbacks.add(rollback);
+ }
+ } 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);
+ }
+ }
+
+ scheduleExpiration(0);
+ }
+
+ /**
+ * Called when a package has been replaced with a different version.
+ * Removes all backups for the package not matching the currently
+ * installed package version.
+ */
+ private void onPackageReplaced(String packageName) {
+ // TODO: Could this end up incorrectly deleting a rollback for a
+ // package that is about to be installed?
+ PackageRollbackInfo.PackageVersion installedVersion =
+ getInstalledPackageVersion(packageName);
+
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ while (iter.hasNext()) {
+ PackageRollbackData data = iter.next();
+ if (data.info.packageName.equals(packageName)
+ && !data.info.higherVersion.equals(installedVersion)) {
+ iter.remove();
+ removeFile(data.backupDir);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when a package has been completely removed from the device.
+ * Removes all backups and rollback history for the given package.
+ */
+ 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();
+ if (packageName.equals(rollback.targetPackage.packageName)) {
+ iter.remove();
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ saveRecentlyExecutedRollbacksLocked();
+ }
+ }
+ }
+
+ /**
+ * Write the list of recently executed rollbacks to storage.
+ * Note: This happens while mLock is held, which should be okay because we
+ * expect executed rollbacks to be modified only in exceptional cases.
+ */
+ @GuardedBy("mLock")
+ private void saveRecentlyExecutedRollbacksLocked() {
+ try {
+ JSONObject json = new JSONObject();
+ JSONArray array = new JSONArray();
+ json.put("recentlyExecuted", array);
+
+ for (int i = 0; i < mRecentlyExecutedRollbacks.size(); ++i) {
+ RollbackInfo rollback = mRecentlyExecutedRollbacks.get(i);
+ JSONObject element = new JSONObject();
+ element.put("packageName", rollback.targetPackage.packageName);
+ element.put("higherVersionCode", rollback.targetPackage.higherVersion.versionCode);
+ element.put("lowerVersionCode", rollback.targetPackage.lowerVersion.versionCode);
+ array.put(element);
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * 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();
+ mRecentlyExecutedRollbacks.add(rollback);
+ saveRecentlyExecutedRollbacksLocked();
+ }
+ }
+
+ /**
+ * Notifies an IntentSender of failure.
+ *
+ * @param statusReceiver where to send the failure
+ * @param message the failure message.
+ */
+ private void sendFailure(IntentSender statusReceiver, String message) {
+ Log.e(TAG, message);
+ try {
+ // TODO: More context on which rollback failed?
+ // TODO: More refined failure code?
+ final Intent fillIn = new Intent();
+ fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
+ fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
+ statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ // Nowhere to send the result back to, so don't bother.
+ }
+ }
+
+ /**
+ * Notifies an IntentSender of success.
+ */
+ private void sendSuccess(IntentSender statusReceiver) {
+ try {
+ final Intent fillIn = new Intent();
+ fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS);
+ statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ // Nowhere to send the result back to, so don't bother.
+ }
+ }
+
+ // 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<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ while (iter.hasNext()) {
+ PackageRollbackData data = iter.next();
+ if (!now.isBefore(data.timestamp.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS))) {
+ iter.remove();
+ removeFile(data.backupDir);
+ } else if (oldest == null || oldest.isAfter(data.timestamp)) {
+ oldest = data.timestamp;
+ }
+ }
+ }
+
+ if (oldest != null) {
+ scheduleExpiration(now.until(oldest.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS),
+ ChronoUnit.MILLIS));
+ }
+ }
+
+ /**
+ * Schedules an expiration check to be run after the given duration in
+ * milliseconds has gone by.
+ */
+ private void scheduleExpiration(long duration) {
+ getHandler().postDelayed(() -> runExpiration(), duration);
+ }
+
+ /**
+ * Gets the RollbackManagerService's handler.
+ * To allow PackageManagerService to call into RollbackManagerService
+ * without fear of blocking the PackageManagerService thread.
+ */
+ public Handler getHandler() {
+ return mHandlerThread.getThreadHandler();
+ }
+
+ /**
+ * Called via broadcast by the package manager when a package is being
+ * staged for install with rollback enabled. Called before the package has
+ * been installed.
+ *
+ * @param id the id of the enable rollback request
+ * @param installFlags information about what is being installed.
+ * @param newPackageCodePath path to the package about to be installed.
+ * @return true if enabling the rollback succeeds, false otherwise.
+ */
+ private boolean enableRollback(int installFlags, File newPackageCodePath) {
+ if ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
+ Log.e(TAG, "Rollbacks not supported for instant app install");
+ return false;
+ }
+ if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
+ Log.e(TAG, "Rollbacks not supported for apex install");
+ return false;
+ }
+
+ // Get information about the package to be installed.
+ PackageParser.PackageLite newPackage = null;
+ try {
+ newPackage = PackageParser.parsePackageLite(newPackageCodePath, 0);
+ } catch (PackageParser.PackageParserException e) {
+ Log.e(TAG, "Unable to parse new package", e);
+ return false;
+ }
+
+ String packageName = newPackage.packageName;
+ Log.i(TAG, "Enabling rollback for install of " + packageName);
+
+ PackageRollbackInfo.PackageVersion newVersion =
+ new PackageRollbackInfo.PackageVersion(newPackage.versionCode);
+
+ // Get information about the currently installed package.
+ PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
+ PackageParser.Package installedPackage = pm.getPackage(packageName);
+ if (installedPackage == null) {
+ // TODO: Support rolling back fresh package installs rather than
+ // fail here. Test this case.
+ Log.e(TAG, packageName + " is not installed");
+ return false;
+ }
+ PackageRollbackInfo.PackageVersion installedVersion =
+ new PackageRollbackInfo.PackageVersion(installedPackage.getLongVersionCode());
+
+ File backupDir;
+ try {
+ backupDir = Files.createTempDirectory(
+ mAvailableRollbacksDir.toPath(), packageName + "-").toFile();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to create rollback for " + packageName, e);
+ return false;
+ }
+
+ // TODO: Should the timestamp be for when we commit the install, not
+ // when we create the pending one?
+ Instant timestamp = Instant.now();
+ try {
+ JSONObject json = new JSONObject();
+ json.put("packageName", packageName);
+ json.put("higherVersionCode", newVersion.versionCode);
+ json.put("lowerVersionCode", installedVersion.versionCode);
+ json.put("timestamp", timestamp.toString());
+
+ File jsonFile = new File(backupDir, "rollback.json");
+ PrintWriter pw = new PrintWriter(jsonFile);
+ pw.println(json.toString());
+ pw.close();
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, "Unable to create rollback for " + packageName, e);
+ removeFile(backupDir);
+ return false;
+ }
+
+ // TODO: Copy by hard link instead to save on cpu and storage space?
+ int status = PackageManagerServiceUtils.copyPackage(installedPackage.codePath, backupDir);
+ if (status != PackageManager.INSTALL_SUCCEEDED) {
+ Log.e(TAG, "Unable to copy package for rollback for " + packageName);
+ removeFile(backupDir);
+ return false;
+ }
+
+ PackageRollbackData data = new PackageRollbackData(
+ new PackageRollbackInfo(packageName, newVersion, installedVersion),
+ backupDir, timestamp);
+
+ synchronized (mLock) {
+ ensureRollbackDataLoadedLocked();
+ mAvailableRollbacks.add(data);
+ }
+
+ return true;
+ }
+
+ // TODO: Don't copy this from PackageManagerShellCommand like this?
+ private static class LocalIntentReceiver {
+ private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
+
+ private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
+ @Override
+ public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
+ IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+ try {
+ mResult.offer(intent, 5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
+ public IntentSender getIntentSender() {
+ return new IntentSender((IIntentSender) mLocalSender);
+ }
+
+ public Intent getResult() {
+ try {
+ return mResult.take();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Deletes a file completely.
+ * 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) {
+ if (file.isDirectory()) {
+ for (File child : file.listFiles()) {
+ removeFile(child);
+ }
+ }
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ /**
+ * Gets the version of the package currently installed.
+ * Returns null if the package is not currently installed.
+ */
+ private PackageRollbackInfo.PackageVersion getInstalledPackageVersion(String packageName) {
+ PackageManager pm = mContext.getPackageManager();
+ PackageInfo pkgInfo = null;
+ try {
+ pkgInfo = pm.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+
+ return new PackageRollbackInfo.PackageVersion(pkgInfo.getLongVersionCode());
+ }
+
+ /**
+ * Manages the lifecycle of RollbackManagerService within System Server.
+ */
+ public static class Lifecycle extends SystemService {
+ private RollbackManagerService mService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mService = new RollbackManagerService(getContext());
+
+ // TODO: Set up sepolicy to allow publishing the service.
+ if (SELinux.isSELinuxEnforced()) {
+ Log.w(TAG, "RollbackManager disabled pending selinux policy updates");
+ } else {
+ publishBinderService(Context.ROLLBACK_SERVICE, mService);
+ }
+ }
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 046c991..159a3f9 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -122,6 +122,7 @@
import com.android.server.power.ThermalManagerService;
import com.android.server.restrictions.RestrictionsManagerService;
import com.android.server.role.RoleManagerService;
+import com.android.server.rollback.RollbackManagerService;
import com.android.server.security.KeyAttestationApplicationIdProviderService;
import com.android.server.security.KeyChainSystemService;
import com.android.server.signedconfig.SignedConfigService;
@@ -778,6 +779,11 @@
traceBeginAndSlog("StartLooperStatsService");
mSystemServiceManager.startService(LooperStatsService.Lifecycle.class);
traceEnd();
+
+ // Manages apk rollbacks.
+ traceBeginAndSlog("StartRollbackManagerService");
+ mSystemServiceManager.startService(RollbackManagerService.Lifecycle.class);
+ traceEnd();
}
/**
diff --git a/test-mock/src/android/test/mock/MockPackageManager.java b/test-mock/src/android/test/mock/MockPackageManager.java
index 89734e3..226c0b8 100644
--- a/test-mock/src/android/test/mock/MockPackageManager.java
+++ b/test-mock/src/android/test/mock/MockPackageManager.java
@@ -18,8 +18,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.PackageInstallObserver;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -52,7 +50,6 @@
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.os.Handler;
import android.os.PersistableBundle;
import android.os.UserHandle;
diff --git a/tests/RollbackTest/Android.mk b/tests/RollbackTest/Android.mk
new file mode 100644
index 0000000..f2bd407
--- /dev/null
+++ b/tests/RollbackTest/Android.mk
@@ -0,0 +1,54 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+# RollbackTestAppV1
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_MANIFEST_FILE := TestApp/AndroidManifestV1.xml
+LOCAL_PACKAGE_NAME := RollbackTestAppV1
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, TestApp/src)
+include $(BUILD_PACKAGE)
+ROLLBACK_TEST_APP_V1 := $(LOCAL_INSTALLED_MODULE)
+
+# RollbackTestAppV2
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_MANIFEST_FILE := TestApp/AndroidManifestV2.xml
+LOCAL_PACKAGE_NAME := RollbackTestAppV2
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, TestApp/src)
+include $(BUILD_PACKAGE)
+ROLLBACK_TEST_APP_V2 := $(LOCAL_INSTALLED_MODULE)
+
+# RollbackTest
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_PACKAGE_NAME := RollbackTest
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_CERTIFICATE := platform
+LOCAL_JAVA_RESOURCE_FILES := $(ROLLBACK_TEST_APP_V1) $(ROLLBACK_TEST_APP_V2)
+LOCAL_SDK_VERSION := test_current
+LOCAL_TEST_CONFIG := RollbackTest.xml
+include $(BUILD_PACKAGE)
+
+# Clean up local variables
+ROLLBACK_TEST_APP_V1 :=
+ROLLBACK_TEST_APP_V2 :=
diff --git a/tests/RollbackTest/AndroidManifest.xml b/tests/RollbackTest/AndroidManifest.xml
new file mode 100644
index 0000000..d1535a3
--- /dev/null
+++ b/tests/RollbackTest/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tests.rollback" >
+
+ <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
+ <uses-permission android:name="android.permission.DELETE_PACKAGES" />
+
+ <application>
+ <receiver android:name="com.android.tests.rollback.LocalIntentSender"
+ android:exported="true" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.tests.rollback"
+ android:label="Rollback Test"/>
+
+</manifest>
diff --git a/tests/RollbackTest/RollbackTest.xml b/tests/RollbackTest/RollbackTest.xml
new file mode 100644
index 0000000..adbad56
--- /dev/null
+++ b/tests/RollbackTest/RollbackTest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs the rollback tests">
+ <option name="test-suite-tag" value="RollbackTest" />
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="RollbackTest.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.tests.rollback" />
+ <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/RollbackTest/TestApp/AndroidManifestV1.xml b/tests/RollbackTest/TestApp/AndroidManifestV1.xml
new file mode 100644
index 0000000..45e9584
--- /dev/null
+++ b/tests/RollbackTest/TestApp/AndroidManifestV1.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tests.rollback.testapp"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+
+ <uses-sdk android:minSdkVersion="19" />
+
+ <application android:label="Rollback Test App V1">
+ <activity android:name="com.android.tests.rollback.testapp.MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/tests/RollbackTest/TestApp/AndroidManifestV2.xml b/tests/RollbackTest/TestApp/AndroidManifestV2.xml
new file mode 100644
index 0000000..0104086
--- /dev/null
+++ b/tests/RollbackTest/TestApp/AndroidManifestV2.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tests.rollback.testapp"
+ android:versionCode="2"
+ android:versionName="2.0" >
+
+
+ <uses-sdk android:minSdkVersion="19" />
+
+ <application android:label="Rollback Test App V2">
+ <activity android:name="com.android.tests.rollback.testapp.MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java
new file mode 100644
index 0000000..1856bac
--- /dev/null
+++ b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.rollback.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * A test app for testing apk rollback support.
+ */
+public class MainActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java b/tests/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java
new file mode 100644
index 0000000..ddcf1da
--- /dev/null
+++ b/tests/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.rollback;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.support.test.InstrumentationRegistry;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Make IntentSender that sends intent locally.
+ */
+public class LocalIntentSender extends BroadcastReceiver {
+
+ private static final String TAG = "RollbackTest";
+
+ private static final BlockingQueue<Intent> sIntentSenderResults = new LinkedBlockingQueue<>();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ sIntentSenderResults.add(intent);
+ }
+
+ /**
+ * Get a LocalIntentSender.
+ */
+ static IntentSender getIntentSender() {
+ Context context = InstrumentationRegistry.getContext();
+ Intent intent = new Intent(context, LocalIntentSender.class);
+ PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, 0);
+ return pending.getIntentSender();
+ }
+
+ /**
+ * Returns the most recent Intent sent by a LocalIntentSender.
+ */
+ static Intent getIntentSenderResult() throws InterruptedException {
+ return sIntentSenderResults.take();
+ }
+}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java
new file mode 100644
index 0000000..d3c39f0
--- /dev/null
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.rollback;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A broadcast receiver that can be used to get
+ * ACTION_PACKAGE_ROLLBACK_EXECUTED broadcasts.
+ */
+class RollbackBroadcastReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "RollbackTest";
+
+ private final BlockingQueue<Intent> mRollbackBroadcasts = new LinkedBlockingQueue<>();
+
+ /**
+ * Creates a RollbackBroadcastReceiver and registers it with the given
+ * context.
+ */
+ RollbackBroadcastReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED);
+ filter.addDataScheme("package");
+ InstrumentationRegistry.getContext().registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "Received rollback broadcast intent");
+ mRollbackBroadcasts.add(intent);
+ }
+
+ /**
+ * Polls for at most the given amount of time for the next rollback
+ * broadcast.
+ */
+ Intent poll(long timeout, TimeUnit unit) throws InterruptedException {
+ return mRollbackBroadcasts.poll(timeout, unit);
+ }
+
+ /**
+ * Unregisters this broadcast receiver.
+ */
+ void unregister() {
+ InstrumentationRegistry.getContext().unregisterReceiver(this);
+ }
+}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
new file mode 100644
index 0000000..02c1ce2
--- /dev/null
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.rollback;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test system Rollback APIs.
+ * TODO: Should this be a cts test instead? Where should it live?
+ */
+@RunWith(JUnit4.class)
+public class RollbackTest {
+
+ private static final String TAG = "RollbackTest";
+
+ private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.rollback.testapp";
+
+ /**
+ * Test basic rollbacks.
+ */
+ @Test
+ public void testBasic() throws Exception {
+ // Make sure an app can't listen to or disturb the internal
+ // ACTION_PACKAGE_ENABLE_ROLLBACK broadcast.
+ Context context = InstrumentationRegistry.getContext();
+ IntentFilter enableRollbackFilter = new IntentFilter();
+ enableRollbackFilter.addAction("android.intent.action.PACKAGE_ENABLE_ROLLBACK");
+ enableRollbackFilter.addDataType("application/vnd.android.package-archive");
+ enableRollbackFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ BroadcastReceiver enableRollbackReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ abortBroadcast();
+ }
+ };
+ context.registerReceiver(enableRollbackReceiver, enableRollbackFilter);
+
+ // Register a broadcast receiver for notification when the rollback is
+ // done executing.
+ RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver();
+ RollbackManager rm = RollbackTestUtils.getRollbackManager();
+
+ // Uninstall com.android.tests.rollback.testapp
+ RollbackTestUtils.uninstall("com.android.tests.rollback.testapp");
+ assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // TODO: There is currently a race condition between when the app is
+ // uninstalled and when rollback manager deletes the rollback. Fix it
+ // so that's not the case!
+ for (int i = 0; i < 5; ++i) {
+ for (RollbackInfo info : rm.getRecentlyExecutedRollbacks()) {
+ if (TEST_APP_PACKAGE_NAME.equals(info.targetPackage.packageName)) {
+ Log.i(TAG, "Sleeping 1 second to wait for uninstall to take effect.");
+ Thread.sleep(1000);
+ break;
+ }
+ }
+ }
+
+ // The app should not be available for rollback.
+ assertNull(rm.getAvailableRollback(TEST_APP_PACKAGE_NAME));
+ assertFalse(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+
+ // There should be no recently executed rollbacks for this package.
+ for (RollbackInfo info : rm.getRecentlyExecutedRollbacks()) {
+ assertNotEquals(TEST_APP_PACKAGE_NAME, info.targetPackage.packageName);
+ }
+
+ // Install v1 of the app (without rollbacks enabled).
+ RollbackTestUtils.install("RollbackTestAppV1.apk", false);
+ assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // Upgrade from v1 to v2, with rollbacks enabled.
+ RollbackTestUtils.install("RollbackTestAppV2.apk", true);
+ assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // The app should now be available for rollback.
+ assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+ RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_PACKAGE_NAME);
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ // We should not have received any rollback requests yet.
+ // TODO: Possibly flaky if, by chance, some other app on device
+ // happens to be rolled back at the same time?
+ assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS));
+
+ // Roll back the app.
+ RollbackTestUtils.rollback(rollback);
+ assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // Verify we received a broadcast for the rollback.
+ // TODO: Race condition between the timeout and when the broadcast is
+ // received could lead to test flakiness.
+ Intent broadcast = broadcastReceiver.poll(5, TimeUnit.SECONDS);
+ assertNotNull(broadcast);
+ assertEquals(TEST_APP_PACKAGE_NAME, broadcast.getData().getSchemeSpecificPart());
+ assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS));
+
+ // Verify the recent rollback has been recorded.
+ rollback = null;
+ for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
+ if (TEST_APP_PACKAGE_NAME.equals(r.targetPackage.packageName)) {
+ assertNull(rollback);
+ rollback = r;
+ }
+ }
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ broadcastReceiver.unregister();
+ context.unregisterReceiver(enableRollbackReceiver);
+ }
+
+ /**
+ * Test that rollback data is properly persisted.
+ */
+ @Test
+ public void testRollbackDataPersistence() throws Exception {
+ RollbackManager rm = RollbackTestUtils.getRollbackManager();
+
+ // Prep installation of com.android.tests.rollback.testapp
+ RollbackTestUtils.uninstall("com.android.tests.rollback.testapp");
+ RollbackTestUtils.install("RollbackTestAppV1.apk", false);
+ RollbackTestUtils.install("RollbackTestAppV2.apk", true);
+ assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // The app should now be available for rollback.
+ assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+ RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_PACKAGE_NAME);
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ // Reload the persisted data.
+ rm.reloadPersistedData();
+
+ // The app should still be available for rollback.
+ assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+ rollback = rm.getAvailableRollback(TEST_APP_PACKAGE_NAME);
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ // Roll back the app.
+ RollbackTestUtils.rollback(rollback);
+ assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // Verify the recent rollback has been recorded.
+ rollback = null;
+ for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
+ if (TEST_APP_PACKAGE_NAME.equals(r.targetPackage.packageName)) {
+ assertNull(rollback);
+ rollback = r;
+ }
+ }
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ // Reload the persisted data.
+ rm.reloadPersistedData();
+
+ // Verify the recent rollback is still recorded.
+ rollback = null;
+ for (RollbackInfo r : rm.getRecentlyExecutedRollbacks()) {
+ if (TEST_APP_PACKAGE_NAME.equals(r.targetPackage.packageName)) {
+ assertNull(rollback);
+ rollback = r;
+ }
+ }
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+ }
+
+ /**
+ * Test explicit expiration of rollbacks.
+ * Does not test the scheduling aspects of rollback expiration.
+ */
+ @Test
+ public void testRollbackExpiration() throws Exception {
+ RollbackManager rm = RollbackTestUtils.getRollbackManager();
+ RollbackTestUtils.uninstall("com.android.tests.rollback.testapp");
+ RollbackTestUtils.install("RollbackTestAppV1.apk", false);
+ RollbackTestUtils.install("RollbackTestAppV2.apk", true);
+ assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_PACKAGE_NAME));
+
+ // The app should now be available for rollback.
+ assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+ RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_PACKAGE_NAME);
+ assertNotNull(rollback);
+ assertEquals(TEST_APP_PACKAGE_NAME, rollback.targetPackage.packageName);
+ assertEquals(2, rollback.targetPackage.higherVersion.versionCode);
+ assertEquals(1, rollback.targetPackage.lowerVersion.versionCode);
+
+ // Expire the rollback.
+ rm.expireRollbackForPackage(TEST_APP_PACKAGE_NAME);
+
+ // The rollback should no longer be available.
+ assertNull(rm.getAvailableRollback(TEST_APP_PACKAGE_NAME));
+ assertFalse(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_PACKAGE_NAME));
+ }
+
+ /**
+ * Test restrictions on rollback broadcast sender.
+ * A random app should not be able to send a PACKAGE_ROLLBACK_EXECUTED broadcast.
+ */
+ @Test
+ public void testRollbackBroadcastRestrictions() throws Exception {
+ RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver();
+ Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
+ Uri.fromParts("package", "com.android.tests.rollback.bogus", null));
+ try {
+ InstrumentationRegistry.getContext().sendBroadcast(broadcast);
+ fail("Succeeded in sending restricted broadcast from app context.");
+ } catch (SecurityException se) {
+ // Expected behavior.
+ }
+
+ // Confirm that we really haven't received the broadcast.
+ // TODO: How long to wait for the expected timeout?
+ assertNull(broadcastReceiver.poll(5, TimeUnit.SECONDS));
+
+ // TODO: Do we need to do this? Do we need to ensure this is always
+ // called, even when the test fails?
+ broadcastReceiver.unregister();
+ }
+}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java
new file mode 100644
index 0000000..c5ce3fa
--- /dev/null
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.rollback;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.support.test.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Utilities to facilitate testing rollbacks.
+ */
+class RollbackTestUtils {
+
+ private static final String TAG = "RollbackTest";
+
+ static RollbackManager getRollbackManager() {
+ Context context = InstrumentationRegistry.getContext();
+ RollbackManager rm = (RollbackManager) context.getSystemService(Context.ROLLBACK_SERVICE);
+ if (rm == null) {
+ throw new AssertionError("Failed to get RollbackManager");
+ }
+ return rm;
+ }
+
+ /**
+ * Returns the version of the given package installed on device.
+ * Returns -1 if the package is not currently installed.
+ */
+ static long getInstalledVersion(String packageName) {
+ Context context = InstrumentationRegistry.getContext();
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo info = pm.getPackageInfo(packageName, 0);
+ return info.getLongVersionCode();
+ } catch (PackageManager.NameNotFoundException e) {
+ return -1;
+ }
+ }
+
+ private static void assertStatusSuccess(Intent result) {
+ int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ if (status == -1) {
+ throw new AssertionError("PENDING USER ACTION");
+ } else if (status > 0) {
+ String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
+ throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
+ }
+ }
+
+ /**
+ * Uninstalls the given package.
+ * Does nothing if the package is not installed.
+ * @throws AssertionError if package can't be uninstalled.
+ */
+ static void uninstall(String packageName) throws InterruptedException, IOException {
+ // No need to uninstall if the package isn't installed.
+ if (getInstalledVersion(packageName) == -1) {
+ return;
+ }
+
+ Context context = InstrumentationRegistry.getContext();
+ PackageManager packageManager = context.getPackageManager();
+ PackageInstaller packageInstaller = packageManager.getPackageInstaller();
+ packageInstaller.uninstall(packageName, LocalIntentSender.getIntentSender());
+ assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+ }
+
+ /**
+ * Execute the given rollback.
+ * @throws AssertionError if the rollback fails.
+ */
+ static void rollback(RollbackInfo rollback) throws InterruptedException {
+ RollbackManager rm = getRollbackManager();
+ rm.executeRollback(rollback, LocalIntentSender.getIntentSender());
+ assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+ }
+
+ /**
+ * Installs the apk with the given name.
+ *
+ * @param resourceName name of class loader resource for the apk to
+ * install.
+ * @param enableRollback if rollback should be enabled.
+ * @throws AssertionError if the installation fails.
+ */
+ static void install(String resourceName, boolean enableRollback)
+ throws InterruptedException, IOException {
+ Context context = InstrumentationRegistry.getContext();
+ PackageInstaller.Session session = null;
+ PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
+ PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ if (enableRollback) {
+ params.setEnableRollback();
+ }
+ int sessionId = packageInstaller.createSession(params);
+ session = packageInstaller.openSession(sessionId);
+
+ ClassLoader loader = RollbackTest.class.getClassLoader();
+ try (OutputStream packageInSession = session.openWrite("package", 0, -1);
+ InputStream is = loader.getResourceAsStream(resourceName);) {
+ byte[] buffer = new byte[4096];
+ int n;
+ while ((n = is.read(buffer)) >= 0) {
+ packageInSession.write(buffer, 0, n);
+ }
+ }
+
+ // Commit the session (this will start the installation workflow).
+ session.commit(LocalIntentSender.getIntentSender());
+ assertStatusSuccess(LocalIntentSender.getIntentSenderResult());
+ }
+
+ // TODO: Unused
+ static void adoptShellPermissionIdentity() {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ }
+
+ // TODO: Unused
+ static void dropShellPermissionIdentity() {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+}