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

Commit 42d0c44a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Extract class AppsBackedUpOnThisDeviceJournal from BackupManagerService"

parents 907b571b 6d2fbdf1
Loading
Loading
Loading
Loading
+125 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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;

import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;

import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashSet;

/**
 * Records which apps have been backed up on this device, persisting it to disk so that it can be
 * read at subsequent boots. This class is threadsafe.
 *
 * <p>This is used to decide, when restoring a package at install time, whether it has been
 * previously backed up on the current device. If it has been previously backed up it should
 * restore from the same restore set that the current device has been backing up to. If it has not
 * been previously backed up, it should restore from the ancestral restore set (i.e., the restore
 * set that the user's previous device was backing up to).
 *
 * <p>NB: this is always backed by the same files within the state directory supplied at
 * construction.
 */
final class AppsBackedUpOnThisDeviceJournal {
    private static final String TAG = "AppsBackedUpOnThisDeviceJournal";
    private static final String JOURNAL_FILE_NAME = "processed";

    @GuardedBy("this")
    private final HashSet<String> mProcessedPackages = new HashSet<>();
    private final File mStateDirectory;

    /**
     * Constructs a new journal, loading state from disk if it has been previously persisted.
     *
     * @param stateDirectory The directory in which backup state (including journals) is stored.
     */
    AppsBackedUpOnThisDeviceJournal(File stateDirectory) {
        mStateDirectory = stateDirectory;
        loadFromDisk();
    }

    /**
     * Returns {@code true} if {@code packageName} has previously been backed up.
     */
    synchronized boolean hasBeenProcessed(String packageName) {
        return mProcessedPackages.contains(packageName);
    }

    synchronized void addPackage(String packageName) {
        if (!mProcessedPackages.add(packageName)) {
            // This package has already been processed - no need to add it to the journal.
            return;
        }

        File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);

        try (RandomAccessFile out = new RandomAccessFile(journalFile, "rws")) {
            out.seek(out.length());
            out.writeUTF(packageName);
        } catch (IOException e) {
            Slog.e(TAG, "Can't log backup of " + packageName + " to " + journalFile);
        }
    }

    /**
     * A copy of the current state of the journal.
     *
     * <p>Used only for dumping out information for logging. {@link #hasBeenProcessed(String)}
     * should be used for efficiently checking whether a package has been backed up before by this
     * device.
     *
     * @return The current set of packages that have been backed up previously.
     */
    synchronized HashSet<String> getPackagesCopy() {
        return new HashSet<>(mProcessedPackages);
    }

    synchronized void reset() {
        mProcessedPackages.clear();
        File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);
        journalFile.delete();
    }

    private void loadFromDisk() {
        File journalFile = new File(mStateDirectory, JOURNAL_FILE_NAME);

        if (!journalFile.exists()) {
            return;
        }

        try (RandomAccessFile oldJournal = new RandomAccessFile(journalFile, "r")) {
            while (true) {
                String packageName = oldJournal.readUTF();
                if (DEBUG) {
                    Slog.v(TAG, "   + " + packageName);
                }
                mProcessedPackages.add(packageName);
            }
        } catch (EOFException e) {
            // Successfully loaded journal file
        } catch (IOException e) {
            Slog.e(TAG, "Error reading processed packages journal", e);
        }
    }
}
+10 −100
Original line number Original line Diff line number Diff line
@@ -27,7 +27,6 @@ import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_CLEAR;
import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_INIT;
import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_INIT;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_BACKUP;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_BACKUP;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_RESTORE;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_RESTORE;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_BACKUP;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_CLEAR;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_CLEAR;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
import static com.android.server.backup.internal.BackupHandler.MSG_SCHEDULE_BACKUP_PACKAGE;
import static com.android.server.backup.internal.BackupHandler.MSG_SCHEDULE_BACKUP_PACKAGE;
@@ -127,14 +126,12 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
import java.security.SecureRandom;
@@ -630,11 +627,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter


    private final SecureRandom mRng = new SecureRandom();
    private final SecureRandom mRng = new SecureRandom();


    // Keep a log of all the apps we've ever backed up, and what the
    // Keep a log of all the apps we've ever backed up, and what the dataset tokens are for both
    // dataset tokens are for both the current backup dataset and
    // the current backup dataset and the ancestral dataset.
    // the ancestral dataset.
    private AppsBackedUpOnThisDeviceJournal mAppsBackedUpOnThisDeviceJournal;
    private File mEverStored;
    private HashSet<String> mEverStoredApps = new HashSet<>();


    private static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1;
    private static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1;
    // increment when the schema changes
    // increment when the schema changes
@@ -821,49 +816,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
            Slog.w(TAG, "Unable to read token file", e);
            Slog.w(TAG, "Unable to read token file", e);
        }
        }


        // Keep a log of what apps we've ever backed up.  Because we might have
        mAppsBackedUpOnThisDeviceJournal = new AppsBackedUpOnThisDeviceJournal(mBaseStateDir);
        // rebooted in the middle of an operation that was removing something from
        // this log, we sanity-check its contents here and reconstruct it.
        mEverStored = new File(mBaseStateDir, "processed");
        File tempProcessedFile = new File(mBaseStateDir, "processed.new");

        // If we were in the middle of removing something from the ever-backed-up
        // file, there might be a transient "processed.new" file still present.
        // Ignore it -- we'll validate "processed" against the current package set.
        if (tempProcessedFile.exists()) {
            tempProcessedFile.delete();
        }

        // If there are previous contents, parse them out then start a new
        // file to continue the recordkeeping.
        if (mEverStored.exists()) {
            try (RandomAccessFile temp = new RandomAccessFile(tempProcessedFile, "rws");
                 RandomAccessFile in = new RandomAccessFile(mEverStored, "r")) {
                // Loop until we hit EOF
                while (true) {
                    String pkg = in.readUTF();
                    try {
                        // is this package still present?
                        mPackageManager.getPackageInfo(pkg, 0);
                        // if we get here then yes it is; remember it
                        mEverStoredApps.add(pkg);
                        temp.writeUTF(pkg);
                        if (MORE_DEBUG) Slog.v(TAG, "   + " + pkg);
                    } catch (NameNotFoundException e) {
                        // nope, this package was uninstalled; don't include it
                        if (MORE_DEBUG) Slog.v(TAG, "   - " + pkg);
                    }
                }
            } catch (EOFException e) {
                // Once we've rewritten the backup history log, atomically replace the
                // old one with the new one then reopen the file for continuing use.
                if (!tempProcessedFile.renameTo(mEverStored)) {
                    Slog.e(TAG, "Error renaming " + tempProcessedFile + " to " + mEverStored);
                }
            } catch (IOException e) {
                Slog.e(TAG, "Error in processed file", e);
            }
        }


        synchronized (mQueueLock) {
        synchronized (mQueueLock) {
            // Resume the full-data backup queue
            // Resume the full-data backup queue
@@ -1115,9 +1068,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    // so we must re-upload all saved settings.
    // so we must re-upload all saved settings.
    public void resetBackupState(File stateFileDir) {
    public void resetBackupState(File stateFileDir) {
        synchronized (mQueueLock) {
        synchronized (mQueueLock) {
            // Wipe the "what we've ever backed up" tracking
            mAppsBackedUpOnThisDeviceJournal.reset();
            mEverStoredApps.clear();
            mEverStored.delete();


            mCurrentToken = 0;
            mCurrentToken = 0;
            writeRestoreTokens();
            writeRestoreTokens();
@@ -1412,49 +1363,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
    public void logBackupComplete(String packageName) {
    public void logBackupComplete(String packageName) {
        if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return;
        if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return;


        synchronized (mEverStoredApps) {
        mAppsBackedUpOnThisDeviceJournal.addPackage(packageName);
            if (!mEverStoredApps.add(packageName)) return;

            try (RandomAccessFile out = new RandomAccessFile(mEverStored, "rws")) {
                out.seek(out.length());
                out.writeUTF(packageName);
            } catch (IOException e) {
                Slog.e(TAG, "Can't log backup of " + packageName + " to " + mEverStored);
            }
        }
    }

    // Remove our awareness of having ever backed up the given package
    void removeEverBackedUp(String packageName) {
        if (DEBUG) Slog.v(TAG, "Removing backed-up knowledge of " + packageName);
        if (MORE_DEBUG) Slog.v(TAG, "New set:");

        synchronized (mEverStoredApps) {
            // Rewrite the file and rename to overwrite.  If we reboot in the middle,
            // we'll recognize on initialization time that the package no longer
            // exists and fix it up then.
            File tempKnownFile = new File(mBaseStateDir, "processed.new");
            try (RandomAccessFile known = new RandomAccessFile(tempKnownFile, "rws")) {
                mEverStoredApps.remove(packageName);
                for (String s : mEverStoredApps) {
                    known.writeUTF(s);
                    if (MORE_DEBUG) Slog.v(TAG, "    " + s);
                }
                known.close();
                if (!tempKnownFile.renameTo(mEverStored)) {
                    throw new IOException("Can't rename " + tempKnownFile + " to " + mEverStored);
                }
            } catch (IOException e) {
                // Bad: we couldn't create the new copy.  For safety's sake we
                // abandon the whole process and remove all what's-backed-up
                // state entirely, meaning we'll force a backup pass for every
                // participant on the next boot or [re]install.
                Slog.w(TAG, "Error rewriting " + mEverStored, e);
                mEverStoredApps.clear();
                tempKnownFile.delete();
                mEverStored.delete();
            }
        }
    }
    }


    // Persistently record the current and ancestral backup tokens as well
    // Persistently record the current and ancestral backup tokens as well
@@ -1591,7 +1500,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter


        long token = mAncestralToken;
        long token = mAncestralToken;
        synchronized (mQueueLock) {
        synchronized (mQueueLock) {
            if (mEverStoredApps.contains(packageName)) {
            if (mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(packageName)) {
                if (MORE_DEBUG) {
                if (MORE_DEBUG) {
                    Slog.i(TAG, "App in ever-stored, so using current token");
                    Slog.i(TAG, "App in ever-stored, so using current token");
                }
                }
@@ -3394,8 +3303,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
                }
                }
            }
            }


            pw.println("Ever backed up: " + mEverStoredApps.size());
            HashSet<String> processedApps = mAppsBackedUpOnThisDeviceJournal.getPackagesCopy();
            for (String pkg : mEverStoredApps) {
            pw.println("Ever backed up: " + processedApps.size());
            for (String pkg : processedApps) {
                pw.println("    " + pkg);
                pw.println("    " + pkg);
            }
            }


+163 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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;

import static com.google.common.truth.Truth.assertThat;

import android.platform.test.annotations.Presubmit;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import com.google.android.collect.Sets;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashSet;
import java.util.Set;

@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class AppsBackedUpOnThisDeviceJournalTest {
    private static final String JOURNAL_FILE_NAME = "processed";

    private static final String GOOGLE_PHOTOS = "com.google.photos";
    private static final String GMAIL = "com.google.gmail";
    private static final String GOOGLE_PLUS = "com.google.plus";

    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();

    private File mStateDirectory;
    private AppsBackedUpOnThisDeviceJournal mAppsBackedUpOnThisDeviceJournal;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mStateDirectory = mTemporaryFolder.newFolder();
        mAppsBackedUpOnThisDeviceJournal = new AppsBackedUpOnThisDeviceJournal(mStateDirectory);
    }

    @Test
    public void constructor_loadsAnyPreviousJournalFromDisk() throws Exception {
        writePermanentJournalPackages(Sets.newHashSet(GOOGLE_PHOTOS, GMAIL));

        AppsBackedUpOnThisDeviceJournal journalFromDisk =
                new AppsBackedUpOnThisDeviceJournal(mStateDirectory);

        assertThat(journalFromDisk.hasBeenProcessed(GOOGLE_PHOTOS)).isTrue();
        assertThat(journalFromDisk.hasBeenProcessed(GMAIL)).isTrue();
    }

    @Test
    public void hasBeenProcessed_isFalseForAnyPackageFromBlankInit() {
        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GOOGLE_PHOTOS)).isFalse();
        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GMAIL)).isFalse();
        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GOOGLE_PLUS)).isFalse();
    }

    @Test
    public void addPackage_addsPackageToObjectState() {
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PHOTOS);

        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GOOGLE_PHOTOS)).isTrue();
    }

    @Test
    public void addPackage_addsPackageToFileSystem() throws Exception {
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PHOTOS);

        assertThat(readJournalPackages()).contains(GOOGLE_PHOTOS);
    }

    @Test
    public void getPackagesCopy_returnsTheCurrentState() throws Exception {
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PHOTOS);
        mAppsBackedUpOnThisDeviceJournal.addPackage(GMAIL);

        assertThat(mAppsBackedUpOnThisDeviceJournal.getPackagesCopy())
                .isEqualTo(Sets.newHashSet(GOOGLE_PHOTOS, GMAIL));
    }

    @Test
    public void getPackagesCopy_returnsACopy() throws Exception {
        mAppsBackedUpOnThisDeviceJournal.getPackagesCopy().add(GMAIL);

        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GMAIL)).isFalse();
    }

    @Test
    public void reset_removesAllPackagesFromObjectState() {
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PHOTOS);
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PLUS);
        mAppsBackedUpOnThisDeviceJournal.addPackage(GMAIL);

        mAppsBackedUpOnThisDeviceJournal.reset();

        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GOOGLE_PHOTOS)).isFalse();
        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GMAIL)).isFalse();
        assertThat(mAppsBackedUpOnThisDeviceJournal.hasBeenProcessed(GOOGLE_PLUS)).isFalse();
    }

    @Test
    public void reset_removesAllPackagesFromFileSystem() throws Exception {
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PHOTOS);
        mAppsBackedUpOnThisDeviceJournal.addPackage(GOOGLE_PLUS);
        mAppsBackedUpOnThisDeviceJournal.addPackage(GMAIL);

        mAppsBackedUpOnThisDeviceJournal.reset();

        assertThat(readJournalPackages()).isEmpty();
    }

    private HashSet<String> readJournalPackages() throws Exception {
        File journal = new File(mStateDirectory, JOURNAL_FILE_NAME);
        HashSet<String> packages = new HashSet<>();

        try (FileInputStream fis = new FileInputStream(journal);
             DataInputStream dis = new DataInputStream(fis)) {
            while (dis.available() > 0) {
                packages.add(dis.readUTF());
            }
        } catch (FileNotFoundException e) {
            return new HashSet<>();
        }

        return packages;
    }

    private void writePermanentJournalPackages(Set<String> packages) throws Exception {
        File journal = new File(mStateDirectory, JOURNAL_FILE_NAME);

        try (FileOutputStream fos = new FileOutputStream(journal);
             DataOutputStream dos = new DataOutputStream(fos)) {
            for (String packageName : packages) {
                dos.writeUTF(packageName);
            }
        }
    }
}