Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

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
Loading
Loading
Loading
Loading
+196 −0
Original line number Diff line number Diff line
@@ -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 {
+94 −0
Original line number Diff line number Diff line
@@ -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 ->