Skip to content
Snippets Groups Projects
Commit 40a7ffe9 authored by Al Sutton's avatar Al Sutton Committed by Android (Google) Code Review
Browse files

Merge "Notify transports of empty K/V backups"

parents 1beddee4 7ab8dc31
No related branches found
No related tags found
No related merge requests found
......@@ -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;
......@@ -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;
......@@ -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;
......@@ -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";
......@@ -336,6 +347,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable {
mHasDataToBackup = false;
Set<String> backedUpApps = new HashSet<>();
int status = BackupTransport.TRANSPORT_OK;
try {
startTask();
......@@ -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);
......@@ -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 {
......
......@@ -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;
......@@ -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);
......@@ -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 ->
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment