diff options
3 files changed, 251 insertions, 38 deletions
diff --git a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java index e103464207d2..cd9311f785d0 100644 --- a/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java +++ b/services/robotests/src/com/android/server/backup/PerformBackupTaskTest.java @@ -26,6 +26,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -40,7 +42,9 @@ import android.app.Application; import android.app.IActivityManager; import android.app.IBackupAgent; import android.app.backup.BackupAgent; +import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; +import android.app.backup.BackupTransport; import android.app.backup.FullBackupDataOutput; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -53,6 +57,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -80,6 +85,8 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @@ -88,6 +95,7 @@ import org.robolectric.shadows.ShadowPackageManager; import org.robolectric.shadows.ShadowQueuedWork; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -184,32 +192,33 @@ public class PerformBackupTaskTest { @Test public void testRunTask_whenTransportProvidesFlags_passesThemToTheAgent() throws Exception { - BackupAgent agent = setUpAgent(PACKAGE_1); + AgentMock agentMock = setUpAgent(PACKAGE_1); int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; when(mTransportBinder.getTransportFlags()).thenReturn(flags); PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); runTask(task); - verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + verify(agentMock.agent) + .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); } @Test public void testRunTask_whenTransportDoesNotProvidesFlags() throws Exception { - BackupAgent agent = setUpAgent(PACKAGE_1); + AgentMock agentMock = setUpAgent(PACKAGE_1); PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); runTask(task); - verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any()); + verify(agentMock.agent).onBackup(any(), argThat(dataOutputWithTransportFlags(0)), any()); } @Test public void testRunTask_whenTransportProvidesFlagsAndMultipleAgents_passesToAll() throws Exception { - List<BackupAgent> agents = setUpAgents(PACKAGE_1, PACKAGE_2); - BackupAgent agent1 = agents.get(0); - BackupAgent agent2 = agents.get(1); + List<AgentMock> agentMocks = setUpAgents(PACKAGE_1, PACKAGE_2); + BackupAgent agent1 = agentMocks.get(0).agent; + BackupAgent agent2 = agentMocks.get(1).agent; int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; when(mTransportBinder.getTransportFlags()).thenReturn(flags); PerformBackupTask task = @@ -223,14 +232,103 @@ public class PerformBackupTaskTest { @Test public void testRunTask_whenTransportChangeFlagsAfterTaskCreation() throws Exception { - BackupAgent agent = setUpAgent(PACKAGE_1); + AgentMock agentMock = setUpAgent(PACKAGE_1); PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); int flags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED; when(mTransportBinder.getTransportFlags()).thenReturn(flags); runTask(task); - verify(agent).onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + verify(agentMock.agent) + .onBackup(any(), argThat(dataOutputWithTransportFlags(flags)), any()); + } + + @Test + public void testRunTask_callsListenerOnTaskFinished() throws Exception { + setUpAgent(PACKAGE_1); + PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); + + runTask(task); + + verify(mListener).onFinished(any()); + } + + @Test + public void testRunTask_callsTransportPerformBackup() throws Exception { + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock.agent, + (oldState, dataOutput, newState) -> { + writeData(dataOutput, "key1", "foo".getBytes()); + writeData(dataOutput, "key2", "bar".getBytes()); + }); + PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); + // We need to verify at call time because the file is deleted right after + when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .then(this::mockAndVerifyTransportPerformBackupData); + + runTask(task); + + // Already verified data in mockAndVerifyPerformBackupData + verify(mTransportBinder).performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt()); + } + + private int mockAndVerifyTransportPerformBackupData(InvocationOnMock invocation) + throws IOException { + ParcelFileDescriptor data = invocation.getArgument(1); + + // Verifying that what we passed to the transport is what the agent wrote + BackupDataInput dataInput = new BackupDataInput(data.getFileDescriptor()); + + // "key1" => "foo" + assertThat(dataInput.readNextHeader()).isTrue(); + assertThat(dataInput.getKey()).isEqualTo("key1"); + int size1 = dataInput.getDataSize(); + byte[] data1 = new byte[size1]; + dataInput.readEntityData(data1, 0, size1); + assertThat(data1).isEqualTo("foo".getBytes()); + + // "key2" => "bar" + assertThat(dataInput.readNextHeader()).isTrue(); + assertThat(dataInput.getKey()).isEqualTo("key2"); + int size2 = dataInput.getDataSize(); + byte[] data2 = new byte[size2]; + dataInput.readEntityData(data2, 0, size2); + assertThat(data2).isEqualTo("bar".getBytes()); + + // No more + assertThat(dataInput.readNextHeader()).isFalse(); + + return BackupTransport.TRANSPORT_OK; + } + + @Test + public void testRunTask_whenPerformBackupSucceeds_callsTransportFinishBackup() + throws Exception { + setUpAgent(PACKAGE_1); + PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); + when(mTransportBinder.performBackup(argThat(packageInfo(PACKAGE_1)), any(), anyInt())) + .thenReturn(BackupTransport.TRANSPORT_OK); + + runTask(task); + + verify(mTransportBinder).finishBackup(); + } + + @Test + public void testRunTask_whenProhibitedKey_failsAgent() throws Exception { + AgentMock agentMock = setUpAgent(PACKAGE_1); + agentOnBackupDo( + agentMock.agent, + (oldState, dataOutput, newState) -> { + char prohibitedChar = 0xff00; + writeData(dataOutput, prohibitedChar + "key", "foo".getBytes()); + }); + PerformBackupTask task = createPerformBackupTask(emptyList(), false, true, PACKAGE_1); + + runTask(task); + + verify(agentMock.agentBinder).fail(any()); } private void runTask(PerformBackupTask task) { @@ -241,26 +339,34 @@ public class PerformBackupTaskTest { } } - private List<BackupAgent> setUpAgents(String... packageNames) { + private List<AgentMock> setUpAgents(String... packageNames) { return Stream.of(packageNames).map(this::setUpAgent).collect(toList()); } - private BackupAgent setUpAgent(String packageName) { - PackageInfo packageInfo = new PackageInfo(); - packageInfo.packageName = packageName; - packageInfo.applicationInfo = new ApplicationInfo(); - packageInfo.applicationInfo.flags = ApplicationInfo.FLAG_ALLOW_BACKUP; - packageInfo.applicationInfo.backupAgentName = "BackupAgent" + packageName; - packageInfo.applicationInfo.packageName = packageName; - mShadowPackageManager.setApplicationEnabledSetting( - packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); - mShadowPackageManager.addPackage(packageInfo); - BackupAgent backupAgent = spy(BackupAgent.class); - IBackupAgent backupAgentBinder = IBackupAgent.Stub.asInterface(backupAgent.onBind()); - when(mBackupManagerService.bindToAgentSynchronous( - eq(packageInfo.applicationInfo), anyInt())) - .thenReturn(backupAgentBinder); - return backupAgent; + private AgentMock setUpAgent(String packageName) { + try { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = packageName; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.flags = ApplicationInfo.FLAG_ALLOW_BACKUP; + packageInfo.applicationInfo.backupAgentName = "BackupAgent" + packageName; + packageInfo.applicationInfo.packageName = packageName; + mShadowPackageManager.setApplicationEnabledSetting( + packageName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); + mShadowPackageManager.addPackage(packageInfo); + BackupAgent backupAgent = spy(BackupAgent.class); + IBackupAgent backupAgentBinder = + spy(IBackupAgent.Stub.asInterface(backupAgent.onBind())); + // Don't crash our only process (in production code this would crash the app, not us) + doNothing().when(backupAgentBinder).fail(any()); + when(mBackupManagerService.bindToAgentSynchronous( + eq(packageInfo.applicationInfo), anyInt())) + .thenReturn(backupAgentBinder); + return new AgentMock(backupAgentBinder, backupAgent); + } catch (RemoteException e) { + // Never happens, compiler happy + throw new AssertionError(e); + } } private PerformBackupTask createPerformBackupTask( @@ -288,10 +394,53 @@ public class PerformBackupTaskTest { return task; } - private ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) { + private static ArgumentMatcher<PackageInfo> packageInfo(String packageName) { + return packageInfo -> packageName.equals(packageInfo.packageName); + } + + private static ArgumentMatcher<BackupDataOutput> dataOutputWithTransportFlags(int flags) { return dataOutput -> dataOutput.getTransportFlags() == flags; } + private static void writeData(BackupDataOutput dataOutput, String key, byte[] data) + throws IOException { + dataOutput.writeEntityHeader(key, data.length); + dataOutput.writeEntityData(data, data.length); + } + + private static void agentOnBackupDo(BackupAgent agent, BackupAgentOnBackup function) + throws Exception { + doAnswer(function).when(agent).onBackup(any(), any(), any()); + } + + @FunctionalInterface + private interface BackupAgentOnBackup extends Answer<Void> { + void onBackup( + ParcelFileDescriptor oldState, + BackupDataOutput dataOutput, + ParcelFileDescriptor newState) + throws IOException; + + @Override + default Void answer(InvocationOnMock invocation) throws Throwable { + onBackup( + invocation.getArgument(0), + invocation.getArgument(1), + invocation.getArgument(2)); + return null; + } + } + + private static class AgentMock { + private final IBackupAgent agentBinder; + private final BackupAgent agent; + + public AgentMock(IBackupAgent agentBinder, BackupAgent agent) { + this.agentBinder = agentBinder; + this.agent = agent; + } + } + private abstract static class FakeIBackupManager extends IBackupManager.Stub { private Handler mBackupHandler; private BackupRestoreTask mTask; diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataInput.java b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataInput.java index 28489afce8d5..8016a8bd5e5f 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataInput.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataInput.java @@ -21,41 +21,76 @@ import android.app.backup.BackupDataInput; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import java.io.EOFException; import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.IOException; +import java.io.ObjectInputStream; +/** + * Shadow for {@link BackupDataInput}. Format read does NOT match implementation. To write data to + * be read by this shadow, you should also declare shadow {@link ShadowBackupDataOutput}. + */ @Implements(BackupDataInput.class) public class ShadowBackupDataInput { - @Implementation - public void __constructor__(FileDescriptor fd) { - } + private ObjectInputStream mInput; + private int mSize; + private String mKey; + private boolean mHeaderReady; @Implementation - protected void finalize() throws Throwable { + public void __constructor__(FileDescriptor fd) { + try { + mInput = new ObjectInputStream(new FileInputStream(fd)); + } catch (IOException e) { + throw new AssertionError(e); + } } @Implementation public boolean readNextHeader() throws IOException { - return false; + mHeaderReady = false; + try { + mSize = mInput.readInt(); + } catch (EOFException e) { + return false; + } + mKey = mInput.readUTF(); + mHeaderReady = true; + return true; } @Implementation public String getKey() { - throw new AssertionError("Can't call because readNextHeader() returned false"); + checkHeaderReady(); + return mKey; } @Implementation public int getDataSize() { - throw new AssertionError("Can't call because readNextHeader() returned false"); + checkHeaderReady(); + return mSize; } @Implementation public int readEntityData(byte[] data, int offset, int size) throws IOException { - throw new AssertionError("Can't call because readNextHeader() returned false"); + checkHeaderReady(); + int result = mInput.read(data, offset, size); + if (result < 0) { + throw new IOException("result=0x" + Integer.toHexString(result)); + } + return result; } @Implementation public void skipEntityData() throws IOException { - throw new AssertionError("Can't call because readNextHeader() returned false"); + checkHeaderReady(); + mInput.read(new byte[mSize], 0, mSize); + } + + private void checkHeaderReady() { + if (!mHeaderReady) { + throw new IllegalStateException("Entity header not read"); + } } } diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java index c7deada95929..e78a4b3cdf65 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java @@ -21,16 +21,29 @@ import android.app.backup.BackupDataOutput; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.ObjectOutputStream; +/** + * Shadow for {@link BackupDataOutput}. Format written does NOT match implementation. To read data + * written with this shadow you should also declare shadow {@link ShadowBackupDataInput}. + */ @Implements(BackupDataOutput.class) public class ShadowBackupDataOutput { private long mQuota; private int mTransportFlags; + private ObjectOutputStream mOutput; @Implementation public void __constructor__(FileDescriptor fd, long quota, int transportFlags) { + try { + mOutput = new ObjectOutputStream(new FileOutputStream(fd)); + } catch (IOException e) { + throw new AssertionError(e); + } mQuota = quota; mTransportFlags = transportFlags; } @@ -47,11 +60,27 @@ public class ShadowBackupDataOutput { @Implementation public int writeEntityHeader(String key, int dataSize) throws IOException { - return 0; + final int size; + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { + writeEntityHeader(new ObjectOutputStream(byteStream), key, dataSize); + size = byteStream.size(); + } + writeEntityHeader(mOutput, key, dataSize); + return size; + } + + private void writeEntityHeader(ObjectOutputStream stream, String key, int dataSize) + throws IOException { + // Write the int first because readInt() throws EOFException, to know when stream ends + stream.writeInt(dataSize); + stream.writeUTF(key); + stream.flush(); } @Implementation public int writeEntityData(byte[] data, int size) throws IOException { - return 0; + mOutput.write(data, 0, size); + mOutput.flush(); + return size; } } |