Loading services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +196 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ import android.os.RemoteException; import android.os.SELinux; import android.os.UserHandle; import android.os.WorkSource; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; Loading @@ -65,14 +66,18 @@ import com.android.server.backup.remote.RemoteCall; import com.android.server.backup.remote.RemoteCallable; import com.android.server.backup.remote.RemoteResult; import com.android.server.backup.transport.TransportClient; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.utils.AppBackupUtils; import libcore.io.IoUtils; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.annotation.Retention; Loading @@ -80,8 +85,10 @@ import java.lang.annotation.RetentionPolicy; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -169,10 +176,14 @@ import java.util.concurrent.atomic.AtomicInteger; // TODO: Consider having the caller responsible for some clean-up (like resetting state) // TODO: Distinguish between cancel and time-out where possible for logging/monitoring/observing public class KeyValueBackupTask implements BackupRestoreTask, Runnable { private static final String TAG = "KVBT"; private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND; private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final String BLANK_STATE_FILE_NAME = "blank_state"; private static final String PM_PACKAGE = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL; private static final String SUCCESS_STATE_SUBDIR = "backing-up"; @VisibleForTesting static final String NO_DATA_END_SENTINEL = "@end@"; @VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data"; @VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new"; Loading Loading @@ -336,6 +347,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mHasDataToBackup = false; Set<String> backedUpApps = new HashSet<>(); int status = BackupTransport.TRANSPORT_OK; try { startTask(); Loading @@ -347,13 +359,18 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } else { backupPackage(packageName); } setSuccessState(packageName, true); backedUpApps.add(packageName); } catch (AgentException e) { setSuccessState(packageName, false); if (e.isTransitory()) { // We try again this package in the next backup pass. mBackupManagerService.dataChangedImpl(packageName); } } } informTransportOfUnchangedApps(backedUpApps); } catch (TaskException e) { if (e.isStateCompromised()) { mBackupManagerService.resetBackupState(mStateDirectory); Loading @@ -364,6 +381,185 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { finishTask(status); } /** * Tell the transport about all of the packages which have successfully backed up but * have not informed the framework that they have new data. This allows transports to * differentiate between packages which are not backing data up due to an error and * packages which are not backing up data because nothing has changed. * * The current implementation involves creating a state file when a backup succeeds, * on subsequent runs the existence of the file indicates the backup ran successfully * but there was no data. If a backup fails with an error, or if the package is not * eligible for backup by the transport any more, the status file is removed and the * "no data" message will not be sent to the transport until another successful data * changed backup has succeeded. * * @param appsBackedUp The Set of apps backed up during this run so we can exclude them * from the list of successfully backed up apps that we signal to * the transport have no data. */ private void informTransportOfUnchangedApps(Set<String> appsBackedUp) { String[] succeedingPackages = getSucceedingPackages(); if (succeedingPackages == null) { // Nothing is succeeding, so end early. return; } int flags = BackupTransport.FLAG_DATA_NOT_CHANGED; if (mUserInitiated) { flags |= BackupTransport.FLAG_USER_INITIATED; } boolean noDataPackageEncountered = false; try { IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.informTransportOfEmptyBackups()"); for (String packageName : succeedingPackages) { if (appsBackedUp.contains(packageName)) { Log.v(TAG, "Skipping package which was backed up this time :" + packageName); // Skip packages we backed up in this run. continue; } PackageInfo packageInfo; try { packageInfo = mPackageManager.getPackageInfo(packageName, /* flags */ 0); if (!isEligibleForNoDataCall(packageInfo)) { // If the package isn't eligible any more we can forget about it and move // on. clearStatus(packageName); continue; } } catch (PackageManager.NameNotFoundException e) { // If the package has been uninstalled we can forget about it and move on. clearStatus(packageName); continue; } sendNoDataChangedTo(transport, packageInfo, flags); noDataPackageEncountered = true; } if (noDataPackageEncountered) { // If we've notified the transport of an unchanged package we need to // tell it that it's seen all of the unchanged packages. We do this by // reporting the end sentinel package as unchanged. PackageInfo endSentinal = new PackageInfo(); endSentinal.packageName = NO_DATA_END_SENTINEL; sendNoDataChangedTo(transport, endSentinal, flags); } } catch (TransportNotAvailableException | RemoteException e) { Log.e(TAG, "Could not inform transport of all unchanged apps", e); } } /** Determine if a package is eligible to be backed up to the transport */ private boolean isEligibleForNoDataCall(PackageInfo packageInfo) { return AppBackupUtils.appIsKeyValueOnly(packageInfo) && AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(mTransportClient, packageInfo.packageName, mPackageManager, mUserId); } /** Send the "no data changed" message to a transport for a specific package */ private void sendNoDataChangedTo(IBackupTransport transport, PackageInfo packageInfo, int flags) throws RemoteException { ParcelFileDescriptor pfd; try { pfd = ParcelFileDescriptor.open(mBlankStateFile, MODE_READ_ONLY | MODE_CREATE); } catch (FileNotFoundException e) { Log.e(TAG, "Unable to find blank state file, aborting unchanged apps signal."); return; } try { int result = transport.performBackup(packageInfo, pfd, flags); if (result == BackupTransport.TRANSPORT_ERROR || result == BackupTransport.TRANSPORT_NOT_INITIALIZED) { Log.w( TAG, "Aborting informing transport of unchanged apps, transport" + " errored"); return; } transport.finishBackup(); } finally { IoUtils.closeQuietly(pfd); } } /** Get the list of package names which are marked as having previously succeeded */ private String[] getSucceedingPackages() { File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ false); if (stateDirectory == null) { // getSuccessStateFileFor logs when we can't use the state area return null; } return stateDirectory.list(); } /** Sets the indicator that a package backup is succeeding */ private void setSuccessState(String packageName, boolean success) { File successStateFile = getSuccessStateFileFor(packageName); if (successStateFile == null) { // The error will have been logged by getSuccessStateFileFor(). return; } if (successStateFile.exists() != success) { // If there's been a change of state if (!success) { // Clear the status if we're now failing clearStatus(packageName, successStateFile); return; } // For succeeding packages we want the file try { if (!successStateFile.createNewFile()) { Log.w(TAG, "Unable to permanently record success for " + packageName); } } catch (IOException e) { Log.w(TAG, "Unable to permanently record success for " + packageName, e); } } } /** Clear the status file for a specific package */ private void clearStatus(String packageName) { File successStateFile = getSuccessStateFileFor(packageName); if (successStateFile == null) { // The error will have been logged by getSuccessStateFileFor(). return; } clearStatus(packageName, successStateFile); } /** Clear the status file for a package once we have the File representation */ private void clearStatus(String packageName, File successStateFile) { if (successStateFile.exists()) { if (!successStateFile.delete()) { Log.w(TAG, "Unable to remove status file for " + packageName); } } } /** Get the backup state file for a package **/ private File getSuccessStateFileFor(String packageName) { File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ true); return stateDirectory == null ? null : new File(stateDirectory, packageName); } /** The top level directory for success state files */ private File getTopLevelSuccessStateDirectory(boolean createIfMissing) { File directory = new File(mStateDirectory, SUCCESS_STATE_SUBDIR); if (!directory.exists() && createIfMissing && !directory.mkdirs()) { Log.e(TAG, "Unable to create backing-up state directory"); return null; } return directory; } /** Returns transport status. */ private int sendDataToTransport(@Nullable PackageInfo packageInfo) throws AgentException, TaskException { Loading services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +94 −0 Original line number Diff line number Diff line Loading @@ -131,6 +131,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; Loading Loading @@ -2339,6 +2340,85 @@ public class KeyValueBackupTaskTest { assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(0L); } /** Do not inform transport of an empty backup if the app hasn't backed up before */ @Test public void testRunTask_whenNoDataToBackupOnFirstBackup_doesNotTellTransportOfBackup() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); mBackupManagerService.setCurrentToken(0L); when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); setUpAgent(PACKAGE_1); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1); runTask(task); verify(transportMock.transport, never()) .performBackup( argThat(packageInfo(PACKAGE_1)), any(ParcelFileDescriptor.class), anyInt()); } /** Let the transport know if there are no changes for a KV backed-up package. */ @Test public void testRunTask_whenBackupHasCompletedAndThenNoDataChanges_transportGetsNotified() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); when(transportMock.transport.isAppEligibleForBackup( argThat(packageInfo(PACKAGE_1)), eq(false))) .thenReturn(true); when(transportMock.transport.isAppEligibleForBackup( argThat(packageInfo(PACKAGE_2)), eq(false))) .thenReturn(true); setUpAgentWithData(PACKAGE_1); setUpAgentWithData(PACKAGE_2); PackageInfo endSentinel = new PackageInfo(); endSentinel.packageName = KeyValueBackupTask.NO_DATA_END_SENTINEL; // Perform First Backup run, which should backup both packages KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2); runTask(task); InOrder order = Mockito.inOrder(transportMock.transport); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_1)), any(), eq(BackupTransport.FLAG_NON_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_2)), any(), eq(BackupTransport.FLAG_NON_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); // Run again with new data for package 1, but nothing new for package 2 task = createKeyValueBackupTask(transportMock, PACKAGE_1); runTask(task); // Now for the second run we performed one incremental backup (package 1) and // made one "no change" call (package 2) before sending the end sentinel. order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_1)), any(), eq(BackupTransport.FLAG_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_2)), any(), eq(BackupTransport.FLAG_DATA_NOT_CHANGED)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(endSentinel)), any(), eq(BackupTransport.FLAG_DATA_NOT_CHANGED)); order.verify(transportMock.transport).finishBackup(); order.verifyNoMoreInteractions(); } private void runTask(KeyValueBackupTask task) { // Pretend we are not on the main-thread to prevent RemoteCall from complaining mShadowMainLooper.setCurrentThread(false); Loading Loading @@ -2576,6 +2656,20 @@ public class KeyValueBackupTaskTest { packageInfo != null && packageData.packageName.equals(packageInfo.packageName); } /** Matches {@link PackageInfo} whose package name is {@code packageData.packageName}. */ private static ArgumentMatcher<PackageInfo> packageInfo(PackageInfo packageData) { // We have to test for packageInfo nulity because of Mockito's own stubbing with argThat(). // E.g. if you do: // // 1. when(object.method(argThat(str -> str.equals("foo")))).thenReturn(0) // 2. when(object.method(argThat(str -> str.equals("bar")))).thenReturn(2) // // The second line will throw NPE because it will call lambda 1 with null, since argThat() // returns null. So we guard against that by checking for null. return packageInfo -> packageInfo != null && packageInfo.packageName.equals(packageInfo.packageName); } /** Matches {@link ApplicationInfo} whose package name is {@code packageData.packageName}. */ private static ArgumentMatcher<ApplicationInfo> applicationInfo(PackageData packageData) { return applicationInfo -> Loading Loading
services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +196 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ import android.os.RemoteException; import android.os.SELinux; import android.os.UserHandle; import android.os.WorkSource; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; Loading @@ -65,14 +66,18 @@ import com.android.server.backup.remote.RemoteCall; import com.android.server.backup.remote.RemoteCallable; import com.android.server.backup.remote.RemoteResult; import com.android.server.backup.transport.TransportClient; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.utils.AppBackupUtils; import libcore.io.IoUtils; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.annotation.Retention; Loading @@ -80,8 +85,10 @@ import java.lang.annotation.RetentionPolicy; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -169,10 +176,14 @@ import java.util.concurrent.atomic.AtomicInteger; // TODO: Consider having the caller responsible for some clean-up (like resetting state) // TODO: Distinguish between cancel and time-out where possible for logging/monitoring/observing public class KeyValueBackupTask implements BackupRestoreTask, Runnable { private static final String TAG = "KVBT"; private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND; private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final String BLANK_STATE_FILE_NAME = "blank_state"; private static final String PM_PACKAGE = UserBackupManagerService.PACKAGE_MANAGER_SENTINEL; private static final String SUCCESS_STATE_SUBDIR = "backing-up"; @VisibleForTesting static final String NO_DATA_END_SENTINEL = "@end@"; @VisibleForTesting public static final String STAGING_FILE_SUFFIX = ".data"; @VisibleForTesting public static final String NEW_STATE_FILE_SUFFIX = ".new"; Loading Loading @@ -336,6 +347,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { mHasDataToBackup = false; Set<String> backedUpApps = new HashSet<>(); int status = BackupTransport.TRANSPORT_OK; try { startTask(); Loading @@ -347,13 +359,18 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } else { backupPackage(packageName); } setSuccessState(packageName, true); backedUpApps.add(packageName); } catch (AgentException e) { setSuccessState(packageName, false); if (e.isTransitory()) { // We try again this package in the next backup pass. mBackupManagerService.dataChangedImpl(packageName); } } } informTransportOfUnchangedApps(backedUpApps); } catch (TaskException e) { if (e.isStateCompromised()) { mBackupManagerService.resetBackupState(mStateDirectory); Loading @@ -364,6 +381,185 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { finishTask(status); } /** * Tell the transport about all of the packages which have successfully backed up but * have not informed the framework that they have new data. This allows transports to * differentiate between packages which are not backing data up due to an error and * packages which are not backing up data because nothing has changed. * * The current implementation involves creating a state file when a backup succeeds, * on subsequent runs the existence of the file indicates the backup ran successfully * but there was no data. If a backup fails with an error, or if the package is not * eligible for backup by the transport any more, the status file is removed and the * "no data" message will not be sent to the transport until another successful data * changed backup has succeeded. * * @param appsBackedUp The Set of apps backed up during this run so we can exclude them * from the list of successfully backed up apps that we signal to * the transport have no data. */ private void informTransportOfUnchangedApps(Set<String> appsBackedUp) { String[] succeedingPackages = getSucceedingPackages(); if (succeedingPackages == null) { // Nothing is succeeding, so end early. return; } int flags = BackupTransport.FLAG_DATA_NOT_CHANGED; if (mUserInitiated) { flags |= BackupTransport.FLAG_USER_INITIATED; } boolean noDataPackageEncountered = false; try { IBackupTransport transport = mTransportClient.connectOrThrow("KVBT.informTransportOfEmptyBackups()"); for (String packageName : succeedingPackages) { if (appsBackedUp.contains(packageName)) { Log.v(TAG, "Skipping package which was backed up this time :" + packageName); // Skip packages we backed up in this run. continue; } PackageInfo packageInfo; try { packageInfo = mPackageManager.getPackageInfo(packageName, /* flags */ 0); if (!isEligibleForNoDataCall(packageInfo)) { // If the package isn't eligible any more we can forget about it and move // on. clearStatus(packageName); continue; } } catch (PackageManager.NameNotFoundException e) { // If the package has been uninstalled we can forget about it and move on. clearStatus(packageName); continue; } sendNoDataChangedTo(transport, packageInfo, flags); noDataPackageEncountered = true; } if (noDataPackageEncountered) { // If we've notified the transport of an unchanged package we need to // tell it that it's seen all of the unchanged packages. We do this by // reporting the end sentinel package as unchanged. PackageInfo endSentinal = new PackageInfo(); endSentinal.packageName = NO_DATA_END_SENTINEL; sendNoDataChangedTo(transport, endSentinal, flags); } } catch (TransportNotAvailableException | RemoteException e) { Log.e(TAG, "Could not inform transport of all unchanged apps", e); } } /** Determine if a package is eligible to be backed up to the transport */ private boolean isEligibleForNoDataCall(PackageInfo packageInfo) { return AppBackupUtils.appIsKeyValueOnly(packageInfo) && AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(mTransportClient, packageInfo.packageName, mPackageManager, mUserId); } /** Send the "no data changed" message to a transport for a specific package */ private void sendNoDataChangedTo(IBackupTransport transport, PackageInfo packageInfo, int flags) throws RemoteException { ParcelFileDescriptor pfd; try { pfd = ParcelFileDescriptor.open(mBlankStateFile, MODE_READ_ONLY | MODE_CREATE); } catch (FileNotFoundException e) { Log.e(TAG, "Unable to find blank state file, aborting unchanged apps signal."); return; } try { int result = transport.performBackup(packageInfo, pfd, flags); if (result == BackupTransport.TRANSPORT_ERROR || result == BackupTransport.TRANSPORT_NOT_INITIALIZED) { Log.w( TAG, "Aborting informing transport of unchanged apps, transport" + " errored"); return; } transport.finishBackup(); } finally { IoUtils.closeQuietly(pfd); } } /** Get the list of package names which are marked as having previously succeeded */ private String[] getSucceedingPackages() { File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ false); if (stateDirectory == null) { // getSuccessStateFileFor logs when we can't use the state area return null; } return stateDirectory.list(); } /** Sets the indicator that a package backup is succeeding */ private void setSuccessState(String packageName, boolean success) { File successStateFile = getSuccessStateFileFor(packageName); if (successStateFile == null) { // The error will have been logged by getSuccessStateFileFor(). return; } if (successStateFile.exists() != success) { // If there's been a change of state if (!success) { // Clear the status if we're now failing clearStatus(packageName, successStateFile); return; } // For succeeding packages we want the file try { if (!successStateFile.createNewFile()) { Log.w(TAG, "Unable to permanently record success for " + packageName); } } catch (IOException e) { Log.w(TAG, "Unable to permanently record success for " + packageName, e); } } } /** Clear the status file for a specific package */ private void clearStatus(String packageName) { File successStateFile = getSuccessStateFileFor(packageName); if (successStateFile == null) { // The error will have been logged by getSuccessStateFileFor(). return; } clearStatus(packageName, successStateFile); } /** Clear the status file for a package once we have the File representation */ private void clearStatus(String packageName, File successStateFile) { if (successStateFile.exists()) { if (!successStateFile.delete()) { Log.w(TAG, "Unable to remove status file for " + packageName); } } } /** Get the backup state file for a package **/ private File getSuccessStateFileFor(String packageName) { File stateDirectory = getTopLevelSuccessStateDirectory(/* createIfMissing */ true); return stateDirectory == null ? null : new File(stateDirectory, packageName); } /** The top level directory for success state files */ private File getTopLevelSuccessStateDirectory(boolean createIfMissing) { File directory = new File(mStateDirectory, SUCCESS_STATE_SUBDIR); if (!directory.exists() && createIfMissing && !directory.mkdirs()) { Log.e(TAG, "Unable to create backing-up state directory"); return null; } return directory; } /** Returns transport status. */ private int sendDataToTransport(@Nullable PackageInfo packageInfo) throws AgentException, TaskException { Loading
services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +94 −0 Original line number Diff line number Diff line Loading @@ -131,6 +131,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; Loading Loading @@ -2339,6 +2340,85 @@ public class KeyValueBackupTaskTest { assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(0L); } /** Do not inform transport of an empty backup if the app hasn't backed up before */ @Test public void testRunTask_whenNoDataToBackupOnFirstBackup_doesNotTellTransportOfBackup() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); mBackupManagerService.setCurrentToken(0L); when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); setUpAgent(PACKAGE_1); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1); runTask(task); verify(transportMock.transport, never()) .performBackup( argThat(packageInfo(PACKAGE_1)), any(ParcelFileDescriptor.class), anyInt()); } /** Let the transport know if there are no changes for a KV backed-up package. */ @Test public void testRunTask_whenBackupHasCompletedAndThenNoDataChanges_transportGetsNotified() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); when(transportMock.transport.getCurrentRestoreSet()).thenReturn(1234L); when(transportMock.transport.isAppEligibleForBackup( argThat(packageInfo(PACKAGE_1)), eq(false))) .thenReturn(true); when(transportMock.transport.isAppEligibleForBackup( argThat(packageInfo(PACKAGE_2)), eq(false))) .thenReturn(true); setUpAgentWithData(PACKAGE_1); setUpAgentWithData(PACKAGE_2); PackageInfo endSentinel = new PackageInfo(); endSentinel.packageName = KeyValueBackupTask.NO_DATA_END_SENTINEL; // Perform First Backup run, which should backup both packages KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1, PACKAGE_2); runTask(task); InOrder order = Mockito.inOrder(transportMock.transport); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_1)), any(), eq(BackupTransport.FLAG_NON_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_2)), any(), eq(BackupTransport.FLAG_NON_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); // Run again with new data for package 1, but nothing new for package 2 task = createKeyValueBackupTask(transportMock, PACKAGE_1); runTask(task); // Now for the second run we performed one incremental backup (package 1) and // made one "no change" call (package 2) before sending the end sentinel. order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_1)), any(), eq(BackupTransport.FLAG_INCREMENTAL)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(PACKAGE_2)), any(), eq(BackupTransport.FLAG_DATA_NOT_CHANGED)); order.verify(transportMock.transport).finishBackup(); order.verify(transportMock.transport) .performBackup( argThat(packageInfo(endSentinel)), any(), eq(BackupTransport.FLAG_DATA_NOT_CHANGED)); order.verify(transportMock.transport).finishBackup(); order.verifyNoMoreInteractions(); } private void runTask(KeyValueBackupTask task) { // Pretend we are not on the main-thread to prevent RemoteCall from complaining mShadowMainLooper.setCurrentThread(false); Loading Loading @@ -2576,6 +2656,20 @@ public class KeyValueBackupTaskTest { packageInfo != null && packageData.packageName.equals(packageInfo.packageName); } /** Matches {@link PackageInfo} whose package name is {@code packageData.packageName}. */ private static ArgumentMatcher<PackageInfo> packageInfo(PackageInfo packageData) { // We have to test for packageInfo nulity because of Mockito's own stubbing with argThat(). // E.g. if you do: // // 1. when(object.method(argThat(str -> str.equals("foo")))).thenReturn(0) // 2. when(object.method(argThat(str -> str.equals("bar")))).thenReturn(2) // // The second line will throw NPE because it will call lambda 1 with null, since argThat() // returns null. So we guard against that by checking for null. return packageInfo -> packageInfo != null && packageInfo.packageName.equals(packageInfo.packageName); } /** Matches {@link ApplicationInfo} whose package name is {@code packageData.packageName}. */ private static ArgumentMatcher<ApplicationInfo> applicationInfo(PackageData packageData) { return applicationInfo -> Loading