Merge "Adds Westworld logging of RescueParty reset events."
diff --git a/api/system-current.txt b/api/system-current.txt
index a4b3978..2935c4f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5662,7 +5662,7 @@
ctor public NotificationAssistantService();
method public final void adjustNotification(android.service.notification.Adjustment);
method public final void adjustNotifications(java.util.List<android.service.notification.Adjustment>);
- method public void onActionClicked(java.lang.String, android.app.Notification.Action, int);
+ method public void onActionInvoked(java.lang.String, android.app.Notification.Action, int);
method public final android.os.IBinder onBind(android.content.Intent);
method public void onNotificationDirectReplied(java.lang.String);
method public android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification);
diff --git a/api/test-current.txt b/api/test-current.txt
index 575875d..957f957 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1275,7 +1275,7 @@
ctor public NotificationAssistantService();
method public final void adjustNotification(android.service.notification.Adjustment);
method public final void adjustNotifications(java.util.List<android.service.notification.Adjustment>);
- method public void onActionClicked(java.lang.String, android.app.Notification.Action, int);
+ method public void onActionInvoked(java.lang.String, android.app.Notification.Action, int);
method public final android.os.IBinder onBind(android.content.Intent);
method public void onNotificationDirectReplied(java.lang.String);
method public android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 574ccaf..ee6e1ba 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13772,6 +13772,7 @@
* requires_targeting_p (boolean)
* max_squeeze_remeasure_attempts (int)
* edit_choices_before_sending (boolean)
+ * show_in_heads_up (boolean)
* </pre>
* @see com.android.systemui.statusbar.policy.SmartReplyConstants
* @hide
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index fd2b0fa..63fd563 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -203,7 +203,7 @@
* @param action the action that is just clicked
* @param source the source that provided the action, e.g. SOURCE_FROM_APP
*/
- public void onActionClicked(@NonNull String key, @NonNull Notification.Action action,
+ public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action,
@Source int source) {
}
@@ -338,7 +338,7 @@
args.arg1 = key;
args.arg2 = action;
args.argi2 = source;
- mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_CLICKED, args).sendToTarget();
+ mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget();
}
}
@@ -349,7 +349,7 @@
public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4;
public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5;
public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6;
- public static final int MSG_ON_ACTION_CLICKED = 7;
+ public static final int MSG_ON_ACTION_INVOKED = 7;
public MyHandler(Looper looper) {
super(looper, null, false);
@@ -419,13 +419,13 @@
onSuggestedReplySent(key, reply, source);
break;
}
- case MSG_ON_ACTION_CLICKED: {
+ case MSG_ON_ACTION_INVOKED: {
SomeArgs args = (SomeArgs) msg.obj;
String key = (String) args.arg1;
Notification.Action action = (Notification.Action) args.arg2;
int source = args.argi2;
args.recycle();
- onActionClicked(key, action, source);
+ onActionInvoked(key, action, source);
break;
}
}
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
index fab1bcc..838b88b 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
@@ -382,11 +382,11 @@
}
@Override
- public void onActionClicked(@NonNull String key, @NonNull Notification.Action action,
+ public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action,
@Source int source) {
if (DEBUG) {
Log.d(TAG,
- "onActionClicked() called with: key = [" + key + "], action = [" + action.title
+ "onActionInvoked() called with: key = [" + key + "], action = [" + action.title
+ "], source = [" + source + "]");
}
mSmartActionsHelper.onActionClicked(key, action, source);
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7cc5524..476089a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -454,6 +454,10 @@
RemoteInput.Builder.setEditChoicesBeforeSending. -->
<bool name="config_smart_replies_in_notifications_edit_choices_before_sending">false</bool>
+ <!-- Smart replies in notifications: Whether smart suggestions in notifications are enabled in
+ heads-up notifications. -->
+ <bool name="config_smart_replies_in_notifications_show_in_heads_up">true</bool>
+
<!-- Screenshot editing default activity. Must handle ACTION_EDIT image/png intents.
Blank sends the user to the Chooser first.
This name is in the ComponentName flattened format (package/class) -->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index bf30cf9..c161da3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1489,7 +1489,7 @@
}
}
}
- if (mHeadsUpChild != null) {
+ if (mHeadsUpChild != null && mSmartReplyConstants.getShowInHeadsUp()) {
mHeadsUpSmartReplyView =
applySmartReplyView(mHeadsUpChild, smartRepliesAndActions, entry);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
index 0c63e29..3bd0d45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
@@ -45,16 +45,19 @@
"max_squeeze_remeasure_attempts";
private static final String KEY_EDIT_CHOICES_BEFORE_SENDING =
"edit_choices_before_sending";
+ private static final String KEY_SHOW_IN_HEADS_UP = "show_in_heads_up";
private final boolean mDefaultEnabled;
private final boolean mDefaultRequiresP;
private final int mDefaultMaxSqueezeRemeasureAttempts;
private final boolean mDefaultEditChoicesBeforeSending;
+ private final boolean mDefaultShowInHeadsUp;
private boolean mEnabled;
private boolean mRequiresTargetingP;
private int mMaxSqueezeRemeasureAttempts;
private boolean mEditChoicesBeforeSending;
+ private boolean mShowInHeadsUp;
private final Context mContext;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -73,6 +76,8 @@
R.integer.config_smart_replies_in_notifications_max_squeeze_remeasure_attempts);
mDefaultEditChoicesBeforeSending = resources.getBoolean(
R.bool.config_smart_replies_in_notifications_edit_choices_before_sending);
+ mDefaultShowInHeadsUp = resources.getBoolean(
+ R.bool.config_smart_replies_in_notifications_show_in_heads_up);
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS),
@@ -99,6 +104,7 @@
KEY_MAX_SQUEEZE_REMEASURE_ATTEMPTS, mDefaultMaxSqueezeRemeasureAttempts);
mEditChoicesBeforeSending = mParser.getBoolean(
KEY_EDIT_CHOICES_BEFORE_SENDING, mDefaultEditChoicesBeforeSending);
+ mShowInHeadsUp = mParser.getBoolean(KEY_SHOW_IN_HEADS_UP, mDefaultShowInHeadsUp);
}
}
@@ -142,4 +148,11 @@
return mEditChoicesBeforeSending;
}
}
+
+ /**
+ * Returns whether smart suggestions should be enabled in heads-up notifications.
+ */
+ public boolean getShowInHeadsUp() {
+ return mShowInHeadsUp;
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
index 37a56a3..3cbf902 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
@@ -54,6 +54,7 @@
R.integer.config_smart_replies_in_notifications_max_squeeze_remeasure_attempts, 7);
resources.addOverride(
R.bool.config_smart_replies_in_notifications_edit_choices_before_sending, false);
+ resources.addOverride(R.bool.config_smart_replies_in_notifications_show_in_heads_up, true);
mConstants = new SmartReplyConstants(Handler.createAsync(Looper.myLooper()), mContext);
}
@@ -152,6 +153,26 @@
RemoteInput.EDIT_CHOICES_BEFORE_SENDING_DISABLED));
}
+ @Test
+ public void testShowInHeadsUpWithNoConfig() {
+ assertTrue(mConstants.isEnabled());
+ assertTrue(mConstants.getShowInHeadsUp());
+ }
+
+ @Test
+ public void testShowInHeadsUpEnabled() {
+ overrideSetting("enabled=true,show_in_heads_up=true");
+ triggerConstantsOnChange();
+ assertTrue(mConstants.getShowInHeadsUp());
+ }
+
+ @Test
+ public void testShowInHeadsUpDisabled() {
+ overrideSetting("enabled=true,show_in_heads_up=false");
+ triggerConstantsOnChange();
+ assertFalse(mConstants.getShowInHeadsUp());
+ }
+
private void overrideSetting(String flags) {
Settings.Global.putString(mContext.getContentResolver(),
Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS, flags);
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 7f1fb6c..aa9bc26 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -313,6 +313,10 @@
Settings.Global.SHOW_HIDDEN_LAUNCHER_ICON_APPS_ENABLED, 1) == 0) {
return launcherActivities;
}
+ if (launcherActivities == null) {
+ // Cannot access profile, so we don't even return any hidden apps.
+ return null;
+ }
final int callingUid = injectBinderCallingUid();
final ArrayList<ResolveInfo> result = new ArrayList<>(launcherActivities.getList());
diff --git a/services/core/java/com/android/server/rollback/PackageRollbackData.java b/services/core/java/com/android/server/rollback/RollbackData.java
similarity index 65%
rename from services/core/java/com/android/server/rollback/PackageRollbackData.java
rename to services/core/java/com/android/server/rollback/RollbackData.java
index 15d1242..f0589aa 100644
--- a/services/core/java/com/android/server/rollback/PackageRollbackData.java
+++ b/services/core/java/com/android/server/rollback/RollbackData.java
@@ -20,17 +20,21 @@
import java.io.File;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
/**
- * Information about a rollback available for a particular package.
- * This is similar to {@link PackageRollbackInfo}, but extended with
- * additional information for internal bookkeeping.
+ * Information about a rollback available for a set of atomically installed
+ * packages.
*/
-class PackageRollbackData {
- public final PackageRollbackInfo info;
+class RollbackData {
+ /**
+ * The per-package rollback information.
+ */
+ public final List<PackageRollbackInfo> packages = new ArrayList<>();
/**
- * The directory where the apk backup is stored.
+ * The directory where the rollback data is stored.
*/
public final File backupDir;
@@ -38,12 +42,9 @@
* The time when the upgrade occurred, for purposes of expiring
* rollback data.
*/
- public final Instant timestamp;
+ public Instant timestamp;
- PackageRollbackData(PackageRollbackInfo info,
- File backupDir, Instant timestamp) {
- this.info = info;
+ RollbackData(File backupDir) {
this.backupDir = backupDir;
- this.timestamp = timestamp;
}
}
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 0c21312..8021265 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -63,9 +63,11 @@
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -86,10 +88,20 @@
// mLock is held when they are called.
private final Object mLock = new Object();
+ // Package rollback data for rollback-enabled installs that have not yet
+ // been committed. Maps from sessionId to rollback data.
+ @GuardedBy("mLock")
+ private final Map<Integer, RollbackData> mPendingRollbacks = new HashMap<>();
+
+ // Map from child session id's for enabled rollbacks to their
+ // corresponding parent session ids.
+ @GuardedBy("mLock")
+ private final Map<Integer, Integer> mChildSessions = new HashMap<>();
+
// 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;
+ private List<RollbackData> mAvailableRollbacks;
// The list of recently executed rollbacks.
// This list is null until the rollback data has been loaded.
@@ -102,14 +114,25 @@
// to store this data:
// /data/rollback/
// available/
- // com.package.A-XXX/
- // base.apk
- // rollback.json
- // com.package.B-YYY/
- // base.apk
- // rollback.json
+ // XXX/
+ // com.package.A/
+ // base.apk
+ // info.json
+ // enabled.txt
+ // YYY/
+ // com.package.B/
+ // base.apk
+ // info.json
+ // enabled.txt
// recently_executed.json
- // TODO: Use AtomicFile for rollback.json and recently_executed.json.
+ //
+ // * XXX, YYY are random strings from Files.createTempDirectory
+ // * info.json contains the package version to roll back from/to.
+ // * enabled.txt contains a timestamp for when the rollback was first
+ // made available. This file is not written until the rollback is made
+ // available.
+ //
+ // TODO: Use AtomicFile for all the .json files?
private final File mRollbackDataDir;
private final File mAvailableRollbacksDir;
private final File mRecentlyExecutedRollbacksFile;
@@ -135,6 +158,9 @@
// expiration.
getHandler().post(() -> ensureRollbackDataLoaded());
+ PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
+ installer.registerSessionCallback(new SessionCallback(), getHandler());
+
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
@@ -199,27 +225,23 @@
android.Manifest.permission.MANAGE_ROLLBACKS,
"getAvailableRollback");
- PackageRollbackInfo.PackageVersion installedVersion =
- getInstalledPackageVersion(packageName);
- if (installedVersion == null) {
+ RollbackData data = getRollbackForPackage(packageName);
+ if (data == 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.packageName.equals(packageName)
- && 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);
- }
+ // Note: The rollback for the package ought to be for the currently
+ // installed version, otherwise the rollback data is out of date. In
+ // that rare case, we'll check when we execute the rollback whether
+ // it's out of date or not, so no need to check package versions here.
+
+ for (PackageRollbackInfo info : data.packages) {
+ if (info.packageName.equals(packageName)) {
+ // TODO: Once the RollbackInfo API supports info about
+ // dependant packages, add that info here.
+ return new RollbackInfo(info);
}
}
-
return null;
}
@@ -229,16 +251,14 @@
android.Manifest.permission.MANAGE_ROLLBACKS,
"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);
+ RollbackData data = mAvailableRollbacks.get(i);
+ for (PackageRollbackInfo info : data.packages) {
+ packageNames.add(info.packageName);
+ }
}
}
return new StringParceledListSlice(new ArrayList<>(packageNames));
@@ -279,47 +299,49 @@
*/
private void executeRollbackInternal(RollbackInfo rollback,
String callerPackageName, IntentSender statusReceiver) {
- String packageName = rollback.targetPackage.packageName;
- Log.i(TAG, "Initiating rollback of " + packageName);
+ String targetPackageName = rollback.targetPackage.packageName;
+ Log.i(TAG, "Initiating rollback of " + targetPackageName);
- PackageRollbackInfo.PackageVersion installedVersion =
- getInstalledPackageVersion(packageName);
- if (installedVersion == null) {
- // TODO: Test this case
- sendFailure(statusReceiver, "Target package to roll back is not installed");
+ // Get the latest RollbackData for the target package.
+ RollbackData data = getRollbackForPackage(targetPackageName);
+ if (data == null) {
+ sendFailure(statusReceiver, "No rollback available for package.");
return;
}
- if (!rollback.targetPackage.higherVersion.equals(installedVersion)) {
- // TODO: Test this case
- sendFailure(statusReceiver, "Target package version to roll back not installed.");
- return;
+ // Verify the latest rollback matches the version requested.
+ // TODO: Check dependant packages too once RollbackInfo includes that
+ // information.
+ for (PackageRollbackInfo info : data.packages) {
+ if (info.packageName.equals(targetPackageName)
+ && !rollback.targetPackage.higherVersion.equals(info.higherVersion)) {
+ sendFailure(statusReceiver, "Rollback is out of date.");
+ return;
+ }
}
+ // Verify the RollbackData is up to date with what's installed on
+ // device.
// TODO: We assume that between now and the time we commit the
// 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.packageName.equals(packageName)
- && available.info.higherVersion.equals(installedVersion)) {
- data = available;
- break;
- }
+ for (PackageRollbackInfo info : data.packages) {
+ PackageRollbackInfo.PackageVersion installedVersion =
+ getInstalledPackageVersion(info.packageName);
+ if (installedVersion == null) {
+ // TODO: Test this case
+ sendFailure(statusReceiver, "Package to roll back is not installed");
+ return;
}
- }
- if (data == null) {
- sendFailure(statusReceiver, "Rollback not available");
- return;
+ if (!info.higherVersion.equals(installedVersion)) {
+ // TODO: Test this case
+ sendFailure(statusReceiver, "Package version to roll back not installed.");
+ return;
+ }
}
// Get a context for the caller to use to install the downgraded
@@ -334,29 +356,39 @@
PackageManager pm = context.getPackageManager();
try {
- PackageInstaller.Session session = null;
-
PackageInstaller packageInstaller = pm.getPackageInstaller();
- PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams parentParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
- params.setAllowDowngrade(true);
- int sessionId = packageInstaller.createSession(params);
- session = packageInstaller.openSession(sessionId);
+ parentParams.setAllowDowngrade(true);
+ parentParams.setMultiPackage();
+ int parentSessionId = packageInstaller.createSession(parentParams);
+ PackageInstaller.Session parentSession = packageInstaller.openSession(parentSessionId);
- // 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);
+ for (PackageRollbackInfo info : data.packages) {
+ PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ params.setAllowDowngrade(true);
+ int sessionId = packageInstaller.createSession(params);
+ PackageInstaller.Session session = packageInstaller.openSession(sessionId);
+
+ // TODO: Will it always be called "base.apk"? What about splits?
+ // What about apex?
+ File packageDir = new File(data.backupDir, info.packageName);
+ File baseApk = new File(packageDir, "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);
+ }
}
+ parentSession.addChildSessionId(sessionId);
}
final LocalIntentReceiver receiver = new LocalIntentReceiver();
- session.commit(receiver.getIntentSender());
+ parentSession.commit(receiver.getIntentSender());
Intent result = receiver.getResult();
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
@@ -371,13 +403,14 @@
sendSuccess(statusReceiver);
Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
- Uri.fromParts("package", packageName, Manifest.permission.MANAGE_ROLLBACKS));
+ Uri.fromParts("package", targetPackageName,
+ Manifest.permission.MANAGE_ROLLBACKS));
// 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);
+ Log.e(TAG, "Unable to roll back " + targetPackageName, e);
sendFailure(statusReceiver, "IOException: " + e.toString());
return;
}
@@ -408,12 +441,15 @@
// testing anyway.
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
- Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
while (iter.hasNext()) {
- PackageRollbackData data = iter.next();
- if (data.info.packageName.equals(packageName)) {
- iter.remove();
- removeFile(data.backupDir);
+ RollbackData data = iter.next();
+ for (PackageRollbackInfo info : data.packages) {
+ if (info.packageName.equals(packageName)) {
+ iter.remove();
+ removeFile(data.backupDir);
+ break;
+ }
}
}
}
@@ -453,27 +489,41 @@
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.
+ File enabledFile = new File(rollbackDir, "enabled.txt");
+ // TODO: Delete any directories without an enabled.txt? That could
+ // potentially delete pending rollback data if reloadPersistedData
+ // is called, though there's no reason besides testing for that to
+ // be called.
+ if (rollbackDir.isDirectory() && enabledFile.isFile()) {
+ RollbackData data = new RollbackData(rollbackDir);
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);
+ PackageRollbackInfo info = null;
+ for (File packageDir : rollbackDir.listFiles()) {
+ if (packageDir.isDirectory()) {
+ File jsonFile = new File(packageDir, "info.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");
+
+ data.packages.add(new PackageRollbackInfo(packageName,
+ new PackageRollbackInfo.PackageVersion(higherVersionCode),
+ new PackageRollbackInfo.PackageVersion(lowerVersionCode)));
+ }
+ }
+
+ if (data.packages.isEmpty()) {
+ throw new IOException("No package rollback info found");
+ }
+
+ String enabledString = IoUtils.readFileAsString(enabledFile.getAbsolutePath());
+ data.timestamp = Instant.parse(enabledString.trim());
mAvailableRollbacks.add(data);
} catch (IOException | JSONException | DateTimeParseException e) {
Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
+ removeFile(rollbackDir);
}
}
}
@@ -521,13 +571,16 @@
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
- Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ Iterator<RollbackData> 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);
+ RollbackData data = iter.next();
+ for (PackageRollbackInfo info : data.packages) {
+ if (info.packageName.equals(packageName)
+ && !info.higherVersion.equals(installedVersion)) {
+ iter.remove();
+ removeFile(data.backupDir);
+ break;
+ }
}
}
}
@@ -643,9 +696,9 @@
synchronized (mLock) {
ensureRollbackDataLoadedLocked();
- Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
+ Iterator<RollbackData> iter = mAvailableRollbacks.iterator();
while (iter.hasNext()) {
- PackageRollbackData data = iter.next();
+ RollbackData data = iter.next();
if (!now.isBefore(data.timestamp.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS))) {
iter.remove();
removeFile(data.backupDir);
@@ -673,6 +726,23 @@
return mHandlerThread.getThreadHandler();
}
+ // Returns true if <code>session</code> has installFlags and code path
+ // matching the installFlags and new package code path given to
+ // enableRollback.
+ private boolean sessionMatchesForEnableRollback(PackageInstaller.SessionInfo session,
+ int installFlags, File newPackageCodePath) {
+ if (session == null || session.resolvedBaseCodePath == null) {
+ return false;
+ }
+
+ File packageCodePath = new File(session.resolvedBaseCodePath).getParentFile();
+ if (newPackageCodePath.equals(packageCodePath) && installFlags == session.installFlags) {
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Called via broadcast by the package manager when a package is being
* staged for install with rollback enabled. Called before the package has
@@ -705,6 +775,34 @@
String packageName = newPackage.packageName;
Log.i(TAG, "Enabling rollback for install of " + packageName);
+ // Figure out the session id associated with this install.
+ int parentSessionId = PackageInstaller.SessionInfo.INVALID_ID;
+ int childSessionId = PackageInstaller.SessionInfo.INVALID_ID;
+ PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
+ for (PackageInstaller.SessionInfo info : installer.getAllSessions()) {
+ if (info.isMultiPackage()) {
+ for (int childId : info.getChildSessionIds()) {
+ PackageInstaller.SessionInfo child = installer.getSessionInfo(childId);
+ if (sessionMatchesForEnableRollback(child, installFlags, newPackageCodePath)) {
+ // TODO: Check we only have one matching session?
+ parentSessionId = info.getSessionId();
+ childSessionId = childId;
+ }
+ }
+ } else {
+ if (sessionMatchesForEnableRollback(info, installFlags, newPackageCodePath)) {
+ // TODO: Check we only have one matching session?
+ parentSessionId = info.getSessionId();
+ childSessionId = parentSessionId;
+ }
+ }
+ }
+
+ if (parentSessionId == PackageInstaller.SessionInfo.INVALID_ID) {
+ Log.e(TAG, "Unable to find session id for enabled rollback.");
+ return false;
+ }
+
PackageRollbackInfo.PackageVersion newVersion =
new PackageRollbackInfo.PackageVersion(newPackage.versionCode);
@@ -720,52 +818,53 @@
PackageRollbackInfo.PackageVersion installedVersion =
new PackageRollbackInfo.PackageVersion(installedPackage.getLongVersionCode());
- File backupDir;
+ PackageRollbackInfo info = new PackageRollbackInfo(
+ packageName, newVersion, installedVersion);
+
+ RollbackData data;
try {
- backupDir = Files.createTempDirectory(
- mAvailableRollbacksDir.toPath(), packageName + "-").toFile();
+ synchronized (mLock) {
+ mChildSessions.put(childSessionId, parentSessionId);
+ data = mPendingRollbacks.get(parentSessionId);
+ if (data == null) {
+ File backupDir = Files.createTempDirectory(
+ mAvailableRollbacksDir.toPath(), null).toFile();
+ data = new RollbackData(backupDir);
+ mPendingRollbacks.put(parentSessionId, data);
+ }
+ data.packages.add(info);
+ }
} 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();
+ File packageDir = new File(data.backupDir, packageName);
+ packageDir.mkdirs();
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");
+ File jsonFile = new File(packageDir, "info.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);
+ removeFile(packageDir);
return false;
}
// TODO: Copy by hard link instead to save on cpu and storage space?
- int status = PackageManagerServiceUtils.copyPackage(installedPackage.codePath, backupDir);
+ int status = PackageManagerServiceUtils.copyPackage(installedPackage.codePath, packageDir);
if (status != PackageManager.INSTALL_SUCCEEDED) {
Log.e(TAG, "Unable to copy package for rollback for " + packageName);
- removeFile(backupDir);
+ removeFile(packageDir);
return false;
}
- PackageRollbackData data = new PackageRollbackData(
- new PackageRollbackInfo(packageName, newVersion, installedVersion),
- backupDir, timestamp);
-
- synchronized (mLock) {
- ensureRollbackDataLoadedLocked();
- mAvailableRollbacks.add(data);
- }
-
return true;
}
@@ -829,4 +928,87 @@
return new PackageRollbackInfo.PackageVersion(pkgInfo.getLongVersionCode());
}
+
+ private class SessionCallback extends PackageInstaller.SessionCallback {
+
+ @Override
+ public void onCreated(int sessionId) { }
+
+ @Override
+ public void onBadgingChanged(int sessionId) { }
+
+ @Override
+ public void onActiveChanged(int sessionId, boolean active) { }
+
+ @Override
+ public void onProgressChanged(int sessionId, float progress) { }
+
+ @Override
+ public void onFinished(int sessionId, boolean success) {
+ RollbackData data = null;
+ synchronized (mLock) {
+ Integer parentSessionId = mChildSessions.remove(sessionId);
+ if (parentSessionId != null) {
+ sessionId = parentSessionId;
+ }
+ data = mPendingRollbacks.remove(sessionId);
+ }
+
+ if (data != null) {
+ if (success) {
+ try {
+ data.timestamp = Instant.now();
+ File enabledFile = new File(data.backupDir, "enabled.txt");
+ PrintWriter pw = new PrintWriter(enabledFile);
+ pw.println(data.timestamp.toString());
+ pw.close();
+
+ synchronized (mLock) {
+ // Note: There is a small window of time between when
+ // the session has been committed by the package
+ // manager and when we make the rollback available
+ // here. Presumably the window is small enough that
+ // nobody will want to roll back the newly installed
+ // package before we make the rollback available.
+ // TODO: We'll lose the rollback data if the
+ // device reboots between when the session is
+ // committed and this point. Revisit this after
+ // adding support for rollback of staged installs.
+ ensureRollbackDataLoadedLocked();
+ mAvailableRollbacks.add(data);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to enable rollback", e);
+ removeFile(data.backupDir);
+ }
+ } else {
+ // The install session was aborted, clean up the pending
+ // install.
+ removeFile(data.backupDir);
+ }
+ }
+ }
+ }
+
+ /*
+ * Returns the RollbackData, if any, for an available rollback that would
+ * roll back the given package. Note: This assumes we have at most one
+ * available rollback for a given package at any one time.
+ */
+ private RollbackData getRollbackForPackage(String packageName) {
+ synchronized (mLock) {
+ // TODO: Have ensureRollbackDataLoadedLocked return the list of
+ // available rollbacks, to hopefully avoid forgetting to call it?
+ ensureRollbackDataLoadedLocked();
+ for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
+ RollbackData data = mAvailableRollbacks.get(i);
+ for (PackageRollbackInfo info : data.packages) {
+ if (info.packageName.equals(packageName)) {
+ return data;
+ }
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
index 0ccfb19..77cd9d8 100644
--- a/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
+++ b/tests/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -108,6 +108,10 @@
}
// The app should not be available for rollback.
+ // TODO: See if there is a way to remove this race condition
+ // between when the app is uninstalled and when the previously
+ // available rollback, if any, is removed.
+ Thread.sleep(1000);
assertNull(rm.getAvailableRollback(TEST_APP_A));
assertFalse(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
@@ -125,6 +129,10 @@
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
+ // TODO: See if there is a way to remove this race condition
+ // between when the app is installed and when the rollback
+ // is made available.
+ Thread.sleep(1000);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollback);
@@ -182,6 +190,8 @@
RollbackManager rm = RollbackTestUtils.getRollbackManager();
+ // TODO: Test this with multi-package rollback, not just single
+ // package rollback.
// Prep installation of TEST_APP_A
RollbackTestUtils.uninstall(TEST_APP_A);
RollbackTestUtils.install("RollbackTestAppAv1.apk", false);
@@ -189,6 +199,10 @@
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
+ // TODO: See if there is a way to remove this race condition
+ // between when the app is installed and when the rollback
+ // is made available.
+ Thread.sleep(1000);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollback);
@@ -263,6 +277,10 @@
assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A));
// The app should now be available for rollback.
+ // TODO: See if there is a way to remove this race condition
+ // between when the app is installed and when the rollback
+ // is made available.
+ Thread.sleep(1000);
assertTrue(rm.getPackagesWithAvailableRollbacks().contains(TEST_APP_A));
RollbackInfo rollback = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollback);
@@ -405,6 +423,10 @@
// Both test apps should now be available for rollback, and the
// targetPackage returned for rollback should be correct.
+ // TODO: See if there is a way to remove this race condition
+ // between when the app is installed and when the rollback
+ // is made available.
+ Thread.sleep(1000);
RollbackInfo rollbackA = rm.getAvailableRollback(TEST_APP_A);
assertNotNull(rollbackA);
assertEquals(TEST_APP_A, rollbackA.targetPackage.packageName);
@@ -491,7 +513,7 @@
* TODO: Stop ignoring this test once support for multi-package rollback
* is implemented.
*/
- @Ignore @Test
+ @Test
public void testMultiPackage() throws Exception {
try {
RollbackTestUtils.adoptShellPermissionIdentity(