summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/BackupEncryption/proto/key_value_listing.proto40
-rw-r--r--packages/BackupEncryption/proto/key_value_pair.proto31
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java111
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java77
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java8
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java164
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java110
-rw-r--r--services/core/java/com/android/server/rollback/AppDataRollbackHelper.java178
-rw-r--r--services/core/java/com/android/server/rollback/Rollback.java121
-rw-r--r--services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java512
-rw-r--r--services/core/java/com/android/server/rollback/RollbackStore.java3
-rw-r--r--services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java14
-rw-r--r--services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java65
13 files changed, 1047 insertions, 387 deletions
diff --git a/packages/BackupEncryption/proto/key_value_listing.proto b/packages/BackupEncryption/proto/key_value_listing.proto
new file mode 100644
index 000000000000..001e697bd804
--- /dev/null
+++ b/packages/BackupEncryption/proto/key_value_listing.proto
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 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
+ */
+
+syntax = "proto2";
+
+package android_backup_crypto;
+
+option java_package = "com.android.server.backup.encryption.protos";
+option java_outer_classname = "KeyValueListingProto";
+
+// An entry of a key-value pair.
+message KeyValueEntry {
+ // Plaintext key of the key-value pair.
+ optional string key = 1;
+ // SHA-256 MAC of the plaintext of the chunk containing the pair
+ optional bytes hash = 2;
+}
+
+// Describes the key/value pairs currently in the backup blob, mapping from the
+// plaintext key to the hash of the chunk containing the pair.
+//
+// This is local state stored on the device. It is never sent to the
+// backup server. See ChunkOrdering for how the device restores the
+// key-value pairs in the correct order.
+message KeyValueListing {
+ repeated KeyValueEntry entries = 1;
+}
diff --git a/packages/BackupEncryption/proto/key_value_pair.proto b/packages/BackupEncryption/proto/key_value_pair.proto
new file mode 100644
index 000000000000..177fa3025dc8
--- /dev/null
+++ b/packages/BackupEncryption/proto/key_value_pair.proto
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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
+ */
+
+syntax = "proto2";
+
+package android_backup_crypto;
+
+option java_package = "com.android.server.backup.encryption.protos";
+option java_outer_classname = "KeyValuePairProto";
+
+// Serialized form of a key-value pair, when it is to be encrypted in a blob.
+// The backup blob for a key-value database consists of repeated encrypted
+// key-value pairs like this, in a randomized order. See ChunkOrdering for how
+// these are then reconstructed during a restore.
+message KeyValuePair {
+ optional string key = 1;
+ optional bytes value = 2;
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
new file mode 100644
index 000000000000..56e1c053d8e3
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutput.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 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.backup.encryption.kv;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Builds a key value backup set from plaintext chunks. Computes a digest over the sorted SHA-256
+ * hashes of the chunks.
+ */
+public class DecryptedChunkKvOutput implements DecryptedChunkOutput {
+ @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
+
+ private final ChunkHasher mChunkHasher;
+ private final List<KeyValuePairProto.KeyValuePair> mUnsortedPairs = new ArrayList<>();
+ private final List<ChunkHash> mUnsortedHashes = new ArrayList<>();
+ private boolean mClosed;
+
+ /** Constructs a new instance which computers the digest using the given hasher. */
+ public DecryptedChunkKvOutput(ChunkHasher chunkHasher) {
+ mChunkHasher = chunkHasher;
+ }
+
+ @Override
+ public DecryptedChunkOutput open() {
+ // As we don't have any resources there is nothing to open.
+ return this;
+ }
+
+ @Override
+ public void processChunk(byte[] plaintextBuffer, int length)
+ throws IOException, InvalidKeyException {
+ checkState(!mClosed, "Cannot process chunk after close()");
+ KeyValuePairProto.KeyValuePair kvPair = new KeyValuePairProto.KeyValuePair();
+ KeyValuePairProto.KeyValuePair.mergeFrom(kvPair, plaintextBuffer, 0, length);
+ mUnsortedPairs.add(kvPair);
+ // TODO(b/71492289): Update ChunkHasher to accept offset and length so we don't have to copy
+ // the buffer into a smaller array.
+ mUnsortedHashes.add(mChunkHasher.computeHash(Arrays.copyOf(plaintextBuffer, length)));
+ }
+
+ @Override
+ public void close() {
+ // As we don't have any resources there is nothing to close.
+ mClosed = true;
+ }
+
+ @Override
+ public byte[] getDigest() throws NoSuchAlgorithmException {
+ checkState(mClosed, "Must close() before getDigest()");
+ MessageDigest digest = getMessageDigest();
+ Collections.sort(mUnsortedHashes);
+ for (ChunkHash hash : mUnsortedHashes) {
+ digest.update(hash.getHash());
+ }
+ return digest.digest();
+ }
+
+ private static MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(DIGEST_ALGORITHM);
+ }
+
+ /**
+ * Returns the key value pairs from the backup, sorted lexicographically by key.
+ *
+ * <p>You must call {@link #close} first.
+ */
+ public List<KeyValuePairProto.KeyValuePair> getPairs() {
+ checkState(mClosed, "Must close() before getPairs()");
+ Collections.sort(
+ mUnsortedPairs,
+ new Comparator<KeyValuePairProto.KeyValuePair>() {
+ @Override
+ public int compare(
+ KeyValuePairProto.KeyValuePair o1, KeyValuePairProto.KeyValuePair o2) {
+ return o1.key.compareTo(o2.key);
+ }
+ });
+ return mUnsortedPairs;
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
new file mode 100644
index 000000000000..b3518e144ce3
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/kv/KeyValueListingBuilder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.backup.encryption.kv;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Builds a {@link KeyValueListingProto.KeyValueListing}, which is a nano proto and so has no
+ * builder.
+ */
+public class KeyValueListingBuilder {
+ private final List<KeyValueListingProto.KeyValueEntry> mEntries = new ArrayList<>();
+
+ /** Adds a new pair entry to the listing. */
+ public KeyValueListingBuilder addPair(String key, ChunkHash hash) {
+ checkArgument(key.length() != 0, "Key must have non-zero length");
+ checkNotNull(hash, "Hash must not be null");
+
+ KeyValueListingProto.KeyValueEntry entry = new KeyValueListingProto.KeyValueEntry();
+ entry.key = key;
+ entry.hash = hash.getHash();
+ mEntries.add(entry);
+
+ return this;
+ }
+
+ /** Adds all pairs contained in a map, where the map is from key to hash. */
+ public KeyValueListingBuilder addAll(Map<String, ChunkHash> map) {
+ for (Entry<String, ChunkHash> entry : map.entrySet()) {
+ addPair(entry.getKey(), entry.getValue());
+ }
+
+ return this;
+ }
+
+ /** Returns a new listing containing all the pairs added so far. */
+ public KeyValueListingProto.KeyValueListing build() {
+ if (mEntries.size() == 0) {
+ return emptyListing();
+ }
+
+ KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
+ listing.entries = new KeyValueListingProto.KeyValueEntry[mEntries.size()];
+ mEntries.toArray(listing.entries);
+ return listing;
+ }
+
+ /** Returns a new listing which does not contain any pairs. */
+ public static KeyValueListingProto.KeyValueListing emptyListing() {
+ KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
+ listing.entries = KeyValueListingProto.KeyValueEntry.emptyArray();
+ return listing;
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
index e3df3c1eb96f..f67f1007f632 100644
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
@@ -19,6 +19,7 @@ package com.android.server.backup.encryption.tasks;
import java.io.Closeable;
import java.io.IOException;
import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
/**
* Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
@@ -30,7 +31,7 @@ public interface DecryptedChunkOutput extends Closeable {
*
* @return {@code this}, to allow use with try-with-resources
*/
- DecryptedChunkOutput open() throws IOException;
+ DecryptedChunkOutput open() throws IOException, NoSuchAlgorithmException;
/**
* Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
@@ -43,12 +44,13 @@ public interface DecryptedChunkOutput extends Closeable {
* at index 0.
* @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
*/
- void processChunk(byte[] plaintextBuffer, int length) throws IOException, InvalidKeyException;
+ void processChunk(byte[] plaintextBuffer, int length)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException;
/**
* Returns the message digest of all the chunks processed by {@link #processChunk}.
*
* <p>You must call {@link Closeable#close()} before calling this method.
*/
- byte[] getDigest();
+ byte[] getDigest() throws NoSuchAlgorithmException;
}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
new file mode 100644
index 000000000000..215e1cbc725e
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/DecryptedChunkKvOutputTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 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.backup.encryption.kv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Debug;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class DecryptedChunkKvOutputTest {
+ private static final String TEST_KEY_1 = "key_1";
+ private static final String TEST_KEY_2 = "key_2";
+ private static final byte[] TEST_VALUE_1 = {1, 2, 3};
+ private static final byte[] TEST_VALUE_2 = {10, 11, 12, 13};
+ private static final byte[] TEST_PAIR_1 = toByteArray(createPair(TEST_KEY_1, TEST_VALUE_1));
+ private static final byte[] TEST_PAIR_2 = toByteArray(createPair(TEST_KEY_2, TEST_VALUE_2));
+ private static final int TEST_BUFFER_SIZE = Math.max(TEST_PAIR_1.length, TEST_PAIR_2.length);
+
+ @Mock private ChunkHasher mChunkHasher;
+ private DecryptedChunkKvOutput mOutput;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mChunkHasher.computeHash(any()))
+ .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
+ mOutput = new DecryptedChunkKvOutput(mChunkHasher);
+ }
+
+ @Test
+ public void open_returnsInstance() throws Exception {
+ assertThat(mOutput.open()).isEqualTo(mOutput);
+ }
+
+ @Test
+ public void processChunk_alreadyClosed_throws() throws Exception {
+ mOutput.open();
+ mOutput.close();
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> mOutput.processChunk(TEST_PAIR_1, TEST_PAIR_1.length));
+ }
+
+ @Test
+ public void getDigest_beforeClose_throws() throws Exception {
+ // TODO: b/141356823 We should add a test which calls .open() here
+ assertThrows(IllegalStateException.class, () -> mOutput.getDigest());
+ }
+
+ @Test
+ public void getDigest_returnsDigestOfSortedHashes() throws Exception {
+ mOutput.open();
+ Debug.waitForDebugger();
+ mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
+ mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
+ mOutput.close();
+
+ byte[] actualDigest = mOutput.getDigest();
+
+ MessageDigest digest = MessageDigest.getInstance(DecryptedChunkKvOutput.DIGEST_ALGORITHM);
+ Stream.of(TEST_PAIR_1, TEST_PAIR_2)
+ .map(DecryptedChunkKvOutputTest::fakeHash)
+ .sorted(Comparator.naturalOrder())
+ .forEachOrdered(hash -> digest.update(hash.getHash()));
+ assertThat(actualDigest).isEqualTo(digest.digest());
+ }
+
+ @Test
+ public void getPairs_beforeClose_throws() throws Exception {
+ // TODO: b/141356823 We should add a test which calls .open() here
+ assertThrows(IllegalStateException.class, () -> mOutput.getPairs());
+ }
+
+ @Test
+ public void getPairs_returnsPairsSortedByKey() throws Exception {
+ mOutput.open();
+ // Write out of order to check that it sorts the chunks.
+ mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
+ mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
+ mOutput.close();
+
+ List<KeyValuePairProto.KeyValuePair> pairs = mOutput.getPairs();
+
+ assertThat(
+ isInOrder(
+ pairs,
+ Comparator.comparing(
+ (KeyValuePairProto.KeyValuePair pair) -> pair.key)))
+ .isTrue();
+ assertThat(pairs).hasSize(2);
+ assertThat(pairs.get(0).key).isEqualTo(TEST_KEY_1);
+ assertThat(pairs.get(0).value).isEqualTo(TEST_VALUE_1);
+ assertThat(pairs.get(1).key).isEqualTo(TEST_KEY_2);
+ assertThat(pairs.get(1).value).isEqualTo(TEST_VALUE_2);
+ }
+
+ private static KeyValuePairProto.KeyValuePair createPair(String key, byte[] value) {
+ KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
+ pair.key = key;
+ pair.value = value;
+ return pair;
+ }
+
+ private boolean isInOrder(
+ List<KeyValuePairProto.KeyValuePair> list,
+ Comparator<KeyValuePairProto.KeyValuePair> comparator) {
+ if (list.size() < 2) {
+ return true;
+ }
+
+ List<KeyValuePairProto.KeyValuePair> sortedList = new ArrayList<>(list);
+ Collections.sort(sortedList, comparator);
+ return list.equals(sortedList);
+ }
+
+ private static byte[] toByteArray(KeyValuePairProto.KeyValuePair nano) {
+ return KeyValuePairProto.KeyValuePair.toByteArray(nano);
+ }
+
+ private static ChunkHash fakeHash(byte[] data) {
+ return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
new file mode 100644
index 000000000000..acc662860528
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/kv/KeyValueListingBuilderTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2019 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.backup.encryption.kv;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class KeyValueListingBuilderTest {
+ private static final String TEST_KEY_1 = "test_key_1";
+ private static final String TEST_KEY_2 = "test_key_2";
+ private static final ChunkHash TEST_HASH_1 =
+ new ChunkHash(Arrays.copyOf(new byte[] {1, 2}, ChunkHash.HASH_LENGTH_BYTES));
+ private static final ChunkHash TEST_HASH_2 =
+ new ChunkHash(Arrays.copyOf(new byte[] {5, 6}, ChunkHash.HASH_LENGTH_BYTES));
+
+ private KeyValueListingBuilder mBuilder;
+
+ @Before
+ public void setUp() {
+ mBuilder = new KeyValueListingBuilder();
+ }
+
+ @Test
+ public void addPair_nullKey_throws() {
+ assertThrows(NullPointerException.class, () -> mBuilder.addPair(null, TEST_HASH_1));
+ }
+
+ @Test
+ public void addPair_emptyKey_throws() {
+ assertThrows(IllegalArgumentException.class, () -> mBuilder.addPair("", TEST_HASH_1));
+ }
+
+ @Test
+ public void addPair_nullHash_throws() {
+ assertThrows(NullPointerException.class, () -> mBuilder.addPair(TEST_KEY_1, null));
+ }
+
+ @Test
+ public void build_noPairs_buildsEmptyListing() {
+ KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+ assertThat(listing.entries).isEmpty();
+ }
+
+ @Test
+ public void build_returnsCorrectListing() {
+ mBuilder.addPair(TEST_KEY_1, TEST_HASH_1);
+
+ KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+ assertThat(listing.entries.length).isEqualTo(1);
+ assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
+ assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+ }
+
+ @Test
+ public void addAll_addsAllPairsInMap() {
+ ImmutableMap<String, ChunkHash> pairs =
+ new ImmutableMap.Builder<String, ChunkHash>()
+ .put(TEST_KEY_1, TEST_HASH_1)
+ .put(TEST_KEY_2, TEST_HASH_2)
+ .build();
+
+ mBuilder.addAll(pairs);
+ KeyValueListingProto.KeyValueListing listing = mBuilder.build();
+
+ assertThat(listing.entries.length).isEqualTo(2);
+ assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
+ assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
+ assertThat(listing.entries[1].key).isEqualTo(TEST_KEY_2);
+ assertThat(listing.entries[1].hash).isEqualTo(TEST_HASH_2.getHash());
+ }
+
+ @Test
+ public void emptyListing_returnsListingWithoutAnyPairs() {
+ KeyValueListingProto.KeyValueListing emptyListing = KeyValueListingBuilder.emptyListing();
+ assertThat(emptyListing.entries).isEmpty();
+ }
+}
diff --git a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
index cae09ea37f2a..3f9cc83b53d8 100644
--- a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
+++ b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
@@ -23,17 +23,12 @@ import android.util.IntArray;
import android.util.Slog;
import android.util.SparseLongArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
/**
* Encapsulates the logic for initiating userdata snapshots and rollbacks via installd.
@@ -56,6 +51,8 @@ public class AppDataRollbackHelper {
* {@code userIds}. Updates said {@code packageRollbackInfo} with the inodes of the CE user data
* snapshot folders.
*/
+ @GuardedBy("rollback.getLock")
+ // TODO(b/136241838): Move into Rollback and synchronize there.
public void snapshotAppData(
int snapshotId, PackageRollbackInfo packageRollbackInfo, int[] userIds) {
for (int user : userIds) {
@@ -92,6 +89,8 @@ public class AppDataRollbackHelper {
* to {@code packageRollbackInfo} are restricted to the removal or addition of {@code
* userId} to the list of pending backups or restores.
*/
+ @GuardedBy("rollback.getLock")
+ // TODO(b/136241838): Move into Rollback and synchronize there.
public boolean restoreAppData(int rollbackId, PackageRollbackInfo packageRollbackInfo,
int userId, int appId, String seInfo) {
int storageFlags = Installer.FLAG_STORAGE_DE;
@@ -135,6 +134,8 @@ public class AppDataRollbackHelper {
* Deletes an app data snapshot with a given {@code rollbackId} for a specified package
* {@code packageName} for a given {@code user}.
*/
+ @GuardedBy("rollback.getLock")
+ // TODO(b/136241838): Move into Rollback and synchronize there.
public void destroyAppDataSnapshot(int rollbackId, PackageRollbackInfo packageRollbackInfo,
int user) {
int storageFlags = Installer.FLAG_STORAGE_DE;
@@ -156,141 +157,68 @@ public class AppDataRollbackHelper {
}
/**
- * Computes the list of pending backups for {@code userId} given lists of rollbacks.
- * Packages pending backup for the given user are added to {@code pendingBackupPackages} along
- * with their corresponding {@code PackageRollbackInfo}.
+ * Commits the pending backups and restores for a given {@code userId} and {@code rollback}. If
+ * the rollback has a pending backup, it is updated with a mapping from {@code userId} to inode
+ * of the CE user data snapshot.
*
- * @return the list of rollbacks that have pending backups. Note that some of the
- * backups won't be performed, because they might be counteracted by pending restores.
+ * @return true if any backups or restores were found for the userId
*/
- private static List<Rollback> computePendingBackups(int userId,
- Map<String, PackageRollbackInfo> pendingBackupPackages,
- List<Rollback> rollbacks) {
- List<Rollback> rollbacksWithPendingBackups = new ArrayList<>();
-
- for (Rollback rollback : rollbacks) {
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- final IntArray pendingBackupUsers = info.getPendingBackups();
- if (pendingBackupUsers != null) {
- final int idx = pendingBackupUsers.indexOf(userId);
- if (idx != -1) {
- pendingBackupPackages.put(info.getPackageName(), info);
- if (rollbacksWithPendingBackups.indexOf(rollback) == -1) {
- rollbacksWithPendingBackups.add(rollback);
- }
- }
+ @GuardedBy("rollback.getLock")
+ boolean commitPendingBackupAndRestoreForUser(int userId, Rollback rollback) {
+ boolean foundBackupOrRestore = false;
+ for (PackageRollbackInfo info : rollback.info.getPackages()) {
+ boolean hasPendingBackup = false;
+ boolean hasPendingRestore = false;
+ final IntArray pendingBackupUsers = info.getPendingBackups();
+ if (pendingBackupUsers != null) {
+ if (pendingBackupUsers.indexOf(userId) != -1) {
+ hasPendingBackup = true;
+ foundBackupOrRestore = true;
}
}
- }
- return rollbacksWithPendingBackups;
- }
-
- /**
- * Computes the list of pending restores for {@code userId} given lists of rollbacks.
- * Packages pending restore are added to {@code pendingRestores} along with their corresponding
- * {@code PackageRollbackInfo}.
- *
- * @return the list of rollbacks that have pending restores. Note that some of the
- * restores won't be performed, because they might be counteracted by pending backups.
- */
- private static List<Rollback> computePendingRestores(int userId,
- Map<String, PackageRollbackInfo> pendingRestorePackages,
- List<Rollback> rollbacks) {
- List<Rollback> rollbacksWithPendingRestores = new ArrayList<>();
- for (Rollback rollback : rollbacks) {
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- final RestoreInfo ri = info.getRestoreInfo(userId);
- if (ri != null) {
- pendingRestorePackages.put(info.getPackageName(), info);
- if (rollbacksWithPendingRestores.indexOf(rollback) == -1) {
- rollbacksWithPendingRestores.add(rollback);
- }
- }
+ RestoreInfo ri = info.getRestoreInfo(userId);
+ if (ri != null) {
+ hasPendingRestore = true;
+ foundBackupOrRestore = true;
}
- }
-
- return rollbacksWithPendingRestores;
- }
-
- /**
- * Commits the list of pending backups and restores for a given {@code userId}. For rollbacks
- * with pending backups, updates the {@code Rollback} instance with a mapping from
- * {@code userId} to inode of the CE user data snapshot.
- *
- * @return the set of rollbacks with changes that should be stored on disk.
- */
- public Set<Rollback> commitPendingBackupAndRestoreForUser(int userId,
- List<Rollback> rollbacks) {
- final Map<String, PackageRollbackInfo> pendingBackupPackages = new HashMap<>();
- final List<Rollback> pendingBackups = computePendingBackups(userId,
- pendingBackupPackages, rollbacks);
-
- final Map<String, PackageRollbackInfo> pendingRestorePackages = new HashMap<>();
- final List<Rollback> pendingRestores = computePendingRestores(userId,
- pendingRestorePackages, rollbacks);
-
- // First remove unnecessary backups, i.e. when user did not unlock their phone between the
- // request to backup data and the request to restore it.
- Iterator<Map.Entry<String, PackageRollbackInfo>> iter =
- pendingBackupPackages.entrySet().iterator();
- while (iter.hasNext()) {
- PackageRollbackInfo backupPackage = iter.next().getValue();
- PackageRollbackInfo restorePackage =
- pendingRestorePackages.get(backupPackage.getPackageName());
- if (restorePackage != null) {
- backupPackage.removePendingBackup(userId);
- backupPackage.removePendingRestoreInfo(userId);
- iter.remove();
- pendingRestorePackages.remove(backupPackage.getPackageName());
+ if (hasPendingBackup && hasPendingRestore) {
+ // Remove unnecessary backup, i.e. when user did not unlock their phone between the
+ // request to backup data and the request to restore it.
+ info.removePendingBackup(userId);
+ info.removePendingRestoreInfo(userId);
+ continue;
}
- }
- if (!pendingBackupPackages.isEmpty()) {
- for (Rollback rollback : pendingBackups) {
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- final IntArray pendingBackupUsers = info.getPendingBackups();
- final int idx = pendingBackupUsers.indexOf(userId);
- if (idx != -1) {
- try {
- long ceSnapshotInode = mInstaller.snapshotAppData(info.getPackageName(),
- userId, rollback.info.getRollbackId(),
- Installer.FLAG_STORAGE_CE);
- info.putCeSnapshotInode(userId, ceSnapshotInode);
- pendingBackupUsers.remove(idx);
- } catch (InstallerException ie) {
- Slog.e(TAG,
- "Unable to create app data snapshot for: "
+ if (hasPendingBackup) {
+ int idx = pendingBackupUsers.indexOf(userId);
+ try {
+ long ceSnapshotInode = mInstaller.snapshotAppData(info.getPackageName(),
+ userId, rollback.info.getRollbackId(),
+ Installer.FLAG_STORAGE_CE);
+ info.putCeSnapshotInode(userId, ceSnapshotInode);
+ pendingBackupUsers.remove(idx);
+ } catch (InstallerException ie) {
+ Slog.e(TAG,
+ "Unable to create app data snapshot for: "
+ info.getPackageName() + ", userId: " + userId, ie);
- }
- }
}
}
- }
- if (!pendingRestorePackages.isEmpty()) {
- for (Rollback rollback : pendingRestores) {
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- final RestoreInfo ri = info.getRestoreInfo(userId);
- if (ri != null) {
- try {
- mInstaller.restoreAppDataSnapshot(info.getPackageName(), ri.appId,
- ri.seInfo, userId, rollback.info.getRollbackId(),
- Installer.FLAG_STORAGE_CE);
- info.removeRestoreInfo(ri);
- } catch (InstallerException ie) {
- Slog.e(TAG, "Unable to restore app data snapshot for: "
- + info.getPackageName(), ie);
- }
- }
+ if (hasPendingRestore) {
+ try {
+ mInstaller.restoreAppDataSnapshot(info.getPackageName(), ri.appId,
+ ri.seInfo, userId, rollback.info.getRollbackId(),
+ Installer.FLAG_STORAGE_CE);
+ info.removeRestoreInfo(ri);
+ } catch (InstallerException ie) {
+ Slog.e(TAG, "Unable to restore app data snapshot for: "
+ + info.getPackageName(), ie);
}
}
}
-
- final Set<Rollback> changed = new HashSet<>(pendingBackups);
- changed.addAll(pendingRestores);
- return changed;
+ return foundBackupOrRestore;
}
/**
diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java
index 6769fe07bbf8..2dc495197254 100644
--- a/services/core/java/com/android/server/rollback/Rollback.java
+++ b/services/core/java/com/android/server/rollback/Rollback.java
@@ -18,19 +18,27 @@ package com.android.server.rollback;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.RollbackInfo;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.List;
/**
- * Information about a rollback available for a set of atomically installed
- * packages.
+ * Information about a rollback available for a set of atomically installed packages.
+ *
+ * <p>When accessing the state of a Rollback object, the caller is responsible for synchronization.
+ * The lock object provided by {@link #getLock} should be acquired when accessing any of the mutable
+ * state of a Rollback, including from the {@link RollbackInfo} and any of the
+ * {@link PackageRollbackInfo} objects held within.
*/
class Rollback {
@IntDef(flag = true, prefix = { "ROLLBACK_STATE_" }, value = {
@@ -58,8 +66,18 @@ class Rollback {
static final int ROLLBACK_STATE_COMMITTED = 3;
/**
+ * The session ID for the staged session if this rollback data represents a staged session,
+ * {@code -1} otherwise.
+ */
+ private final int mStagedSessionId;
+
+ /**
* The rollback info for this rollback.
+ *
+ * <p>Any access to this field that touches any mutable state should be synchronized on
+ * {@link #getLock}.
*/
+ @GuardedBy("getLock")
public final RollbackInfo info;
/**
@@ -74,23 +92,20 @@ class Rollback {
* The timestamp is not applicable for all rollback states, but we make
* sure to keep it non-null to avoid potential errors there.
*/
+ @GuardedBy("mLock")
private @NonNull Instant mTimestamp;
/**
- * The session ID for the staged session if this rollback data represents a staged session,
- * {@code -1} otherwise.
- */
- private final int mStagedSessionId;
-
- /**
* The current state of the rollback.
* ENABLING, AVAILABLE, or COMMITTED.
*/
+ @GuardedBy("mLock")
private @RollbackState int mState;
/**
* The id of the post-reboot apk session for a staged install, if any.
*/
+ @GuardedBy("mLock")
private int mApkSessionId = -1;
/**
@@ -98,10 +113,17 @@ class Rollback {
* for this rollback because it has just been committed but the rollback
* has not yet been fully applied.
*/
- // NOTE: All accesses to this field are from the RollbackManager handler thread.
+ @GuardedBy("mLock")
private boolean mRestoreUserDataInProgress = false;
/**
+ * Lock object to guard all access to Rollback state.
+ *
+ * @see #getLock
+ */
+ private final Object mLock = new Object();
+
+ /**
* Constructs a new, empty Rollback instance.
*
* @param rollbackId the id of the rollback.
@@ -135,8 +157,23 @@ class Rollback {
}
/**
+ * Returns a lock object that should be acquired before accessing any Rollback state from
+ * {@link RollbackManagerServiceImpl}.
+ *
+ * <p>Note that while holding this lock, the lock for {@link RollbackManagerServiceImpl} should
+ * not be acquired (but it is ok to acquire this lock while already holding the lock for that
+ * class).
+ */
+ // TODO(b/136241838): Move rollback functionality into this class and synchronize on the lock
+ // internally. Remove this method once this has been done for all cases.
+ Object getLock() {
+ return mLock;
+ }
+
+ /**
* Whether the rollback is for rollback of a staged install.
*/
+ @GuardedBy("getLock")
boolean isStaged() {
return info.isStaged();
}
@@ -151,6 +188,7 @@ class Rollback {
/**
* Returns the time when the upgrade occurred, for purposes of expiring rollback data.
*/
+ @GuardedBy("getLock")
Instant getTimestamp() {
return mTimestamp;
}
@@ -158,6 +196,7 @@ class Rollback {
/**
* Sets the time at which upgrade occurred.
*/
+ @GuardedBy("getLock")
void setTimestamp(Instant timestamp) {
mTimestamp = timestamp;
}
@@ -173,6 +212,7 @@ class Rollback {
/**
* Returns true if the rollback is in the ENABLING state.
*/
+ @GuardedBy("getLock")
boolean isEnabling() {
return mState == ROLLBACK_STATE_ENABLING;
}
@@ -180,6 +220,7 @@ class Rollback {
/**
* Returns true if the rollback is in the AVAILABLE state.
*/
+ @GuardedBy("getLock")
boolean isAvailable() {
return mState == ROLLBACK_STATE_AVAILABLE;
}
@@ -187,6 +228,7 @@ class Rollback {
/**
* Returns true if the rollback is in the COMMITTED state.
*/
+ @GuardedBy("getLock")
boolean isCommitted() {
return mState == ROLLBACK_STATE_COMMITTED;
}
@@ -194,6 +236,7 @@ class Rollback {
/**
* Sets the state of the rollback to AVAILABLE.
*/
+ @GuardedBy("getLock")
void setAvailable() {
mState = ROLLBACK_STATE_AVAILABLE;
}
@@ -201,6 +244,7 @@ class Rollback {
/**
* Sets the state of the rollback to COMMITTED.
*/
+ @GuardedBy("getLock")
void setCommitted() {
mState = ROLLBACK_STATE_COMMITTED;
}
@@ -208,6 +252,7 @@ class Rollback {
/**
* Returns the id of the post-reboot apk session for a staged install, if any.
*/
+ @GuardedBy("getLock")
int getApkSessionId() {
return mApkSessionId;
}
@@ -215,6 +260,7 @@ class Rollback {
/**
* Sets the id of the post-reboot apk session for a staged install.
*/
+ @GuardedBy("getLock")
void setApkSessionId(int apkSessionId) {
mApkSessionId = apkSessionId;
}
@@ -223,6 +269,7 @@ class Rollback {
* Returns true if we are expecting the package manager to call restoreUserData for this
* rollback because it has just been committed but the rollback has not yet been fully applied.
*/
+ @GuardedBy("getLock")
boolean isRestoreUserDataInProgress() {
return mRestoreUserDataInProgress;
}
@@ -231,10 +278,65 @@ class Rollback {
* Sets whether we are expecting the package manager to call restoreUserData for this
* rollback because it has just been committed but the rollback has not yet been fully applied.
*/
+ @GuardedBy("getLock")
void setRestoreUserDataInProgress(boolean restoreUserDataInProgress) {
mRestoreUserDataInProgress = restoreUserDataInProgress;
}
+ /**
+ * Returns true if this rollback includes the package with the provided {@code packageName}.
+ */
+ @GuardedBy("getLock")
+ boolean includesPackage(String packageName) {
+ for (PackageRollbackInfo info : info.getPackages()) {
+ if (info.getPackageName().equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this rollback includes the package with the provided {@code packageName}
+ * with a <i>version rolled back from</i> that is not {@code versionCode}.
+ */
+ @GuardedBy("getLock")
+ boolean includesPackageWithDifferentVersion(String packageName, long versionCode) {
+ for (PackageRollbackInfo info : info.getPackages()) {
+ if (info.getPackageName().equals(packageName)
+ && info.getVersionRolledBackFrom().getLongVersionCode() != versionCode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a list containing the names of all the packages included in this rollback.
+ */
+ @GuardedBy("getLock")
+ List<String> getPackageNames() {
+ List<String> result = new ArrayList<>();
+ for (PackageRollbackInfo info : info.getPackages()) {
+ result.add(info.getPackageName());
+ }
+ return result;
+ }
+
+ /**
+ * Returns a list containing the names of all the apex packages included in this rollback.
+ */
+ @GuardedBy("getLock")
+ List<String> getApexPackageNames() {
+ List<String> result = new ArrayList<>();
+ for (PackageRollbackInfo info : info.getPackages()) {
+ if (info.isApex()) {
+ result.add(info.getPackageName());
+ }
+ }
+ return result;
+ }
+
static String rollbackStateToString(@RollbackState int state) {
switch (state) {
case Rollback.ROLLBACK_STATE_ENABLING: return "enabling";
@@ -254,6 +356,7 @@ class Rollback {
throw new ParseException("Invalid rollback state: " + state, 0);
}
+ @GuardedBy("getLock")
String getStateAsString() {
return rollbackStateToString(mState);
}
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 96d284bb1c58..e8e448aa118e 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -282,8 +282,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.isAvailable()) {
- rollbacks.add(rollback.info);
+ synchronized (rollback.getLock()) {
+ if (rollback.isAvailable()) {
+ rollbacks.add(rollback.info);
+ }
}
}
return new ParceledListSlice<>(rollbacks);
@@ -298,8 +300,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
List<RollbackInfo> rollbacks = new ArrayList<>();
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.isCommitted()) {
- rollbacks.add(rollback.info);
+ synchronized (rollback.getLock()) {
+ if (rollback.isCommitted()) {
+ rollbacks.add(rollback.info);
+ }
}
}
return new ParceledListSlice<>(rollbacks);
@@ -332,8 +336,11 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- rollback.setTimestamp(rollback.getTimestamp().plusMillis(timeDifference));
- saveRollback(rollback);
+ synchronized (rollback.getLock()) {
+ rollback.setTimestamp(
+ rollback.getTimestamp().plusMillis(timeDifference));
+ saveRollback(rollback);
+ }
}
}
}
@@ -358,86 +365,94 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Slog.i(TAG, "Initiating rollback");
Rollback rollback = getRollbackForId(rollbackId);
- if (rollback == null || !rollback.isAvailable()) {
+ if (rollback == null) {
sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
"Rollback unavailable");
return;
}
-
- // Get a context for the caller to use to install the downgraded
- // version of the package.
- final Context context;
- try {
- context = mContext.createPackageContext(callerPackageName, 0);
- } catch (PackageManager.NameNotFoundException e) {
- sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
- "Invalid callerPackageName");
- return;
- }
-
- PackageManager pm = context.getPackageManager();
- try {
- PackageInstaller packageInstaller = pm.getPackageInstaller();
- PackageInstaller.SessionParams parentParams = new PackageInstaller.SessionParams(
- PackageInstaller.SessionParams.MODE_FULL_INSTALL);
- parentParams.setRequestDowngrade(true);
- parentParams.setMultiPackage();
- if (rollback.isStaged()) {
- parentParams.setStaged();
+ synchronized (rollback.getLock()) {
+ if (!rollback.isAvailable()) {
+ sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE,
+ "Rollback unavailable");
+ return;
}
- int parentSessionId = packageInstaller.createSession(parentParams);
- PackageInstaller.Session parentSession = packageInstaller.openSession(parentSessionId);
+ // Get a context for the caller to use to install the downgraded
+ // version of the package.
+ final Context context;
+ try {
+ context = mContext.createPackageContext(callerPackageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
+ "Invalid callerPackageName");
+ return;
+ }
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInstaller packageInstaller = pm.getPackageInstaller();
+ PackageInstaller.SessionParams parentParams = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
- // TODO: We can't get the installerPackageName for apex
- // (b/123920130). Is it okay to ignore the installer package
- // for apex?
- if (!info.isApex()) {
- String installerPackageName = pm.getInstallerPackageName(info.getPackageName());
- if (installerPackageName != null) {
- params.setInstallerPackageName(installerPackageName);
- }
- }
- params.setRequestDowngrade(true);
- params.setRequiredInstalledVersionCode(
- info.getVersionRolledBackFrom().getLongVersionCode());
+ parentParams.setRequestDowngrade(true);
+ parentParams.setMultiPackage();
if (rollback.isStaged()) {
- params.setStaged();
- }
- if (info.isApex()) {
- params.setInstallAsApex();
- }
- int sessionId = packageInstaller.createSession(params);
- PackageInstaller.Session session = packageInstaller.openSession(sessionId);
- File[] packageCodePaths = RollbackStore.getPackageCodePaths(
- rollback, info.getPackageName());
- if (packageCodePaths == null) {
- sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
- "Backup copy of package inaccessible");
- return;
+ parentParams.setStaged();
}
- for (File packageCodePath : packageCodePaths) {
- try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(packageCodePath,
+ int parentSessionId = packageInstaller.createSession(parentParams);
+ PackageInstaller.Session parentSession = packageInstaller.openSession(
+ parentSessionId);
+
+ for (PackageRollbackInfo info : rollback.info.getPackages()) {
+ PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ // TODO: We can't get the installerPackageName for apex
+ // (b/123920130). Is it okay to ignore the installer package
+ // for apex?
+ if (!info.isApex()) {
+ String installerPackageName =
+ pm.getInstallerPackageName(info.getPackageName());
+ if (installerPackageName != null) {
+ params.setInstallerPackageName(installerPackageName);
+ }
+ }
+ params.setRequestDowngrade(true);
+ params.setRequiredInstalledVersionCode(
+ info.getVersionRolledBackFrom().getLongVersionCode());
+ if (rollback.isStaged()) {
+ params.setStaged();
+ }
+ if (info.isApex()) {
+ params.setInstallAsApex();
+ }
+ int sessionId = packageInstaller.createSession(params);
+ PackageInstaller.Session session = packageInstaller.openSession(sessionId);
+ File[] packageCodePaths = RollbackStore.getPackageCodePaths(
+ rollback, info.getPackageName());
+ if (packageCodePaths == null) {
+ sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
+ "Backup copy of package inaccessible");
+ return;
+ }
+
+ for (File packageCodePath : packageCodePaths) {
+ try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(packageCodePath,
ParcelFileDescriptor.MODE_READ_ONLY)) {
- final long token = Binder.clearCallingIdentity();
- try {
- session.write(packageCodePath.getName(), 0, packageCodePath.length(),
- fd);
- } finally {
- Binder.restoreCallingIdentity(token);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ session.write(packageCodePath.getName(), 0,
+ packageCodePath.length(),
+ fd);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
}
+ parentSession.addChildSessionId(sessionId);
}
- parentSession.addChildSessionId(sessionId);
- }
- final LocalIntentReceiver receiver = new LocalIntentReceiver(
- (Intent result) -> {
- getHandler().post(() -> {
+ final LocalIntentReceiver receiver = new LocalIntentReceiver(
+ (Intent result) -> getHandler().post(() -> {
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
@@ -450,21 +465,22 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
// TODO: Should we just kill this rollback if
// commit failed? Why would we expect commit
// not to fail again?
- synchronized (mLock) {
- // TODO: Could this cause a rollback to be
- // resurrected if it should otherwise have
- // expired by now?
+ // TODO: Could this cause a rollback to be
+ // resurrected if it should otherwise have
+ // expired by now?
+ synchronized (rollback.getLock()) {
rollback.setAvailable();
rollback.setRestoreUserDataInProgress(false);
}
- sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_INSTALL,
+ sendFailure(statusReceiver,
+ RollbackManager.STATUS_FAILURE_INSTALL,
"Rollback downgrade install failed: "
- + result.getStringExtra(
+ + result.getStringExtra(
PackageInstaller.EXTRA_STATUS_MESSAGE));
return;
}
- synchronized (mLock) {
+ synchronized (rollback.getLock()) {
if (!rollback.isStaged()) {
// All calls to restoreUserData should have
// completed by now for a non-staged install.
@@ -473,32 +489,31 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
rollback.info.setCommittedSessionId(parentSessionId);
rollback.info.getCausePackages().addAll(causePackages);
+ RollbackStore.deletePackageCodePaths(rollback);
+ saveRollback(rollback);
}
- mRollbackStore.deletePackageCodePaths(rollback);
- saveRollback(rollback);
sendSuccess(statusReceiver);
Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED);
for (UserInfo userInfo : UserManager.get(mContext).getUsers(true)) {
- mContext.sendBroadcastAsUser(broadcast, userInfo.getUserHandle(),
+ mContext.sendBroadcastAsUser(broadcast,
+ userInfo.getUserHandle(),
Manifest.permission.MANAGE_ROLLBACKS);
}
- });
- }
- );
+ })
+ );
- synchronized (mLock) {
rollback.setCommitted();
rollback.setRestoreUserDataInProgress(true);
+ parentSession.commit(receiver.getIntentSender());
+ } catch (IOException e) {
+ Slog.e(TAG, "Rollback failed", e);
+ sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
+ "IOException: " + e.toString());
+ return;
}
- parentSession.commit(receiver.getIntentSender());
- } catch (IOException e) {
- Slog.e(TAG, "Rollback failed", e);
- sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE,
- "IOException: " + e.toString());
- return;
}
}
@@ -534,19 +549,17 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- if (info.getPackageName().equals(packageName)) {
+ synchronized (rollback.getLock()) {
+ if (rollback.includesPackage(packageName)) {
iter.remove();
deleteRollback(rollback);
- break;
}
}
}
for (NewRollback newRollback : mNewRollbacks) {
- for (PackageRollbackInfo info : newRollback.rollback.info.getPackages()) {
- if (info.getPackageName().equals(packageName)) {
+ synchronized (newRollback.rollback.getLock()) {
+ if (newRollback.rollback.includesPackage(packageName)) {
newRollback.isCancelled = true;
- break;
}
}
}
@@ -578,12 +591,16 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
rollbacks = new ArrayList<>(mRollbacks);
}
- final Set<Rollback> changed =
- mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(userId, rollbacks);
-
- for (Rollback rollback : changed) {
- saveRollback(rollback);
+ for (int i = 0; i < rollbacks.size(); i++) {
+ Rollback rollback = rollbacks.get(i);
+ synchronized (rollback.getLock()) {
+ if (mAppDataRollbackHelper.commitPendingBackupAndRestoreForUser(
+ userId, rollback)) {
+ saveRollback(rollback);
+ }
+ }
}
+
latch.countDown();
});
@@ -617,17 +634,15 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Set<String> apexPackageNames = new HashSet<>();
synchronized (mLock) {
for (Rollback rollback : mRollbacks) {
- if (rollback.isStaged()) {
- if (rollback.isEnabling()) {
- enabling.add(rollback);
- } else if (rollback.isRestoreUserDataInProgress()) {
- restoreInProgress.add(rollback);
- }
-
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- if (info.isApex()) {
- apexPackageNames.add(info.getPackageName());
+ synchronized (rollback.getLock()) {
+ if (rollback.isStaged()) {
+ if (rollback.isEnabling()) {
+ enabling.add(rollback);
+ } else if (rollback.isRestoreUserDataInProgress()) {
+ restoreInProgress.add(rollback);
}
+
+ apexPackageNames.addAll(rollback.getApexPackageNames());
}
}
}
@@ -635,30 +650,32 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
for (Rollback rollback : enabling) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo session =
- installer.getSessionInfo(rollback.getStagedSessionId());
- if (session == null || session.isStagedSessionFailed()) {
- // TODO: Do we need to remove this from
- // mRollbacks, or is it okay to leave as
- // unavailable until the next reboot when it will go
- // away on its own?
- deleteRollback(rollback);
- } else if (session.isStagedSessionApplied()) {
- makeRollbackAvailable(rollback);
+ synchronized (rollback.getLock()) {
+ PackageInstaller.SessionInfo session =
+ installer.getSessionInfo(rollback.getStagedSessionId());
+ if (session == null || session.isStagedSessionFailed()) {
+ // TODO: Do we need to remove this from
+ // mRollbacks, or is it okay to leave as
+ // unavailable until the next reboot when it will go
+ // away on its own?
+ deleteRollback(rollback);
+ } else if (session.isStagedSessionApplied()) {
+ makeRollbackAvailable(rollback);
+ }
}
}
for (Rollback rollback : restoreInProgress) {
PackageInstaller installer = mContext.getPackageManager().getPackageInstaller();
- PackageInstaller.SessionInfo session =
- installer.getSessionInfo(rollback.getStagedSessionId());
- // TODO: What if session is null?
- if (session != null) {
- if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) {
- synchronized (mLock) {
+ synchronized (rollback.getLock()) {
+ PackageInstaller.SessionInfo session =
+ installer.getSessionInfo(rollback.getStagedSessionId());
+ // TODO: What if session is null?
+ if (session != null) {
+ if (session.isStagedSessionApplied() || session.isStagedSessionFailed()) {
rollback.setRestoreUserDataInProgress(false);
+ saveRollback(rollback);
}
- saveRollback(rollback);
}
}
}
@@ -687,23 +704,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
private void onPackageReplaced(String packageName) {
// TODO: Could this end up incorrectly deleting a rollback for a
// package that is about to be installed?
- VersionedPackage installedVersion = getInstalledPackageVersion(packageName);
+ long installedVersion = getInstalledPackageVersion(packageName);
synchronized (mLock) {
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- // TODO: Should we remove rollbacks in the ENABLING state here?
- if (rollback.isEnabling() || rollback.isAvailable()) {
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- if (info.getPackageName().equals(packageName)
- && !packageVersionsEqual(
- info.getVersionRolledBackFrom(),
- installedVersion)) {
- iter.remove();
- deleteRollback(rollback);
- break;
- }
+ synchronized (rollback.getLock()) {
+ // TODO: Should we remove rollbacks in the ENABLING state here?
+ if ((rollback.isEnabling() || rollback.isAvailable())
+ && rollback.includesPackageWithDifferentVersion(packageName,
+ installedVersion)) {
+ iter.remove();
+ deleteRollback(rollback);
}
}
}
@@ -760,16 +773,18 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
Iterator<Rollback> iter = mRollbacks.iterator();
while (iter.hasNext()) {
Rollback rollback = iter.next();
- if (!rollback.isAvailable()) {
- continue;
- }
- if (!now.isBefore(
+ synchronized (rollback.getLock()) {
+ if (!rollback.isAvailable()) {
+ continue;
+ }
+ if (!now.isBefore(
rollback.getTimestamp()
.plusMillis(mRollbackLifetimeDurationInMillis))) {
- iter.remove();
- deleteRollback(rollback);
- } else if (oldest == null || oldest.isAfter(rollback.getTimestamp())) {
- oldest = rollback.getTimestamp();
+ iter.remove();
+ deleteRollback(rollback);
+ } else if (oldest == null || oldest.isAfter(rollback.getTimestamp())) {
+ oldest = rollback.getTimestamp();
+ }
}
}
}
@@ -877,10 +892,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback rollback = mRollbacks.get(i);
- if (rollback.getApkSessionId() == parentSession.getSessionId()) {
- // This is the apk session for a staged session with rollback enabled. We do not
- // need to create a new rollback for this session.
- return true;
+ synchronized (rollback.getLock()) {
+ if (rollback.getApkSessionId() == parentSession.getSessionId()) {
+ // This is the apk session for a staged session with rollback enabled. We do
+ // not need to create a new rollback for this session.
+ return true;
+ }
}
}
}
@@ -979,6 +996,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
new IntArray() /* pendingBackups */, new ArrayList<>() /* pendingRestores */,
isApex, new IntArray(), new SparseLongArray() /* ceSnapshotInodes */);
+
try {
ApplicationInfo appInfo = pkgInfo.applicationInfo;
RollbackStore.backupPackageCodePath(rollback, packageName, appInfo.sourceDir);
@@ -992,7 +1010,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
return false;
}
- synchronized (mLock) {
+ synchronized (rollback.getLock()) {
rollback.info.getPackages().add(packageRollbackInfo);
}
return true;
@@ -1020,27 +1038,31 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
// staged installs
for (int i = 0; i < mRollbacks.size(); i++) {
Rollback rollback = mRollbacks.get(i);
- if (!rollback.isEnabling()) {
- continue;
- }
+ synchronized (rollback.getLock()) {
+ if (!rollback.isEnabling()) {
+ continue;
+ }
- for (PackageRollbackInfo info : rollback.info.getPackages()) {
- if (info.getPackageName().equals(packageName)) {
- mAppDataRollbackHelper.snapshotAppData(
- rollback.info.getRollbackId(), info, userIds);
- saveRollback(rollback);
- break;
+ for (PackageRollbackInfo info : rollback.info.getPackages()) {
+ if (info.getPackageName().equals(packageName)) {
+ mAppDataRollbackHelper.snapshotAppData(
+ rollback.info.getRollbackId(), info, userIds);
+ saveRollback(rollback);
+ break;
+ }
}
}
}
// non-staged installs
PackageRollbackInfo info;
for (NewRollback rollback : mNewRollbacks) {
- info = getPackageRollbackInfo(rollback.rollback, packageName);
- if (info != null) {
- mAppDataRollbackHelper.snapshotAppData(
- rollback.rollback.info.getRollbackId(), info, userIds);
- saveRollback(rollback.rollback);
+ synchronized (rollback.rollback.getLock()) {
+ info = getPackageRollbackInfo(rollback.rollback, packageName);
+ if (info != null) {
+ mAppDataRollbackHelper.snapshotAppData(
+ rollback.rollback.info.getRollbackId(), info, userIds);
+ saveRollback(rollback.rollback);
+ }
}
}
}
@@ -1053,11 +1075,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
synchronized (mLock) {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback candidate = mRollbacks.get(i);
- if (candidate.isRestoreUserDataInProgress()) {
- info = getPackageRollbackInfo(candidate, packageName);
- if (info != null) {
- rollback = candidate;
- break;
+ synchronized (candidate.getLock()) {
+ if (candidate.isRestoreUserDataInProgress()) {
+ info = getPackageRollbackInfo(candidate, packageName);
+ if (info != null) {
+ rollback = candidate;
+ break;
+ }
}
}
}
@@ -1068,12 +1092,14 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
for (int userId : userIds) {
- final boolean changedRollback = mAppDataRollbackHelper.restoreAppData(
- rollback.info.getRollbackId(), info, userId, appId, seInfo);
+ synchronized (rollback.getLock()) {
+ final boolean changedRollback = mAppDataRollbackHelper.restoreAppData(
+ rollback.info.getRollbackId(), info, userId, appId, seInfo);
- // We've updated metadata about this rollback, so save it to flash.
- if (changedRollback) {
- saveRollback(rollback);
+ // We've updated metadata about this rollback, so save it to flash.
+ if (changedRollback) {
+ saveRollback(rollback);
+ }
}
}
}
@@ -1147,7 +1173,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
for (int i = 0; i < mRollbacks.size(); ++i) {
Rollback candidate = mRollbacks.get(i);
if (candidate.getStagedSessionId() == originalSessionId) {
- candidate.setApkSessionId(apkSessionId);
rollback = candidate;
break;
}
@@ -1162,7 +1187,10 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
if (rollback != null) {
- saveRollback(rollback);
+ synchronized (rollback.getLock()) {
+ rollback.setApkSessionId(apkSessionId);
+ saveRollback(rollback);
+ }
}
});
}
@@ -1207,18 +1235,18 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
/**
* Gets the version of the package currently installed.
- * Returns null if the package is not currently installed.
+ * Returns -1 if the package is not currently installed.
*/
- private VersionedPackage getInstalledPackageVersion(String packageName) {
+ private long getInstalledPackageVersion(String packageName) {
PackageManager pm = mContext.getPackageManager();
PackageInfo pkgInfo = null;
try {
pkgInfo = getPackageInfo(packageName);
} catch (PackageManager.NameNotFoundException e) {
- return null;
+ return -1;
}
- return new VersionedPackage(packageName, pkgInfo.getLongVersionCode());
+ return pkgInfo.getLongVersionCode();
}
/**
@@ -1273,44 +1301,49 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
if (newRollback != null) {
Rollback rollback = completeEnableRollback(newRollback, success);
- if (rollback != null && !rollback.isStaged()) {
- makeRollbackAvailable(rollback);
+ if (rollback != null) {
+ synchronized (rollback.getLock()) {
+ if (!rollback.isStaged()) {
+ makeRollbackAvailable(rollback);
+ }
+ }
}
}
}
}
/**
- * Add a rollback to the list of rollbacks.
- * This should be called after rollback has been enabled for all packages
- * in the rollback. It does not make the rollback available yet.
+ * Add a rollback to the list of rollbacks. This should be called after rollback has been
+ * enabled for all packages in the rollback. It does not make the rollback available yet.
+ *
+ * <p>Note that no rollback-specific locks should be held when this method is called.
*
* @return the Rollback instance for a successfully enable-completed rollback,
* or null on error.
*/
private Rollback completeEnableRollback(NewRollback newRollback, boolean success) {
Rollback rollback = newRollback.rollback;
- if (!success) {
- // The install session was aborted, clean up the pending install.
- deleteRollback(rollback);
- return null;
- }
- if (newRollback.isCancelled) {
- Slog.e(TAG, "Rollback has been cancelled by PackageManager");
- deleteRollback(rollback);
- return null;
- }
+ synchronized (rollback.getLock()) {
+ if (!success) {
+ // The install session was aborted, clean up the pending install.
+ deleteRollback(rollback);
+ return null;
+ }
+ if (newRollback.isCancelled) {
+ Slog.e(TAG, "Rollback has been cancelled by PackageManager");
+ deleteRollback(rollback);
+ return null;
+ }
- // It's safe to access rollback.info outside a synchronized block because
- // this is running on the handler thread and all changes to the
- // rollback.info occur on the handler thread.
- if (rollback.info.getPackages().size() != newRollback.packageSessionIds.length) {
- Slog.e(TAG, "Failed to enable rollback for all packages in session.");
- deleteRollback(rollback);
- return null;
- }
- saveRollback(rollback);
+ if (rollback.info.getPackages().size() != newRollback.packageSessionIds.length) {
+ Slog.e(TAG, "Failed to enable rollback for all packages in session.");
+ deleteRollback(rollback);
+ return null;
+ }
+
+ saveRollback(rollback);
+ }
synchronized (mLock) {
// Note: There is a small window of time between when
// the session has been committed by the package
@@ -1328,14 +1361,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
return rollback;
}
+ @GuardedBy("rollback.getLock")
private void makeRollbackAvailable(Rollback rollback) {
// TODO: What if the rollback has since been expired, for example due
// to a new package being installed. Won't this revive an expired
// rollback? Consider adding a ROLLBACK_STATE_EXPIRED to address this.
- synchronized (mLock) {
- rollback.setAvailable();
- rollback.setTimestamp(Instant.now());
- }
+ rollback.setAvailable();
+ rollback.setTimestamp(Instant.now());
saveRollback(rollback);
// TODO(zezeozue): Provide API to explicitly start observing instead
@@ -1343,11 +1375,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
// should document in PackageInstaller.SessionParams#setEnableRollback
// After enabling and commiting any rollback, observe packages and
// prepare to rollback if packages crashes too frequently.
- List<String> packages = new ArrayList<>();
- for (int i = 0; i < rollback.info.getPackages().size(); i++) {
- packages.add(rollback.info.getPackages().get(i).getPackageName());
- }
- mPackageHealthObserver.startObservingHealth(packages,
+ mPackageHealthObserver.startObservingHealth(rollback.getPackageNames(),
mRollbackLifetimeDurationInMillis);
scheduleExpiration(mRollbackLifetimeDurationInMillis);
}
@@ -1372,6 +1400,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
* Returns the {@code PackageRollbackInfo} associated with {@code packageName} from
* a specified {@code Rollback}.
*/
+ @GuardedBy("rollback.getLock")
private static PackageRollbackInfo getPackageRollbackInfo(Rollback rollback,
String packageName) {
for (PackageRollbackInfo info : rollback.info.getPackages()) {
@@ -1398,6 +1427,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
throw new IllegalStateException("Failed to allocate rollback ID");
}
+ @GuardedBy("rollback.getLock")
private void deleteRollback(Rollback rollback) {
for (PackageRollbackInfo info : rollback.info.getPackages()) {
IntArray snapshottedUsers = info.getSnapshottedUsers();
@@ -1416,6 +1446,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
* TODO: Double check we can't do a better job handling the IOException in
* a cases where this method is called.
*/
+ @GuardedBy("rollback.getLock")
private void saveRollback(Rollback rollback) {
try {
mRollbackStore.saveRollback(rollback);
@@ -1430,32 +1461,34 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
synchronized (mLock) {
for (Rollback rollback : mRollbacks) {
- RollbackInfo info = rollback.info;
- ipw.println(info.getRollbackId() + ":");
- ipw.increaseIndent();
- ipw.println("-state: " + rollback.getStateAsString());
- ipw.println("-timestamp: " + rollback.getTimestamp());
- if (rollback.getStagedSessionId() != -1) {
- ipw.println("-stagedSessionId: " + rollback.getStagedSessionId());
- }
- ipw.println("-packages:");
- ipw.increaseIndent();
- for (PackageRollbackInfo pkg : info.getPackages()) {
- ipw.println(pkg.getPackageName()
- + " " + pkg.getVersionRolledBackFrom().getLongVersionCode()
- + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
- }
- ipw.decreaseIndent();
- if (rollback.isCommitted()) {
- ipw.println("-causePackages:");
+ synchronized (rollback.getLock()) {
+ RollbackInfo info = rollback.info;
+ ipw.println(info.getRollbackId() + ":");
+ ipw.increaseIndent();
+ ipw.println("-state: " + rollback.getStateAsString());
+ ipw.println("-timestamp: " + rollback.getTimestamp());
+ if (rollback.getStagedSessionId() != -1) {
+ ipw.println("-stagedSessionId: " + rollback.getStagedSessionId());
+ }
+ ipw.println("-packages:");
ipw.increaseIndent();
- for (VersionedPackage cPkg : info.getCausePackages()) {
- ipw.println(cPkg.getPackageName() + " " + cPkg.getLongVersionCode());
+ for (PackageRollbackInfo pkg : info.getPackages()) {
+ ipw.println(pkg.getPackageName()
+ + " " + pkg.getVersionRolledBackFrom().getLongVersionCode()
+ + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
+ }
+ ipw.decreaseIndent();
+ if (rollback.isCommitted()) {
+ ipw.println("-causePackages:");
+ ipw.increaseIndent();
+ for (VersionedPackage cPkg : info.getCausePackages()) {
+ ipw.println(cPkg.getPackageName() + " " + cPkg.getLongVersionCode());
+ }
+ ipw.decreaseIndent();
+ ipw.println("-committedSessionId: " + info.getCommittedSessionId());
}
ipw.decreaseIndent();
- ipw.println("-committedSessionId: " + info.getCommittedSessionId());
}
- ipw.decreaseIndent();
}
}
}
@@ -1516,7 +1549,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
}
}
- NewRollback createNewRollbackLocked(PackageInstaller.SessionInfo parentSession) {
+ @GuardedBy("mLock")
+ private NewRollback createNewRollbackLocked(PackageInstaller.SessionInfo parentSession) {
int rollbackId = allocateRollbackIdLocked();
final Rollback rollback;
int parentSessionId = parentSession.getSessionId();
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index 772c53fec4ce..b6d1f1875907 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -27,6 +27,8 @@ import android.util.IntArray;
import android.util.Slog;
import android.util.SparseLongArray;
+import com.android.internal.annotations.GuardedBy;
+
import libcore.io.IoUtils;
import org.json.JSONArray;
@@ -250,6 +252,7 @@ class RollbackStore {
/**
* Saves the given rollback to persistent storage.
*/
+ @GuardedBy("rollback.getLock")
void saveRollback(Rollback rollback) throws IOException {
try {
JSONObject dataJson = new JSONObject();
diff --git a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java
index 8cb5197f2601..0b8c2a55b45e 100644
--- a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java
@@ -45,8 +45,6 @@ import org.mockito.Mockito;
import java.io.File;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Set;
@RunWith(JUnit4.class)
public class AppDataRollbackHelperTest {
@@ -250,28 +248,22 @@ public class AppDataRollbackHelperTest {
dataForRestore.info.getPackages().add(pendingRestore);
dataForRestore.info.getPackages().add(wasRecentlyRestored);
- Set<Rollback> changed = helper.commitPendingBackupAndRestoreForUser(37,
- Arrays.asList(dataWithPendingBackup, dataWithRecentRestore, dataForDifferentUser,
- dataForRestore));
InOrder inOrder = Mockito.inOrder(installer);
// Check that pending backup and restore for the same package mutually destroyed each other.
+ assertTrue(helper.commitPendingBackupAndRestoreForUser(37, dataWithRecentRestore));
assertEquals(-1, wasRecentlyRestored.getPendingBackups().indexOf(37));
assertNull(wasRecentlyRestored.getRestoreInfo(37));
// Check that backup was performed.
+ assertTrue(helper.commitPendingBackupAndRestoreForUser(37, dataWithPendingBackup));
inOrder.verify(installer).snapshotAppData(eq("com.foo"), eq(37), eq(101),
eq(Installer.FLAG_STORAGE_CE));
assertEquals(-1, pendingBackup.getPendingBackups().indexOf(37));
assertEquals(53, pendingBackup.getCeSnapshotInodes().get(37));
- // Check that changed returns correct Rollback.
- assertEquals(3, changed.size());
- assertTrue(changed.contains(dataWithPendingBackup));
- assertTrue(changed.contains(dataWithRecentRestore));
- assertTrue(changed.contains(dataForRestore));
-
// Check that restore was performed.
+ assertTrue(helper.commitPendingBackupAndRestoreForUser(37, dataForRestore));
inOrder.verify(installer).restoreAppDataSnapshot(
eq("com.abc"), eq(57) /* appId */, eq("seInfo"), eq(37) /* userId */,
eq(17239) /* rollbackId */, eq(Installer.FLAG_STORAGE_CE));
diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java
index d27f1c7e0ce7..b5925a6e750f 100644
--- a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java
+++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java
@@ -18,11 +18,18 @@ package com.android.server.rollback;
import static com.google.common.truth.Truth.assertThat;
+import android.content.pm.VersionedPackage;
+import android.content.rollback.PackageRollbackInfo;
+import android.util.IntArray;
+import android.util.SparseLongArray;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
@RunWith(JUnit4.class)
public class RollbackUnitTest {
@@ -74,4 +81,62 @@ public class RollbackUnitTest {
assertThat(rollback.isCommitted()).isTrue();
}
+ @Test
+ public void getPackageNamesAllAndJustApex() {
+ String pkg1 = "test.testpackage.pkg1";
+ String pkg2 = "test.testpackage.pkg2";
+ String pkg3 = "com.blah.hello.three";
+ String pkg4 = "com.something.4pack";
+
+ Rollback rollback = new Rollback(123, new File("/test/testing"), -1);
+ PackageRollbackInfo pkgInfo1 = pkgInfoFor(pkg1, 12, 10, false);
+ PackageRollbackInfo pkgInfo2 = pkgInfoFor(pkg2, 12, 10, true);
+ PackageRollbackInfo pkgInfo3 = pkgInfoFor(pkg3, 12, 10, false);
+ PackageRollbackInfo pkgInfo4 = pkgInfoFor(pkg4, 12, 10, true);
+
+ rollback.info.getPackages().addAll(Arrays.asList(pkgInfo1, pkgInfo2, pkgInfo3, pkgInfo4));
+
+ assertThat(rollback.getPackageNames()).containsExactly(pkg1, pkg2, pkg3, pkg4);
+ assertThat(rollback.getApexPackageNames()).containsExactly(pkg2, pkg4);
+ }
+
+ @Test
+ public void includesPackages() {
+ String pkg1 = "test.testpackage.pkg1";
+ String pkg2 = "test.testpackage.pkg2";
+ String pkg3 = "com.blah.hello.three";
+ String pkg4 = "com.something.4pack";
+
+ Rollback rollback = new Rollback(123, new File("/test/testing"), -1);
+ PackageRollbackInfo pkgInfo1 = pkgInfoFor(pkg1, 12, 10, false);
+ PackageRollbackInfo pkgInfo2 = pkgInfoFor(pkg2, 18, 12, true);
+ PackageRollbackInfo pkgInfo3 = pkgInfoFor(pkg3, 157, 156, false);
+ PackageRollbackInfo pkgInfo4 = pkgInfoFor(pkg4, 99, 1, true);
+
+ rollback.info.getPackages().addAll(Arrays.asList(pkgInfo1, pkgInfo2, pkgInfo3, pkgInfo4));
+
+ assertThat(rollback.includesPackage(pkg2)).isTrue();
+ assertThat(rollback.includesPackage(pkg3)).isTrue();
+ assertThat(rollback.includesPackage("com.something.else")).isFalse();
+
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg1, 12)).isFalse();
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg1, 1)).isTrue();
+
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg2, 18)).isFalse();
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg2, 12)).isTrue();
+
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg3, 157)).isFalse();
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg3, 156)).isTrue();
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg3, 15)).isTrue();
+
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg4, 99)).isFalse();
+ assertThat(rollback.includesPackageWithDifferentVersion(pkg4, 100)).isTrue();
+ }
+
+ private static PackageRollbackInfo pkgInfoFor(
+ String packageName, long fromVersion, long toVersion, boolean isApex) {
+ return new PackageRollbackInfo(new VersionedPackage(packageName, fromVersion),
+ new VersionedPackage(packageName, toVersion),
+ new IntArray(), new ArrayList<>(), isApex, new IntArray(), new SparseLongArray());
+ }
}