/*
 * 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.fullbackup;

import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;

import android.annotation.Nullable;
import android.app.IBackupAgent;
import android.app.backup.BackupManager;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupProgress;
import android.app.backup.BackupTransport;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
import android.app.backup.IFullBackupRestoreObserver;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;

import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.Preconditions;
import com.android.server.EventLogTags;
import com.android.server.backup.BackupAgentTimeoutParameters;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.FullBackupJob;
import com.android.server.backup.TransportManager;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.internal.Operation;
import com.android.server.backup.remote.RemoteCall;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.transport.TransportNotAvailableException;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;
import com.android.server.backup.utils.BackupObserverUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Full backup task extension used for transport-oriented operation.
 *
 * Flow:
 * For each requested package:
 *     - Spin off a new SinglePackageBackupRunner (mBackupRunner) for the current package.
 *     - Wait until preflight is complete. (mBackupRunner.getPreflightResultBlocking())
 *     - If preflight data size is within limit, start reading data from agent pipe and writing
 *       to transport pipe. While there is data to send, call transport.sendBackupData(int) to
 *       tell the transport how many bytes to expect on its pipe.
 *     - After sending all data, call transport.finishBackup() if things went well. And
 *       transport.cancelFullBackup() otherwise.
 *
 * Interactions with mCurrentOperations:
 *     - An entry for this object is added to mCurrentOperations for the entire lifetime of this
 *       object. Used to cancel the operation.
 *     - SinglePackageBackupRunner and SinglePackageBackupPreflight will put ephemeral entries
 *       to get timeouts or operation complete callbacks.
 *
 * Handling cancels:
 *     - The contract we provide is that the task won't interact with the transport after
 *       handleCancel() is done executing.
 *     - This task blocks at 3 points: 1. Preflight result check 2. Reading on agent side pipe
 *       and 3. Get backup result from mBackupRunner.
 *     - Bubbling up handleCancel to mBackupRunner handles all 3: 1. Calls handleCancel on the
 *       preflight operation which counts down on the preflight latch. 2. Tears down the agent,
 *       so read() returns -1. 3. Notifies mCurrentOpLock which unblocks
 *       mBackupRunner.getBackupResultBlocking().
 */
public class PerformFullTransportBackupTask extends FullBackupTask implements BackupRestoreTask {
    public static PerformFullTransportBackupTask newWithCurrentTransport(
            UserBackupManagerService backupManagerService,
            IFullBackupRestoreObserver observer,
            String[] whichPackages,
            boolean updateSchedule,
            FullBackupJob runningJob,
            CountDownLatch latch,
            IBackupObserver backupObserver,
            IBackupManagerMonitor monitor,
            boolean userInitiated,
            String caller) {
        TransportManager transportManager = backupManagerService.getTransportManager();
        TransportClient transportClient = transportManager.getCurrentTransportClient(caller);
        OnTaskFinishedListener listener =
                listenerCaller ->
                        transportManager.disposeOfTransportClient(transportClient, listenerCaller);
        return new PerformFullTransportBackupTask(
                backupManagerService,
                transportClient,
                observer,
                whichPackages,
                updateSchedule,
                runningJob,
                latch,
                backupObserver,
                monitor,
                listener,
                userInitiated);
    }

    private static final String TAG = "PFTBT";

    private UserBackupManagerService backupManagerService;
    private final Object mCancelLock = new Object();

    ArrayList<PackageInfo> mPackages;
    PackageInfo mCurrentPackage;
    boolean mUpdateSchedule;
    CountDownLatch mLatch;
    FullBackupJob mJob;             // if a scheduled job needs to be finished afterwards
    IBackupObserver mBackupObserver;
    @Nullable private IBackupManagerMonitor mMonitor;
    boolean mUserInitiated;
    SinglePackageBackupRunner mBackupRunner;
    private final int mBackupRunnerOpToken;
    private final OnTaskFinishedListener mListener;
    private final TransportClient mTransportClient;
    private final int mUserId;

    // This is true when a backup operation for some package is in progress.
    private volatile boolean mIsDoingBackup;
    private volatile boolean mCancelAll;
    private final int mCurrentOpToken;
    private final BackupAgentTimeoutParameters mAgentTimeoutParameters;

    public PerformFullTransportBackupTask(UserBackupManagerService backupManagerService,
            TransportClient transportClient,
            IFullBackupRestoreObserver observer,
            String[] whichPackages, boolean updateSchedule,
            FullBackupJob runningJob, CountDownLatch latch, IBackupObserver backupObserver,
            @Nullable IBackupManagerMonitor monitor, @Nullable OnTaskFinishedListener listener,
            boolean userInitiated) {
        super(observer);
        this.backupManagerService = backupManagerService;
        mTransportClient = transportClient;
        mUpdateSchedule = updateSchedule;
        mLatch = latch;
        mJob = runningJob;
        mPackages = new ArrayList<>(whichPackages.length);
        mBackupObserver = backupObserver;
        mMonitor = monitor;
        mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP;
        mUserInitiated = userInitiated;
        mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
        mBackupRunnerOpToken = backupManagerService.generateRandomIntegerToken();
        mAgentTimeoutParameters = Preconditions.checkNotNull(
                backupManagerService.getAgentTimeoutParameters(),
                "Timeout parameters cannot be null");
        mUserId = backupManagerService.getUserId();

        if (backupManagerService.isBackupOperationInProgress()) {
            if (DEBUG) {
                Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
            }
            mCancelAll = true;
            return;
        }

        registerTask();

        for (String pkg : whichPackages) {
            try {
                PackageManager pm = backupManagerService.getPackageManager();
                PackageInfo info = pm.getPackageInfoAsUser(pkg,
                        PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
                mCurrentPackage = info;
                if (!AppBackupUtils.appIsEligibleForBackup(info.applicationInfo, mUserId)) {
                    // Cull any packages that have indicated that backups are not permitted,
                    // that run as system-domain uids but do not define their own backup agents,
                    // as well as any explicit mention of the 'special' shared-storage agent
                    // package (we handle that one at the end).
                    if (MORE_DEBUG) {
                        Slog.d(TAG, "Ignoring ineligible package " + pkg);
                    }
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
                            mCurrentPackage,
                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            null);
                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                    continue;
                } else if (!AppBackupUtils.appGetsFullBackup(info)) {
                    // Cull any packages that are found in the queue but now aren't supposed
                    // to get full-data backup operations.
                    if (MORE_DEBUG) {
                        Slog.d(TAG, "Ignoring full-data backup of key/value participant "
                                + pkg);
                    }
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
                            mCurrentPackage,
                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            null);
                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                    continue;
                } else if (AppBackupUtils.appIsStopped(info.applicationInfo)) {
                    // Cull any packages in the 'stopped' state: they've either just been
                    // installed or have explicitly been force-stopped by the user.  In both
                    // cases we do not want to launch them for backup.
                    if (MORE_DEBUG) {
                        Slog.d(TAG, "Ignoring stopped package " + pkg);
                    }
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                            BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
                            mCurrentPackage,
                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            null);
                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
                            BackupManager.ERROR_BACKUP_NOT_ALLOWED);
                    continue;
                }
                mPackages.add(info);
            } catch (NameNotFoundException e) {
                Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
                        mCurrentPackage,
                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                        null);
            }
        }
    }

    private void registerTask() {
        synchronized (backupManagerService.getCurrentOpLock()) {
            Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
            backupManagerService.getCurrentOperations().put(
                    mCurrentOpToken,
                    new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
        }
    }

    public void unregisterTask() {
        backupManagerService.removeOperation(mCurrentOpToken);
    }

    @Override
    public void execute() {
        // Nothing to do.
    }

    @Override
    public void handleCancel(boolean cancelAll) {
        synchronized (mCancelLock) {
            // We only support 'cancelAll = true' case for this task. Cancelling of a single package

            // due to timeout is handled by SinglePackageBackupRunner and
            // SinglePackageBackupPreflight.

            if (!cancelAll) {
                Slog.wtf(TAG, "Expected cancelAll to be true.");
            }

            if (mCancelAll) {
                Slog.d(TAG, "Ignoring duplicate cancel call.");
                return;
            }

            mCancelAll = true;
            if (mIsDoingBackup) {
                backupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll);
                try {
                    // If we're running a backup we should be connected to a transport
                    IBackupTransport transport =
                            mTransportClient.getConnectedTransport("PFTBT.handleCancel()");
                    transport.cancelFullBackup();
                } catch (RemoteException | TransportNotAvailableException e) {
                    Slog.w(TAG, "Error calling cancelFullBackup() on transport: " + e);
                    // Can't do much.
                }
            }
        }
    }

    @Override
    public void operationComplete(long result) {
        // Nothing to do.
    }

    @Override
    public void run() {

        // data from the app, passed to us for bridging to the transport
        ParcelFileDescriptor[] enginePipes = null;

        // Pipe through which we write data to the transport
        ParcelFileDescriptor[] transportPipes = null;

        long backoff = 0;
        int backupRunStatus = BackupManager.SUCCESS;

        try {
            if (!backupManagerService.isEnabled() || !backupManagerService.isSetupComplete()) {
                // Backups are globally disabled, so don't proceed.
                if (DEBUG) {
                    Slog.i(TAG, "full backup requested but enabled=" + backupManagerService
                            .isEnabled()
                            + " setupComplete=" + backupManagerService.isSetupComplete()
                            + "; ignoring");
                }
                int monitoringEvent;
                if (backupManagerService.isSetupComplete()) {
                    monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED;
                } else {
                    monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
                }
                mMonitor = BackupManagerMonitorUtils
                        .monitorEvent(mMonitor, monitoringEvent, null,
                                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                null);
                mUpdateSchedule = false;
                backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
                return;
            }

            IBackupTransport transport = mTransportClient.connect("PFTBT.run()");
            if (transport == null) {
                Slog.w(TAG, "Transport not present; full data backup not performed");
                backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
                        mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
                        null);
                return;
            }

            // Set up to send data to the transport
            final int N = mPackages.size();
            final byte[] buffer = new byte[8192];
            for (int i = 0; i < N; i++) {
                mBackupRunner = null;
                PackageInfo currentPackage = mPackages.get(i);
                String packageName = currentPackage.packageName;
                if (DEBUG) {
                    Slog.i(TAG, "Initiating full-data transport backup of " + packageName
                            + " token: " + mCurrentOpToken);
                }
                EventLog.writeEvent(EventLogTags.FULL_BACKUP_PACKAGE, packageName);

                transportPipes = ParcelFileDescriptor.createPipe();

                // Tell the transport the data's coming
                int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
                int backupPackageStatus;
                long quota = Long.MAX_VALUE;
                synchronized (mCancelLock) {
                    if (mCancelAll) {
                        break;
                    }
                    backupPackageStatus = transport.performFullBackup(currentPackage,
                            transportPipes[0], flags);

                    if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
                        quota = transport.getBackupQuota(currentPackage.packageName,
                                true /* isFullBackup */);
                        // Now set up the backup engine / data source end of things
                        enginePipes = ParcelFileDescriptor.createPipe();
                        mBackupRunner =
                                new SinglePackageBackupRunner(enginePipes[1], currentPackage,
                                        mTransportClient, quota, mBackupRunnerOpToken,
                                        transport.getTransportFlags());
                        // The runner dup'd the pipe half, so we close it here
                        enginePipes[1].close();
                        enginePipes[1] = null;

                        mIsDoingBackup = true;
                    }
                }
                if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {

                    // The transport has its own copy of the read end of the pipe,
                    // so close ours now
                    transportPipes[0].close();
                    transportPipes[0] = null;

                    // Spin off the runner to fetch the app's data and pipe it
                    // into the engine pipes
                    (new Thread(mBackupRunner, "package-backup-bridge")).start();

                    // Read data off the engine pipe and pass it to the transport
                    // pipe until we hit EOD on the input stream.  We do not take
                    // close() responsibility for these FDs into these stream wrappers.
                    FileInputStream in = new FileInputStream(
                            enginePipes[0].getFileDescriptor());
                    FileOutputStream out = new FileOutputStream(
                            transportPipes[1].getFileDescriptor());
                    long totalRead = 0;
                    final long preflightResult = mBackupRunner.getPreflightResultBlocking();
                    // Preflight result is negative if some error happened on preflight.
                    if (preflightResult < 0) {
                        if (MORE_DEBUG) {
                            Slog.d(TAG, "Backup error after preflight of package "
                                    + packageName + ": " + preflightResult
                                    + ", not running backup.");
                        }
                        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                                BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
                                mCurrentPackage,
                                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                BackupManagerMonitorUtils.putMonitoringExtra(null,
                                        BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
                                        preflightResult));
                        backupPackageStatus = (int) preflightResult;
                    } else {
                        int nRead = 0;
                        do {
                            nRead = in.read(buffer);
                            if (MORE_DEBUG) {
                                Slog.v(TAG, "in.read(buffer) from app: " + nRead);
                            }
                            if (nRead > 0) {
                                out.write(buffer, 0, nRead);
                                synchronized (mCancelLock) {
                                    if (!mCancelAll) {
                                        backupPackageStatus = transport.sendBackupData(nRead);
                                    }
                                }
                                totalRead += nRead;
                                if (mBackupObserver != null && preflightResult > 0) {
                                    BackupObserverUtils
                                            .sendBackupOnUpdate(mBackupObserver, packageName,
                                                    new BackupProgress(preflightResult, totalRead));
                                }
                            }
                        } while (nRead > 0
                                && backupPackageStatus == BackupTransport.TRANSPORT_OK);
                        // Despite preflight succeeded, package still can hit quota on flight.
                        if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
                            Slog.w(TAG, "Package hit quota limit in-flight " + packageName
                                    + ": " + totalRead + " of " + quota);
                            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                                    BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
                                    mCurrentPackage,
                                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
                                    null);
                            mBackupRunner.sendQuotaExceeded(totalRead, quota);
                        }
                    }

                    final int backupRunnerResult = mBackupRunner.getBackupResultBlocking();

                    synchronized (mCancelLock) {
                        mIsDoingBackup = false;
                        // If mCancelCurrent is true, we have already called cancelFullBackup().
                        if (!mCancelAll) {
                            if (backupRunnerResult == BackupTransport.TRANSPORT_OK) {
                                // If we were otherwise in a good state, now interpret the final
                                // result based on what finishBackup() returns.  If we're in a
                                // failure case already, preserve that result and ignore whatever
                                // finishBackup() reports.
                                final int finishResult = transport.finishBackup();
                                if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
                                    backupPackageStatus = finishResult;
                                }
                            } else {
                                transport.cancelFullBackup();
                            }
                        }
                    }

                    // A transport-originated error here means that we've hit an error that the
                    // runner doesn't know about, so it's still moving data but we're pulling the
                    // rug out from under it.  Don't ask for its result:  we already know better
                    // and we'll hang if we block waiting for it, since it relies on us to
                    // read back the data it's writing into the engine.  Just proceed with
                    // a graceful failure.  The runner/engine mechanism will tear itself
                    // down cleanly when we close the pipes from this end.  Transport-level
                    // errors take precedence over agent/app-specific errors for purposes of
                    // determining our course of action.
                    if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
                        // We still could fail in backup runner thread.
                        if (backupRunnerResult != BackupTransport.TRANSPORT_OK) {
                            // If there was an error in runner thread and
                            // not TRANSPORT_ERROR here, overwrite it.
                            backupPackageStatus = backupRunnerResult;
                        }
                    } else {
                        if (MORE_DEBUG) {
                            Slog.i(TAG, "Transport-level failure; cancelling agent work");
                        }
                    }

                    if (MORE_DEBUG) {
                        Slog.i(TAG, "Done delivering backup data: result="
                                + backupPackageStatus);
                    }

                    if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
                        Slog.e(TAG, "Error " + backupPackageStatus + " backing up "
                                + packageName);
                    }

                    // Also ask the transport how long it wants us to wait before
                    // moving on to the next package, if any.
                    backoff = transport.requestFullBackupTime();
                    if (DEBUG_SCHEDULING) {
                        Slog.i(TAG, "Transport suggested backoff=" + backoff);
                    }

                }

                // Roll this package to the end of the backup queue if we're
                // in a queue-driven mode (regardless of success/failure)
                if (mUpdateSchedule) {
                    backupManagerService.enqueueFullBackup(packageName, System.currentTimeMillis());
                }

                if (backupPackageStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
                    if (DEBUG) {
                        Slog.i(TAG, "Transport rejected backup of " + packageName
                                + ", skipping");
                    }
                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_AGENT_FAILURE, packageName,
                            "transport rejected");
                    // This failure state can come either a-priori from the transport, or
                    // from the preflight pass.  If we got as far as preflight, we now need
                    // to tear down the target process.
                    if (mBackupRunner != null) {
                        backupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
                    }
                    // ... and continue looping.
                } else if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
                    if (DEBUG) {
                        Slog.i(TAG, "Transport quota exceeded for package: " + packageName);
                        EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED,
                                packageName);
                    }
                    backupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
                    // Do nothing, clean up, and continue looping.
                } else if (backupPackageStatus == BackupTransport.AGENT_ERROR) {
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.ERROR_AGENT_FAILURE);
                    Slog.w(TAG, "Application failure for package: " + packageName);
                    EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
                    backupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
                    // Do nothing, clean up, and continue looping.
                } else if (backupPackageStatus == BackupManager.ERROR_BACKUP_CANCELLED) {
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.ERROR_BACKUP_CANCELLED);
                    Slog.w(TAG, "Backup cancelled. package=" + packageName +
                            ", cancelAll=" + mCancelAll);
                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName);
                    backupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
                    // Do nothing, clean up, and continue looping.
                } else if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.ERROR_TRANSPORT_ABORTED);
                    Slog.w(TAG, "Transport failed; aborting backup: " + backupPackageStatus);
                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_TRANSPORT_FAILURE);
                    // Abort entire backup pass.
                    backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
                    backupManagerService.tearDownAgentAndKill(currentPackage.applicationInfo);
                    return;
                } else {
                    // Success!
                    BackupObserverUtils
                            .sendBackupOnPackageResult(mBackupObserver, packageName,
                                    BackupManager.SUCCESS);
                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_SUCCESS, packageName);
                    backupManagerService.logBackupComplete(packageName);
                }
                cleanUpPipes(transportPipes);
                cleanUpPipes(enginePipes);
                if (currentPackage.applicationInfo != null) {
                    Slog.i(TAG, "Unbinding agent in " + packageName);
                    try {
                        backupManagerService.getActivityManager().unbindBackupAgent(
                                currentPackage.applicationInfo);
                    } catch (RemoteException e) { /* can't happen; activity manager is local */ }
                }
            }
        } catch (Exception e) {
            backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
            Slog.w(TAG, "Exception trying full transport backup", e);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
                    mCurrentPackage,
                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                    BackupManagerMonitorUtils.putMonitoringExtra(null,
                            BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
                            Log.getStackTraceString(e)));

        } finally {

            if (mCancelAll) {
                backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
            }

            if (DEBUG) {
                Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
            }
            BackupObserverUtils.sendBackupFinished(mBackupObserver, backupRunStatus);

            cleanUpPipes(transportPipes);
            cleanUpPipes(enginePipes);

            unregisterTask();

            if (mJob != null) {
                mJob.finishBackupPass(mUserId);
            }

            synchronized (backupManagerService.getQueueLock()) {
                backupManagerService.setRunningFullBackupTask(null);
            }

            mListener.onFinished("PFTBT.run()");

            mLatch.countDown();

            // Now that we're actually done with schedule-driven work, reschedule
            // the next pass based on the new queue state.
            if (mUpdateSchedule) {
                backupManagerService.scheduleNextFullBackupJob(backoff);
            }

            Slog.i(TAG, "Full data backup pass finished.");
            backupManagerService.getWakelock().release();
        }
    }

    void cleanUpPipes(ParcelFileDescriptor[] pipes) {
        if (pipes != null) {
            if (pipes[0] != null) {
                ParcelFileDescriptor fd = pipes[0];
                pipes[0] = null;
                try {
                    fd.close();
                } catch (IOException e) {
                    Slog.w(TAG, "Unable to close pipe!");
                }
            }
            if (pipes[1] != null) {
                ParcelFileDescriptor fd = pipes[1];
                pipes[1] = null;
                try {
                    fd.close();
                } catch (IOException e) {
                    Slog.w(TAG, "Unable to close pipe!");
                }
            }
        }
    }

    // Run the backup and pipe it back to the given socket -- expects to run on
    // a standalone thread.  The  runner owns this half of the pipe, and closes
    // it to indicate EOD to the other end.
    class SinglePackageBackupPreflight implements BackupRestoreTask, FullBackupPreflight {
        final AtomicLong mResult = new AtomicLong(BackupTransport.AGENT_ERROR);
        final CountDownLatch mLatch = new CountDownLatch(1);
        final TransportClient mTransportClient;
        final long mQuota;
        private final int mCurrentOpToken;
        private final int mTransportFlags;

        SinglePackageBackupPreflight(
                TransportClient transportClient,
                long quota,
                int currentOpToken,
                int transportFlags) {
            mTransportClient = transportClient;
            mQuota = quota;
            mCurrentOpToken = currentOpToken;
            mTransportFlags = transportFlags;
        }

        @Override
        public int preflightFullBackup(PackageInfo pkg, IBackupAgent agent) {
            int result;
            long fullBackupAgentTimeoutMillis =
                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
            try {
                backupManagerService.prepareOperationTimeout(
                        mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OP_TYPE_BACKUP_WAIT);
                if (MORE_DEBUG) {
                    Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
                }
                agent.doMeasureFullBackup(mQuota, mCurrentOpToken,
                        backupManagerService.getBackupManagerBinder(), mTransportFlags);

                // Now wait to get our result back.  If this backstop timeout is reached without
                // the latch being thrown, flow will continue as though a result or "normal"
                // timeout had been produced.  In case of a real backstop timeout, mResult
                // will still contain the value it was constructed with, AGENT_ERROR, which
                // intentionaly falls into the "just report failure" code.
                mLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);

                long totalSize = mResult.get();
                // If preflight timed out, mResult will contain error code as int.
                if (totalSize < 0) {
                    return (int) totalSize;
                }
                if (MORE_DEBUG) {
                    Slog.v(TAG, "Got preflight response; size=" + totalSize);
                }

                IBackupTransport transport =
                        mTransportClient.connectOrThrow("PFTBT$SPBP.preflightFullBackup()");
                result = transport.checkFullBackupSize(totalSize);
                if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
                    if (MORE_DEBUG) {
                        Slog.d(TAG, "Package hit quota limit on preflight " +
                                pkg.packageName + ": " + totalSize + " of " + mQuota);
                    }
                    RemoteCall.execute(
                            callback -> agent.doQuotaExceeded(totalSize, mQuota, callback),
                            mAgentTimeoutParameters.getQuotaExceededTimeoutMillis());
                }
            } catch (Exception e) {
                Slog.w(TAG, "Exception preflighting " + pkg.packageName + ": " + e.getMessage());
                result = BackupTransport.AGENT_ERROR;
            }
            return result;
        }

        @Override
        public void execute() {
            // Unused.
        }

        @Override
        public void operationComplete(long result) {
            // got the callback, and our preflightFullBackup() method is waiting for the result
            if (MORE_DEBUG) {
                Slog.i(TAG, "Preflight op complete, result=" + result);
            }
            mResult.set(result);
            mLatch.countDown();
            backupManagerService.removeOperation(mCurrentOpToken);
        }

        @Override
        public void handleCancel(boolean cancelAll) {
            if (MORE_DEBUG) {
                Slog.i(TAG, "Preflight cancelled; failing");
            }
            mResult.set(BackupTransport.AGENT_ERROR);
            mLatch.countDown();
            backupManagerService.removeOperation(mCurrentOpToken);
        }

        @Override
        public long getExpectedSizeOrErrorCode() {
            long fullBackupAgentTimeoutMillis =
                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
            try {
                mLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
                return mResult.get();
            } catch (InterruptedException e) {
                return BackupTransport.NO_MORE_DATA;
            }
        }
    }

    class SinglePackageBackupRunner implements Runnable, BackupRestoreTask {
        final ParcelFileDescriptor mOutput;
        final PackageInfo mTarget;
        final SinglePackageBackupPreflight mPreflight;
        final CountDownLatch mPreflightLatch;
        final CountDownLatch mBackupLatch;
        private final int mCurrentOpToken;
        private final int mEphemeralToken;
        private FullBackupEngine mEngine;
        private volatile int mPreflightResult;
        private volatile int mBackupResult;
        private final long mQuota;
        private volatile boolean mIsCancelled;
        private final int mTransportFlags;

        SinglePackageBackupRunner(ParcelFileDescriptor output, PackageInfo target,
                TransportClient transportClient, long quota, int currentOpToken, int transportFlags)
                throws IOException {
            mOutput = ParcelFileDescriptor.dup(output.getFileDescriptor());
            mTarget = target;
            mCurrentOpToken = currentOpToken;
            mEphemeralToken = backupManagerService.generateRandomIntegerToken();
            mPreflight = new SinglePackageBackupPreflight(
                    transportClient, quota, mEphemeralToken, transportFlags);
            mPreflightLatch = new CountDownLatch(1);
            mBackupLatch = new CountDownLatch(1);
            mPreflightResult = BackupTransport.AGENT_ERROR;
            mBackupResult = BackupTransport.AGENT_ERROR;
            mQuota = quota;
            mTransportFlags = transportFlags;
            registerTask();
        }

        void registerTask() {
            synchronized (backupManagerService.getCurrentOpLock()) {
                backupManagerService.getCurrentOperations().put(
                        mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP_WAIT));
            }
        }

        void unregisterTask() {
            synchronized (backupManagerService.getCurrentOpLock()) {
                backupManagerService.getCurrentOperations().remove(mCurrentOpToken);
            }
        }

        @Override
        public void run() {
            FileOutputStream out = new FileOutputStream(mOutput.getFileDescriptor());
            mEngine = new FullBackupEngine(backupManagerService, out, mPreflight, mTarget, false,
                    this, mQuota, mCurrentOpToken, mTransportFlags);
            try {
                try {
                    if (!mIsCancelled) {
                        mPreflightResult = mEngine.preflightCheck();
                    }
                } finally {
                    mPreflightLatch.countDown();
                }
                // If there is no error on preflight, continue backup.
                if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
                    if (!mIsCancelled) {
                        mBackupResult = mEngine.backupOnePackage();
                    }
                }
            } catch (Exception e) {
                Slog.e(TAG, "Exception during full package backup of " + mTarget.packageName);
            } finally {
                unregisterTask();
                mBackupLatch.countDown();
                try {
                    mOutput.close();
                } catch (IOException e) {
                    Slog.w(TAG, "Error closing transport pipe in runner");
                }
            }
        }

        public void sendQuotaExceeded(final long backupDataBytes, final long quotaBytes) {
            mEngine.sendQuotaExceeded(backupDataBytes, quotaBytes);
        }

        // If preflight succeeded, returns positive number - preflight size,
        // otherwise return negative error code.
        long getPreflightResultBlocking() {
            long fullBackupAgentTimeoutMillis =
                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
            try {
                mPreflightLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
                if (mIsCancelled) {
                    return BackupManager.ERROR_BACKUP_CANCELLED;
                }
                if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
                    return mPreflight.getExpectedSizeOrErrorCode();
                } else {
                    return mPreflightResult;
                }
            } catch (InterruptedException e) {
                return BackupTransport.AGENT_ERROR;
            }
        }

        int getBackupResultBlocking() {
            long fullBackupAgentTimeoutMillis =
                    mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
            try {
                mBackupLatch.await(fullBackupAgentTimeoutMillis, TimeUnit.MILLISECONDS);
                if (mIsCancelled) {
                    return BackupManager.ERROR_BACKUP_CANCELLED;
                }
                return mBackupResult;
            } catch (InterruptedException e) {
                return BackupTransport.AGENT_ERROR;
            }
        }


        // BackupRestoreTask interface: specifically, timeout detection

        @Override
        public void execute() { /* intentionally empty */ }

        @Override
        public void operationComplete(long result) { /* intentionally empty */ }

        @Override
        public void handleCancel(boolean cancelAll) {
            if (DEBUG) {
                Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
            }

            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
            mIsCancelled = true;
            // Cancel tasks spun off by this task.
            backupManagerService.handleCancel(mEphemeralToken, cancelAll);
            backupManagerService.tearDownAgentAndKill(mTarget.applicationInfo);
            // Free up everyone waiting on this task and its children.
            mPreflightLatch.countDown();
            mBackupLatch.countDown();
            // We are done with this operation.
            backupManagerService.removeOperation(mCurrentOpToken);
        }
    }
}
