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

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

Add `onMeasureFullBackup` method to `BackupAgent`

When backing up, a preflight check is performed that checks the size of
the backup against the backup quota. By default, the backup agent
measures the size of a backup by performing a full backup and discarding
the backed up data. This means for a normal backup session, the
`BackupAgent#onFullBackup` is called twice. For most backup agents this
is fine, as they won't have much data to back up.

We want to give apps that back up a lot of data (for example, during D2D
where the backup quota is multiple gigabytes) the possibility to provide
their own implementation of measuring the backup size so they can
provide a more efficient implementation if needed.

Bug: 403956528
Test: atest BackupAgentTest.java
Test: atest MeasureFullBackupTest.java (cts test added in same topic)
Flag: com.android.server.backup.enable_cross_platform_transfer
Change-Id: If6a869e772d433acc770f47f4aa05f54c54d3c0e
parent f2011b57
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -9283,6 +9283,7 @@ package android.app.backup {
    method public void onCreate();
    method public void onDestroy();
    method public void onFullBackup(android.app.backup.FullBackupDataOutput) throws java.io.IOException;
    method @FlaggedApi("com.android.server.backup.enable_cross_platform_transfer") public long onMeasureFullBackup(long, int) throws java.io.IOException;
    method public void onQuotaExceeded(long, long);
    method public abstract void onRestore(android.app.backup.BackupDataInput, int, android.os.ParcelFileDescriptor) throws java.io.IOException;
    method public void onRestore(android.app.backup.BackupDataInput, long, android.os.ParcelFileDescriptor) throws java.io.IOException;
+39 −6
Original line number Diff line number Diff line
@@ -159,8 +159,8 @@ public abstract class BackupAgent extends ContextWrapper {
    public static final int TYPE_SYMLINK = 3;

    /**
     * Flag for {@link BackupDataOutput#getTransportFlags()} and {@link
     * FullBackupDataOutput#getTransportFlags()} only.
     * Flag for {@link BackupDataOutput#getTransportFlags()}, {@link
     * FullBackupDataOutput#getTransportFlags()} and {@link #onMeasureFullBackup(long, int)} only.
     *
     * <p>The transport has client-side encryption enabled. i.e., the user's backup has been
     * encrypted with a key known only to the device, and not to the remote storage solution. Even
@@ -170,8 +170,8 @@ public abstract class BackupAgent extends ContextWrapper {
    public static final int FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED = 1;

    /**
     * Flag for {@link BackupDataOutput#getTransportFlags()} and {@link
     * FullBackupDataOutput#getTransportFlags()} only.
     * Flag for {@link BackupDataOutput#getTransportFlags()}, {@link
     * FullBackupDataOutput#getTransportFlags()} and {@link #onMeasureFullBackup(long, int)} only.
     *
     * <p>The transport is for a device-to-device transfer. There is no third party or intermediate
     * storage. The user's backup data is sent directly to another device over e.g., USB or WiFi.
@@ -624,6 +624,26 @@ public abstract class BackupAgent extends ContextWrapper {
        return new IncludeExcludeRules(manifestIncludeMap, manifestExcludeSet);
    }

    /**
     * Estimate how much data in bytes a full backup will deliver. This is used during the preflight
     * check to make sure the size doesn't exceed the backup quota.
     *
     * <p>By default, the backup size is measured by calling {@link
     * #onFullBackup(FullBackupDataOutput)} and looking at the size of the backup it produces while
     * discarding the data. This method can be overridden to provide an alternative, more efficient
     * estimation if necessary.
     *
     * @param quotaBytes The maximum data size that the transport currently permits this application
     *     to store as a backup.
     * @param transportFlags flags with additional information about the backup transport.
     * @return estimated size of the full backup. If the returned size is negative, the backup agent
     *     will fallback to using {@link #onFullBackup(FullBackupDataOutput)} to measure the size.
     */
    @FlaggedApi(Flags.FLAG_ENABLE_CROSS_PLATFORM_TRANSFER)
    public long onMeasureFullBackup(long quotaBytes, int transportFlags) throws IOException {
        return -1;
    }

    /**
     * Notification that the application's current backup operation causes it to exceed the maximum
     * size permitted by the transport. The ongoing backup operation is halted and rolled back: any
@@ -1336,6 +1356,7 @@ public abstract class BackupAgent extends ContextWrapper {

        public void doMeasureFullBackup(
                long quotaBytes, int token, IBackupManager callbackBinder, int transportFlags) {
            long estimatedBackupSize = -1;
            FullBackupDataOutput measureOutput =
                    new FullBackupDataOutput(quotaBytes, transportFlags);

@@ -1344,7 +1365,15 @@ public abstract class BackupAgent extends ContextWrapper {
            // Ensure that we're running with the app's normal permission level
            final long ident = Binder.clearCallingIdentity();
            try {
                if (Flags.enableCrossPlatformTransfer()) {
                    estimatedBackupSize =
                            BackupAgent.this.onMeasureFullBackup(quotaBytes, transportFlags);
                    if (estimatedBackupSize < 0) {
                        BackupAgent.this.onFullBackup(measureOutput);
                    }
                } else {
                    BackupAgent.this.onFullBackup(measureOutput);
                }
            } catch (IOException ex) {
                Log.d(
                        TAG,
@@ -1361,7 +1390,11 @@ public abstract class BackupAgent extends ContextWrapper {
                Binder.restoreCallingIdentity(ident);
                try {
                    callbackBinder.opCompleteForUser(
                            getBackupUserId(), token, measureOutput.getSize());
                            getBackupUserId(),
                            token,
                            estimatedBackupSize >= 0
                                    ? estimatedBackupSize
                                    : measureOutput.getSize());
                } catch (RemoteException e) {
                    // timeout, so we're safe
                }
+85 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.app.backup;

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

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.IBackupAgent;
@@ -28,6 +29,8 @@ import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
@@ -179,6 +182,53 @@ public class BackupAgentTest {
        }
    }

    @Test
    @DisableFlags({Flags.FLAG_ENABLE_CROSS_PLATFORM_TRANSFER})
    public void doMeasureFullBackup_flagOff_callOnFullBackup() throws Exception {
        TestMeasureSizeBackupAgent agent = new TestMeasureSizeBackupAgent(1024);
        agent.attach(mContext);
        agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.BACKUP);
        IBackupAgent agentBinder = (IBackupAgent) agent.onBind();

        agentBinder.doMeasureFullBackup(
                /* quotaBytes= */ 2048, /* token= */ 0, mIBackupManager, /* transportFlags= */ 0);

        assertThat(agent.mOnMeasureFullBackupCalled).isFalse();
        assertThat(agent.mOnFullBackupCalled).isTrue();
        verify(mIBackupManager).opCompleteForUser(USER_HANDLE.getIdentifier(), 0, 0);
    }

    @Test
    @EnableFlags({Flags.FLAG_ENABLE_CROSS_PLATFORM_TRANSFER})
    public void doMeasureFullBackup_flagOn_callsOnMeasureFullBackup() throws Exception {
        TestMeasureSizeBackupAgent agent = new TestMeasureSizeBackupAgent(1024);
        agent.attach(mContext);
        agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.BACKUP);
        IBackupAgent agentBinder = (IBackupAgent) agent.onBind();

        agentBinder.doMeasureFullBackup(
                /* quotaBytes= */ 2048, /* token= */ 0, mIBackupManager, /* transportFlags= */ 0);

        assertThat(agent.mOnMeasureFullBackupCalled).isTrue();
        assertThat(agent.mOnFullBackupCalled).isFalse();
        verify(mIBackupManager).opCompleteForUser(USER_HANDLE.getIdentifier(), 0, 1024);
    }

    @Test
    public void doMeasureFullBackup_flagOn_negativeSize_callsOnFullBackup() throws Exception {
        TestMeasureSizeBackupAgent agent = new TestMeasureSizeBackupAgent(-1);
        agent.attach(mContext);
        agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.BACKUP);
        IBackupAgent agentBinder = (IBackupAgent) agent.onBind();

        agentBinder.doMeasureFullBackup(
                /* quotaBytes= */ 2048, /* token= */ 0, mIBackupManager, /* transportFlags= */ 0);

        assertThat(agent.mOnMeasureFullBackupCalled).isTrue();
        assertThat(agent.mOnFullBackupCalled).isTrue();
        verify(mIBackupManager).opCompleteForUser(USER_HANDLE.getIdentifier(), 0, 0);
    }

    private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) {
        BackupAgent agent = new TestFullBackupAgent();
        agent.onCreate(USER_HANDLE, backupDestination);
@@ -221,4 +271,39 @@ public class BackupAgentTest {
            // Ignore the file and don't consume any data.
        }
    }

    private static class TestMeasureSizeBackupAgent extends BackupAgent {
        private final long mSize;
        private boolean mOnFullBackupCalled;
        private boolean mOnMeasureFullBackupCalled;

        TestMeasureSizeBackupAgent(long size) {
            this.mSize = size;
        }

        @Override
        public void onBackup(
                ParcelFileDescriptor oldState,
                BackupDataOutput data,
                ParcelFileDescriptor newState) {
            // Left empty as this is a full backup agent.
        }

        @Override
        public void onRestore(
                BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
            // Left empty as this is a full backup agent.
        }

        @Override
        public void onFullBackup(FullBackupDataOutput data) {
            mOnFullBackupCalled = true;
        }

        @Override
        public long onMeasureFullBackup(long quotaBytes, int transportFlags) {
            mOnMeasureFullBackupCalled = true;
            return mSize;
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -76,6 +76,11 @@ public class ForwardingBackupAgent extends BackupAgent {
        mBackupAgent.onFullBackup(data);
    }

    @Override
    public long onMeasureFullBackup(long quotaBytes, int transportFlags) throws IOException {
        return mBackupAgent.onMeasureFullBackup(quotaBytes, transportFlags);
    }

    @Override
    public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
        mBackupAgent.onQuotaExceeded(backupDataBytes, quotaBytes);