diff options
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()); + } } |