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

Commit 3dc25cca authored by Joël Stemmer's avatar Joël Stemmer
Browse files

Write tests for PackageManagerBackupAgent

Change-Id: I28490272c9e739802288f40078432750fc985fc6

Bug: 277594991
Test: atest PackageManagerBackupAgentTest.java
Change-Id: I13744aaf54e546ab696f98cd775a4b93da6bc4d5
parent 234ef02d
Loading
Loading
Loading
Loading
+8 −6
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.utils.BackupEligibilityRules;

import java.io.BufferedInputStream;
@@ -62,7 +63,7 @@ public class PackageManagerBackupAgent extends BackupAgent {

    // key under which we store global metadata (individual app metadata
    // is stored using the package name as a key)
    private static final String GLOBAL_METADATA_KEY = "@meta@";
    @VisibleForTesting static final String GLOBAL_METADATA_KEY = "@meta@";

    // key under which we store the identity of the user's chosen default home app
    private static final String DEFAULT_HOME_KEY = "@home@";
@@ -72,19 +73,19 @@ public class PackageManagerBackupAgent extends BackupAgent {
    // ANCESTRAL_RECORD_VERSION=1 (introduced Android P).
    // Should the ANCESTRAL_RECORD_VERSION be bumped up in the future, STATE_FILE_VERSION will also
    // need bumping up, assuming more data needs saving to the state file.
    private static final String STATE_FILE_HEADER = "=state=";
    private static final int STATE_FILE_VERSION = 2;
    @VisibleForTesting static final String STATE_FILE_HEADER = "=state=";
    @VisibleForTesting static final int STATE_FILE_VERSION = 2;

    // key under which we store the saved ancestral-dataset format (starting from Android P)
    // IMPORTANT: this key needs to come first in the restore data stream (to find out
    // whether this version of Android knows how to restore the incoming data set), so it needs
    // to be always the first one in alphabetical order of all the keys
    private static final String ANCESTRAL_RECORD_KEY = "@ancestral_record@";
    @VisibleForTesting static final String ANCESTRAL_RECORD_KEY = "@ancestral_record@";

    // Current version of the saved ancestral-dataset format
    // Note that this constant was not used until Android P, and started being used
    // to version @pm@ data for forwards-compatibility.
    private static final int ANCESTRAL_RECORD_VERSION = 1;
    @VisibleForTesting static final int ANCESTRAL_RECORD_VERSION = 1;

    // Undefined version of the saved ancestral-dataset file format means that the restore data
    // is coming from pre-Android P device.
@@ -593,7 +594,8 @@ public class PackageManagerBackupAgent extends BackupAgent {
    }

    // Util: write out our new backup state file
    private void writeStateFile(List<PackageInfo> pkgs, ParcelFileDescriptor stateFile) {
    @VisibleForTesting
    static void writeStateFile(List<PackageInfo> pkgs, ParcelFileDescriptor stateFile) {
        FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
        BufferedOutputStream outbuf = new BufferedOutputStream(outstream);
        DataOutputStream out = new DataOutputStream(outbuf);
+288 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 androidx.test.core.app.ApplicationProvider.getApplicationContext;

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

import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.Presubmit;

import androidx.test.runner.AndroidJUnit4;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

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

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.util.Optional;

@Presubmit
@RunWith(AndroidJUnit4.class)
public class PackageManagerBackupAgentTest {

    private static final String EXISTING_PACKAGE_NAME = "com.android.wallpaperbackup";
    private static final int USER_ID = 0;

    @Rule public TemporaryFolder folder = new TemporaryFolder();

    private PackageManagerBackupAgent mPackageManagerBackupAgent;
    private ImmutableList<PackageInfo> mPackages;
    private File mBackupData, mOldState, mNewState;

    @Before
    public void setUp() throws Exception {
        PackageManager packageManager = getApplicationContext().getPackageManager();

        PackageInfo existingPackageInfo =
                packageManager.getPackageInfoAsUser(
                        EXISTING_PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES, USER_ID);
        mPackages = ImmutableList.of(existingPackageInfo);
        mPackageManagerBackupAgent =
                new PackageManagerBackupAgent(packageManager, mPackages, USER_ID);

        mBackupData = folder.newFile("backup_data");
        mOldState = folder.newFile("old_state");
        mNewState = folder.newFile("new_state");
    }

    @Test
    public void onBackup_noState_backsUpEverything() throws Exception {
        // no setup needed

        runBackupAgentOnBackup();

        // key/values should be written to backup data
        ImmutableMap<String, Optional<ByteBuffer>> keyValues = getKeyValues(mBackupData);
        assertThat(keyValues.keySet())
                .containsExactly(
                        PackageManagerBackupAgent.ANCESTRAL_RECORD_KEY,
                        PackageManagerBackupAgent.GLOBAL_METADATA_KEY,
                        EXISTING_PACKAGE_NAME)
                .inOrder();
        // new state must not be empty
        assertThat(mNewState.length()).isGreaterThan(0);
    }

    @Test
    public void onBackup_recentState_backsUpNothing() throws Exception {
        try (ParcelFileDescriptor oldStateDescriptor = openForWriting(mOldState)) {
            PackageManagerBackupAgent.writeStateFile(mPackages, oldStateDescriptor);
        }

        runBackupAgentOnBackup();

        // We shouldn't have written anything, but a known issue is that we always write the
        // ancestral record version.
        ImmutableMap<String, Optional<ByteBuffer>> keyValues = getKeyValues(mBackupData);
        assertThat(keyValues.keySet())
                .containsExactly(PackageManagerBackupAgent.ANCESTRAL_RECORD_KEY);
        assertThat(mNewState.length()).isGreaterThan(0);
        assertThat(mNewState.length()).isEqualTo(mOldState.length());
    }

    @Test
    public void onBackup_oldState_backsUpChanges() throws Exception {
        String uninstalledPackageName = "does.not.exist";
        try (ParcelFileDescriptor oldStateDescriptor = openForWriting(mOldState)) {
            PackageManagerBackupAgent.writeStateFile(
                    ImmutableList.of(createPackage(uninstalledPackageName, 1)), oldStateDescriptor);
        }

        runBackupAgentOnBackup();

        // Note that uninstalledPackageName should not exist, i.e. it did not get deleted.
        ImmutableMap<String, Optional<ByteBuffer>> keyValues = getKeyValues(mBackupData);
        assertThat(keyValues.keySet())
                .containsExactly(
                        PackageManagerBackupAgent.ANCESTRAL_RECORD_KEY, EXISTING_PACKAGE_NAME);
        assertThat(mNewState.length()).isGreaterThan(0);
    }

    @Test
    public void onBackup_legacyState_backsUpEverything() throws Exception {
        String uninstalledPackageName = "does.not.exist";
        writeLegacyStateFile(
                mOldState,
                ImmutableList.of(createPackage(uninstalledPackageName, 1), mPackages.getFirst()));

        runBackupAgentOnBackup();

        ImmutableMap<String, Optional<ByteBuffer>> keyValues = getKeyValues(mBackupData);
        assertThat(keyValues.keySet())
                .containsExactly(
                        PackageManagerBackupAgent.ANCESTRAL_RECORD_KEY,
                        PackageManagerBackupAgent.GLOBAL_METADATA_KEY,
                        EXISTING_PACKAGE_NAME);
        assertThat(mNewState.length()).isGreaterThan(0);
    }

    @Test
    public void onRestore_recentBackup_restoresBackup() throws Exception {
        runBackupAgentOnBackup();

        runBackupAgentOnRestore();

        assertThat(mPackageManagerBackupAgent.getRestoredPackages())
                .containsExactly(EXISTING_PACKAGE_NAME);
        // onRestore does not write to newState
        assertThat(mNewState.length()).isEqualTo(0);
    }

    @Test
    public void onRestore_legacyBackup_restoresBackup() throws Exception {
        // A legacy backup is one without an ancestral record version. Ancestral record versions
        // are always written however, so we'll need to delete it from the backup data before
        // restoring.
        runBackupAgentOnBackup();
        deleteKeyFromBackupData(mBackupData, PackageManagerBackupAgent.ANCESTRAL_RECORD_KEY);

        runBackupAgentOnRestore();

        assertThat(mPackageManagerBackupAgent.getRestoredPackages())
                .containsExactly(EXISTING_PACKAGE_NAME);
        // onRestore does not write to newState
        assertThat(mNewState.length()).isEqualTo(0);
    }

    private void runBackupAgentOnBackup() throws Exception {
        try (ParcelFileDescriptor oldStateDescriptor = openForReading(mOldState);
                ParcelFileDescriptor backupDataDescriptor = openForWriting(mBackupData);
                ParcelFileDescriptor newStateDescriptor = openForWriting(mNewState)) {
            mPackageManagerBackupAgent.onBackup(
                    oldStateDescriptor,
                    new BackupDataOutput(backupDataDescriptor.getFileDescriptor()),
                    newStateDescriptor);
        }
    }

    private void runBackupAgentOnRestore() throws Exception {
        try (ParcelFileDescriptor backupDataDescriptor = openForReading(mBackupData);
                ParcelFileDescriptor newStateDescriptor = openForWriting(mNewState)) {
            mPackageManagerBackupAgent.onRestore(
                    new BackupDataInput(backupDataDescriptor.getFileDescriptor()),
                    /* appVersionCode= */ 0,
                    newStateDescriptor);
        }
    }

    private void deleteKeyFromBackupData(File backupData, String key) throws Exception {
        File temporaryBackupData = folder.newFile("backup_data.tmp");
        try (ParcelFileDescriptor inputDescriptor = openForReading(backupData);
                ParcelFileDescriptor outputDescriptor = openForWriting(temporaryBackupData); ) {
            BackupDataInput input = new BackupDataInput(inputDescriptor.getFileDescriptor());
            BackupDataOutput output = new BackupDataOutput(outputDescriptor.getFileDescriptor());
            while (input.readNextHeader()) {
                if (input.getKey().equals(key)) {
                    if (input.getDataSize() > 0) {
                        input.skipEntityData();
                    }
                    continue;
                }
                output.writeEntityHeader(input.getKey(), input.getDataSize());
                if (input.getDataSize() < 0) {
                    input.skipEntityData();
                } else {
                    byte[] buf = new byte[input.getDataSize()];
                    input.readEntityData(buf, 0, buf.length);
                    output.writeEntityData(buf, buf.length);
                }
            }
        }
        assertThat(temporaryBackupData.renameTo(backupData)).isTrue();
    }

    private static PackageInfo createPackage(String name, int versionCode) {
        PackageInfo packageInfo = new PackageInfo();
        packageInfo.packageName = name;
        packageInfo.versionCodeMajor = versionCode;
        return packageInfo;
    }

    /** This creates a legacy state file in which {@code STATE_FILE_HEADER} was not yet present. */
    private static void writeLegacyStateFile(File stateFile, ImmutableList<PackageInfo> packages)
            throws Exception {
        try (ParcelFileDescriptor stateFileDescriptor = openForWriting(stateFile);
                DataOutputStream out =
                        new DataOutputStream(
                                new BufferedOutputStream(
                                        new FileOutputStream(
                                                stateFileDescriptor.getFileDescriptor())))) {
            out.writeUTF(PackageManagerBackupAgent.GLOBAL_METADATA_KEY);
            out.writeInt(Build.VERSION.SDK_INT);
            out.writeUTF(Build.VERSION.INCREMENTAL);

            // now write all the app names + versions
            for (PackageInfo pkg : packages) {
                out.writeUTF(pkg.packageName);
                out.writeInt(pkg.versionCode);
            }
            out.flush();
        }
    }

    /**
     * Reads the given backup data file and returns a map of key-value pairs. The value is a {@link
     * ByteBuffer} wrapped in an {@link Optional}, where the empty {@link Optional} represents a key
     * deletion.
     */
    private static ImmutableMap<String, Optional<ByteBuffer>> getKeyValues(File backupData)
            throws Exception {
        ImmutableMap.Builder<String, Optional<ByteBuffer>> builder = ImmutableMap.builder();
        try (ParcelFileDescriptor backupDataDescriptor = openForReading(backupData)) {
            BackupDataInput backupDataInput =
                    new BackupDataInput(backupDataDescriptor.getFileDescriptor());
            while (backupDataInput.readNextHeader()) {
                ByteBuffer value = null;
                if (backupDataInput.getDataSize() >= 0) {
                    byte[] val = new byte[backupDataInput.getDataSize()];
                    backupDataInput.readEntityData(val, 0, val.length);
                    value = ByteBuffer.wrap(val);
                }
                builder.put(backupDataInput.getKey(), Optional.ofNullable(value));
            }
        }
        return builder.build();
    }

    private static ParcelFileDescriptor openForWriting(File file) throws Exception {
        return ParcelFileDescriptor.open(
                file,
                ParcelFileDescriptor.MODE_CREATE
                        | ParcelFileDescriptor.MODE_TRUNCATE
                        | ParcelFileDescriptor.MODE_WRITE_ONLY);
    }

    private static ParcelFileDescriptor openForReading(File file) throws Exception {
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
}