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

import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.KEY_WIDGET_STATE;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.OP_TYPE_RESTORE_WAIT;
import static com.android.server.backup.BackupManagerService.PACKAGE_MANAGER_SENTINEL;
import static com.android.server.backup.BackupManagerService.SETTINGS_PACKAGE;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.BackupManagerService
        .TIMEOUT_RESTORE_FINISHED_INTERVAL;
import static com.android.server.backup.BackupManagerService.TIMEOUT_RESTORE_INTERVAL;
import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_RESTORE_STEP;
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;

import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupTransport;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IRestoreObserver;
import android.app.backup.RestoreDescription;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.EventLog;
import android.util.Slog;

import com.android.internal.backup.IBackupTransport;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.EventLogTags;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.BackupUtils;
import com.android.server.backup.PackageManagerBackupAgent;
import com.android.server.backup.PackageManagerBackupAgent.Metadata;
import com.android.server.backup.BackupManagerService;
import com.android.server.backup.TransportManager;
import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;

import libcore.io.IoUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class PerformUnifiedRestoreTask implements BackupRestoreTask {

    private BackupManagerService backupManagerService;
    private final TransportManager mTransportManager;
    // Transport client we're working with to do the restore
    private final TransportClient mTransportClient;

    // Where per-transport saved state goes
    File mStateDir;

    // Restore observer; may be null
    private IRestoreObserver mObserver;

    // BackuoManagerMonitor; may be null
    private IBackupManagerMonitor mMonitor;

    // Token identifying the dataset to the transport
    private long mToken;

    // When this is a restore-during-install, this is the token identifying the
    // operation to the Package Manager, and we must ensure that we let it know
    // when we're finished.
    private int mPmToken;

    // When this is restore-during-install, we need to tell the package manager
    // whether we actually launched the app, because this affects notifications
    // around externally-visible state transitions.
    private boolean mDidLaunch;

    // Is this a whole-system restore, i.e. are we establishing a new ancestral
    // dataset to base future restore-at-install operations from?
    private boolean mIsSystemRestore;

    // If this is a single-package restore, what package are we interested in?
    private PackageInfo mTargetPackage;

    // In all cases, the calculated list of packages that we are trying to restore
    private List<PackageInfo> mAcceptSet;

    // Our bookkeeping about the ancestral dataset
    private PackageManagerBackupAgent mPmAgent;

    // Currently-bound backup agent for restore + restoreFinished purposes
    private IBackupAgent mAgent;

    // What sort of restore we're doing now
    private RestoreDescription mRestoreDescription;

    // The package we're currently restoring
    private PackageInfo mCurrentPackage;

    // Widget-related data handled as part of this restore operation
    private byte[] mWidgetData;

    // Number of apps restored in this pass
    private int mCount;

    // When did we start?
    private long mStartRealtime;

    // State machine progress
    private UnifiedRestoreState mState;

    // How are things going?
    private int mStatus;

    // Done?
    private boolean mFinished;

    // When finished call listener
    private final OnTaskFinishedListener mListener;

    // Key/value: bookkeeping about staged data and files for agent access
    private File mBackupDataName;
    private File mStageName;
    private File mSavedStateName;
    private File mNewStateName;
    ParcelFileDescriptor mBackupData;
    ParcelFileDescriptor mNewState;

    private final int mEphemeralOpToken;

    // Invariant: mWakelock is already held, and this task is responsible for
    // releasing it at the end of the restore operation.
    public PerformUnifiedRestoreTask(BackupManagerService backupManagerService,
            TransportClient transportClient, IRestoreObserver observer,
            IBackupManagerMonitor monitor, long restoreSetToken, PackageInfo targetPackage,
            int pmToken, boolean isFullSystemRestore, String[] filterSet,
            OnTaskFinishedListener listener) {
        this.backupManagerService = backupManagerService;
        mTransportManager = backupManagerService.getTransportManager();
        mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
        mState = UnifiedRestoreState.INITIAL;
        mStartRealtime = SystemClock.elapsedRealtime();

        mTransportClient = transportClient;
        mObserver = observer;
        mMonitor = monitor;
        mToken = restoreSetToken;
        mPmToken = pmToken;
        mTargetPackage = targetPackage;
        mIsSystemRestore = isFullSystemRestore;
        mFinished = false;
        mDidLaunch = false;
        mListener = listener;

        if (targetPackage != null) {
            // Single package restore
            mAcceptSet = new ArrayList<>();
            mAcceptSet.add(targetPackage);
        } else {
            // Everything possible, or a target set
            if (filterSet == null) {
                // We want everything and a pony
                List<PackageInfo> apps =
                        PackageManagerBackupAgent.getStorableApplications(
                                backupManagerService.getPackageManager());
                filterSet = packagesToNames(apps);
                if (DEBUG) {
                    Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps");
                }
            }

            mAcceptSet = new ArrayList<>(filterSet.length);

            // Pro tem, we insist on moving the settings provider package to last place.
            // Keep track of whether it's in the list, and bump it down if so.  We also
            // want to do the system package itself first if it's called for.
            boolean hasSystem = false;
            boolean hasSettings = false;
            for (int i = 0; i < filterSet.length; i++) {
                try {
                    PackageManager pm = backupManagerService.getPackageManager();
                    PackageInfo info = pm.getPackageInfo(filterSet[i], 0);
                    if ("android".equals(info.packageName)) {
                        hasSystem = true;
                        continue;
                    }
                    if (SETTINGS_PACKAGE.equals(info.packageName)) {
                        hasSettings = true;
                        continue;
                    }

                    if (AppBackupUtils.appIsEligibleForBackup(info.applicationInfo, pm)) {
                        mAcceptSet.add(info);
                    }
                } catch (NameNotFoundException e) {
                    // requested package name doesn't exist; ignore it
                }
            }
            if (hasSystem) {
                try {
                    mAcceptSet.add(0,
                            backupManagerService.getPackageManager().getPackageInfo("android", 0));
                } catch (NameNotFoundException e) {
                    // won't happen; we know a priori that it's valid
                }
            }
            if (hasSettings) {
                try {
                    mAcceptSet.add(backupManagerService.getPackageManager().getPackageInfo(
                            SETTINGS_PACKAGE, 0));
                } catch (NameNotFoundException e) {
                    // this one is always valid too
                }
            }
        }

        if (MORE_DEBUG) {
            Slog.v(TAG, "Restore; accept set size is " + mAcceptSet.size());
            for (PackageInfo info : mAcceptSet) {
                Slog.v(TAG, "   " + info.packageName);
            }
        }
    }

    private String[] packagesToNames(List<PackageInfo> apps) {
        final int N = apps.size();
        String[] names = new String[N];
        for (int i = 0; i < N; i++) {
            names[i] = apps.get(i).packageName;
        }
        return names;
    }

    // Execute one tick of whatever state machine the task implements
    @Override
    public void execute() {
        if (MORE_DEBUG) {
            Slog.v(TAG, "*** Executing restore step " + mState);
        }
        switch (mState) {
            case INITIAL:
                startRestore();
                break;

            case RUNNING_QUEUE:
                dispatchNextRestore();
                break;

            case RESTORE_KEYVALUE:
                restoreKeyValue();
                break;

            case RESTORE_FULL:
                restoreFull();
                break;

            case RESTORE_FINISHED:
                restoreFinished();
                break;

            case FINAL:
                if (!mFinished) {
                    finalizeRestore();
                } else {
                    Slog.e(TAG, "Duplicate finish");
                }
                mFinished = true;
                break;
        }
    }

    /*
     * SKETCH OF OPERATION
     *
     * create one of these PerformUnifiedRestoreTask objects, telling it which
     * dataset & transport to address, and then parameters within the restore
     * operation: single target package vs many, etc.
     *
     * 1. transport.startRestore(token, list-of-packages).  If we need @pm@  it is
     * always placed first and the settings provider always placed last [for now].
     *
     * 1a [if we needed @pm@ then nextRestorePackage() and restore the PMBA inline]
     *
     *   [ state change => RUNNING_QUEUE ]
     *
     * NOW ITERATE:
     *
     * { 3. t.nextRestorePackage()
     *   4. does the metadata for this package allow us to restore it?
     *      does the on-disk app permit us to restore it? [re-check allowBackup etc]
     *   5. is this a key/value dataset?  => key/value agent restore
     *       [ state change => RESTORE_KEYVALUE ]
     *       5a. spin up agent
     *       5b. t.getRestoreData() to stage it properly
     *       5c. call into agent to perform restore
     *       5d. tear down agent
     *       [ state change => RUNNING_QUEUE ]
     *
     *   6. else it's a stream dataset:
     *       [ state change => RESTORE_FULL ]
     *       6a. instantiate the engine for a stream restore: engine handles agent lifecycles
     *       6b. spin off engine runner on separate thread
     *       6c. ITERATE getNextFullRestoreDataChunk() and copy data to engine runner socket
     *       [ state change => RUNNING_QUEUE ]
     * }
     *
     *   [ state change => FINAL ]
     *
     * 7. t.finishRestore(), release wakelock, etc.
     *
     *
     */

    // state INITIAL : set up for the restore and read the metadata if necessary
    private void startRestore() {
        sendStartRestore(mAcceptSet.size());

        // If we're starting a full-system restore, set up to begin widget ID remapping
        if (mIsSystemRestore) {
            // TODO: http://b/22388012
            AppWidgetBackupBridge.restoreStarting(UserHandle.USER_SYSTEM);
        }

        try {
            String transportDirName =
                    mTransportManager.getTransportDirName(mTransportClient.getTransportComponent());
            mStateDir = new File(backupManagerService.getBaseStateDir(), transportDirName);

            // Fetch the current metadata from the dataset first
            PackageInfo pmPackage = new PackageInfo();
            pmPackage.packageName = PACKAGE_MANAGER_SENTINEL;
            mAcceptSet.add(0, pmPackage);

            PackageInfo[] packages = mAcceptSet.toArray(new PackageInfo[0]);

            IBackupTransport transport =
                    mTransportClient.connectOrThrow("PerformUnifiedRestoreTask.startRestore()");

            mStatus = transport.startRestore(mToken, packages);
            if (mStatus != BackupTransport.TRANSPORT_OK) {
                Slog.e(TAG, "Transport error " + mStatus + "; no restore possible");
                mStatus = BackupTransport.TRANSPORT_ERROR;
                executeNextState(UnifiedRestoreState.FINAL);
                return;
            }

            RestoreDescription desc = transport.nextRestorePackage();
            if (desc == null) {
                Slog.e(TAG, "No restore metadata available; halting");
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE,
                        mCurrentPackage,
                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
                mStatus = BackupTransport.TRANSPORT_ERROR;
                executeNextState(UnifiedRestoreState.FINAL);
                return;
            }
            if (!PACKAGE_MANAGER_SENTINEL.equals(
                    desc.getPackageName())) {
                Slog.e(TAG, "Required package metadata but got "
                        + desc.getPackageName());
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED,
                        mCurrentPackage,
                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
                mStatus = BackupTransport.TRANSPORT_ERROR;
                executeNextState(UnifiedRestoreState.FINAL);
                return;
            }

            // Pull the Package Manager metadata from the restore set first
            mCurrentPackage = new PackageInfo();
            mCurrentPackage.packageName = PACKAGE_MANAGER_SENTINEL;
            mPmAgent = backupManagerService.makeMetadataAgent(null);
            mAgent = IBackupAgent.Stub.asInterface(mPmAgent.onBind());
            if (MORE_DEBUG) {
                Slog.v(TAG, "initiating restore for PMBA");
            }
            initiateOneRestore(mCurrentPackage, 0);
            // The PM agent called operationComplete() already, because our invocation
            // of it is process-local and therefore synchronous.  That means that the
            // next-state message (RUNNING_QUEUE) is already enqueued.  Only if we're
            // unable to proceed with running the queue do we remove that pending
            // message and jump straight to the FINAL state.  Because this was
            // synchronous we also know that we should cancel the pending timeout
            // message.
            backupManagerService.getBackupHandler().removeMessages(
                    MSG_RESTORE_OPERATION_TIMEOUT);

            // Verify that the backup set includes metadata.  If not, we can't do
            // signature/version verification etc, so we simply do not proceed with
            // the restore operation.
            if (!mPmAgent.hasMetadata()) {
                Slog.e(TAG, "PM agent has no metadata, so not restoring");
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA,
                        mCurrentPackage,
                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                        PACKAGE_MANAGER_SENTINEL,
                        "Package manager restore metadata missing");
                mStatus = BackupTransport.TRANSPORT_ERROR;
                backupManagerService.getBackupHandler().removeMessages(
                        MSG_BACKUP_RESTORE_STEP, this);
                executeNextState(UnifiedRestoreState.FINAL);
                return;
            }

            // Success; cache the metadata and continue as expected with the
            // next state already enqueued

        } catch (Exception e) {
            // If we lost the transport at any time, halt
            Slog.e(TAG, "Unable to contact transport for restore: " + e.getMessage());
            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT,
                    null,
                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
            mStatus = BackupTransport.TRANSPORT_ERROR;
            backupManagerService.getBackupHandler().removeMessages(
                    MSG_BACKUP_RESTORE_STEP, this);
            executeNextState(UnifiedRestoreState.FINAL);
            return;
        }
    }

    // state RUNNING_QUEUE : figure out what the next thing to be restored is,
    // and fire the appropriate next step
    private void dispatchNextRestore() {
        UnifiedRestoreState nextState = UnifiedRestoreState.FINAL;
        try {
            IBackupTransport transport =
                    mTransportClient.connectOrThrow(
                            "PerformUnifiedRestoreTask.dispatchNextRestore()");
            mRestoreDescription = transport.nextRestorePackage();
            final String pkgName = (mRestoreDescription != null)
                    ? mRestoreDescription.getPackageName() : null;
            if (pkgName == null) {
                Slog.e(TAG, "Failure getting next package name");
                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
                nextState = UnifiedRestoreState.FINAL;
                return;
            } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
                // Yay we've reached the end cleanly
                if (DEBUG) {
                    Slog.v(TAG, "No more packages; finishing restore");
                }
                int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime);
                EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis);
                nextState = UnifiedRestoreState.FINAL;
                return;
            }

            if (DEBUG) {
                Slog.i(TAG, "Next restore package: " + mRestoreDescription);
            }
            sendOnRestorePackage(pkgName);

            Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName);
            if (metaInfo == null) {
                Slog.e(TAG, "No metadata for " + pkgName);
                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
                        "Package metadata missing");
                nextState = UnifiedRestoreState.RUNNING_QUEUE;
                return;
            }

            try {
                mCurrentPackage = backupManagerService.getPackageManager().getPackageInfo(
                        pkgName, PackageManager.GET_SIGNATURES);
            } catch (NameNotFoundException e) {
                // Whoops, we thought we could restore this package but it
                // turns out not to be present.  Skip it.
                Slog.e(TAG, "Package not present: " + pkgName);
                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT,
                        mCurrentPackage,
                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                        null);
                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
                        "Package missing on device");
                nextState = UnifiedRestoreState.RUNNING_QUEUE;
                return;
            }

            if (metaInfo.versionCode > mCurrentPackage.getLongVersionCode()) {
                // Data is from a "newer" version of the app than we have currently
                // installed.  If the app has not declared that it is prepared to
                // handle this case, we do not attempt the restore.
                if ((mCurrentPackage.applicationInfo.flags
                        & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) {
                    String message = "Source version " + metaInfo.versionCode
                            + " > installed version " + mCurrentPackage.getLongVersionCode();
                    Slog.w(TAG, "Package " + pkgName + ": " + message);
                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
                            BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
                            metaInfo.versionCode);
                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
                            monitoringExtras,
                            BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, false);
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                            BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
                            mCurrentPackage,
                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            monitoringExtras);
                    EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                            pkgName, message);
                    nextState = UnifiedRestoreState.RUNNING_QUEUE;
                    return;
                } else {
                    if (DEBUG) {
                        Slog.v(TAG, "Source version " + metaInfo.versionCode
                                + " > installed version " + mCurrentPackage.getLongVersionCode()
                                + " but restoreAnyVersion");
                    }
                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
                            BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
                            metaInfo.versionCode);
                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
                            monitoringExtras,
                            BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, true);
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                            BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
                            mCurrentPackage,
                            BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            monitoringExtras);
                }
            }

            if (MORE_DEBUG) {
                Slog.v(TAG, "Package " + pkgName
                        + " restore version [" + metaInfo.versionCode
                        + "] is compatible with installed version ["
                        + mCurrentPackage.getLongVersionCode() + "]");
            }

            // Reset per-package preconditions and fire the appropriate next state
            mWidgetData = null;
            final int type = mRestoreDescription.getDataType();
            if (type == RestoreDescription.TYPE_KEY_VALUE) {
                nextState = UnifiedRestoreState.RESTORE_KEYVALUE;
            } else if (type == RestoreDescription.TYPE_FULL_STREAM) {
                nextState = UnifiedRestoreState.RESTORE_FULL;
            } else {
                // Unknown restore type; ignore this package and move on
                Slog.e(TAG, "Unrecognized restore type " + type);
                nextState = UnifiedRestoreState.RUNNING_QUEUE;
                return;
            }
        } catch (Exception e) {
            Slog.e(TAG, "Can't get next restore target from transport; halting: "
                    + e.getMessage());
            EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
            nextState = UnifiedRestoreState.FINAL;
            return;
        } finally {
            executeNextState(nextState);
        }
    }

    // state RESTORE_KEYVALUE : restore one package via key/value API set
    private void restoreKeyValue() {
        // Initiating the restore will pass responsibility for the state machine's
        // progress to the agent callback, so we do not always execute the
        // next state here.
        final String packageName = mCurrentPackage.packageName;
        // Validate some semantic requirements that apply in this way
        // only to the key/value restore API flow
        if (mCurrentPackage.applicationInfo.backupAgentName == null
                || "".equals(mCurrentPackage.applicationInfo.backupAgentName)) {
            if (MORE_DEBUG) {
                Slog.i(TAG, "Data exists for package " + packageName
                        + " but app has no agent; skipping");
            }
            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT, mCurrentPackage,
                    BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                    "Package has no agent");
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
            return;
        }

        Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName);
        if (!BackupUtils.signaturesMatch(metaInfo.sigHashes, mCurrentPackage)) {
            Slog.w(TAG, "Signature mismatch restoring " + packageName);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH, mCurrentPackage,
                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                    "Signature mismatch");
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
            return;
        }

        // Good to go!  Set up and bind the agent...
        mAgent = backupManagerService.bindToAgentSynchronous(
                mCurrentPackage.applicationInfo,
                ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL);
        if (mAgent == null) {
            Slog.w(TAG, "Can't find backup agent for " + packageName);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT, mCurrentPackage,
                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                    "Restore agent missing");
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
            return;
        }

        // Whatever happens next, we've launched the target app now; remember that.
        mDidLaunch = true;

        // And then finally start the restore on this agent
        try {
            initiateOneRestore(mCurrentPackage, metaInfo.versionCode);
            ++mCount;
        } catch (Exception e) {
            Slog.e(TAG, "Error when attempting restore: " + e.toString());
            keyValueAgentErrorCleanup();
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
        }
    }

    // Guts of a key/value restore operation
    void initiateOneRestore(PackageInfo app, long appVersionCode) {
        final String packageName = app.packageName;

        if (DEBUG) {
            Slog.d(TAG, "initiateOneRestore packageName=" + packageName);
        }

        // !!! TODO: get the dirs from the transport
        mBackupDataName = new File(backupManagerService.getDataDir(), packageName + ".restore");
        mStageName = new File(backupManagerService.getDataDir(), packageName + ".stage");
        mNewStateName = new File(mStateDir, packageName + ".new");
        mSavedStateName = new File(mStateDir, packageName);

        // don't stage the 'android' package where the wallpaper data lives.  this is
        // an optimization: we know there's no widget data hosted/published by that
        // package, and this way we avoid doing a spurious copy of MB-sized wallpaper
        // data following the download.
        boolean staging = !packageName.equals("android");
        ParcelFileDescriptor stage;
        File downloadFile = (staging) ? mStageName : mBackupDataName;

        try {
            IBackupTransport transport =
                    mTransportClient.connectOrThrow(
                            "PerformUnifiedRestoreTask.initiateOneRestore()");

            // Run the transport's restore pass
            stage = ParcelFileDescriptor.open(downloadFile,
                    ParcelFileDescriptor.MODE_READ_WRITE |
                            ParcelFileDescriptor.MODE_CREATE |
                            ParcelFileDescriptor.MODE_TRUNCATE);

            if (transport.getRestoreData(stage) != BackupTransport.TRANSPORT_OK) {
                // Transport-level failure, so we wind everything up and
                // terminate the restore operation.
                Slog.e(TAG, "Error getting restore data for " + packageName);
                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
                stage.close();
                downloadFile.delete();
                executeNextState(UnifiedRestoreState.FINAL);
                return;
            }

            // We have the data from the transport. Now we extract and strip
            // any per-package metadata (typically widget-related information)
            // if appropriate
            if (staging) {
                stage.close();
                stage = ParcelFileDescriptor.open(downloadFile,
                        ParcelFileDescriptor.MODE_READ_ONLY);

                mBackupData = ParcelFileDescriptor.open(mBackupDataName,
                        ParcelFileDescriptor.MODE_READ_WRITE |
                                ParcelFileDescriptor.MODE_CREATE |
                                ParcelFileDescriptor.MODE_TRUNCATE);

                BackupDataInput in = new BackupDataInput(stage.getFileDescriptor());
                BackupDataOutput out = new BackupDataOutput(mBackupData.getFileDescriptor());
                byte[] buffer = new byte[8192]; // will grow when needed
                while (in.readNextHeader()) {
                    final String key = in.getKey();
                    final int size = in.getDataSize();

                    // is this a special key?
                    if (key.equals(KEY_WIDGET_STATE)) {
                        if (DEBUG) {
                            Slog.i(TAG, "Restoring widget state for " + packageName);
                        }
                        mWidgetData = new byte[size];
                        in.readEntityData(mWidgetData, 0, size);
                    } else {
                        if (size > buffer.length) {
                            buffer = new byte[size];
                        }
                        in.readEntityData(buffer, 0, size);
                        out.writeEntityHeader(key, size);
                        out.writeEntityData(buffer, size);
                    }
                }

                mBackupData.close();
            }

            // Okay, we have the data.  Now have the agent do the restore.
            stage.close();

            mBackupData = ParcelFileDescriptor.open(mBackupDataName,
                    ParcelFileDescriptor.MODE_READ_ONLY);

            mNewState = ParcelFileDescriptor.open(mNewStateName,
                    ParcelFileDescriptor.MODE_READ_WRITE |
                            ParcelFileDescriptor.MODE_CREATE |
                            ParcelFileDescriptor.MODE_TRUNCATE);

            // Kick off the restore, checking for hung agents.  The timeout or
            // the operationComplete() callback will schedule the next step,
            // so we do not do that here.
            backupManagerService.prepareOperationTimeout(
                    mEphemeralOpToken, TIMEOUT_RESTORE_INTERVAL, this, OP_TYPE_RESTORE_WAIT);
            mAgent.doRestore(mBackupData, appVersionCode, mNewState,
                    mEphemeralOpToken, backupManagerService.getBackupManagerBinder());
        } catch (Exception e) {
            Slog.e(TAG, "Unable to call app for restore: " + packageName, e);
            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                    packageName, e.toString());
            keyValueAgentErrorCleanup();    // clears any pending timeout messages as well

            // After a restore failure we go back to running the queue.  If there
            // are no more packages to be restored that will be handled by the
            // next step.
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
        }
    }

    // state RESTORE_FULL : restore one package via streaming engine
    private void restoreFull() {
        // None of this can run on the work looper here, so we spin asynchronous
        // work like this:
        //
        //   StreamFeederThread: read data from transport.getNextFullRestoreDataChunk()
        //                       write it into the pipe to the engine
        //   EngineThread: FullRestoreEngine thread communicating with the target app
        //
        // When finished, StreamFeederThread executes next state as appropriate on the
        // backup looper, and the overall unified restore task resumes
        try {
            StreamFeederThread feeder = new StreamFeederThread();
            if (MORE_DEBUG) {
                Slog.i(TAG, "Spinning threads for stream restore of "
                        + mCurrentPackage.packageName);
            }
            new Thread(feeder, "unified-stream-feeder").start();

            // At this point the feeder is responsible for advancing the restore
            // state, so we're done here.
        } catch (IOException e) {
            // Unable to instantiate the feeder thread -- we need to bail on the
            // current target.  We haven't asked the transport for data yet, though,
            // so we can do that simply by going back to running the restore queue.
            Slog.e(TAG, "Unable to construct pipes for stream restore!");
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
        }
    }

    // state RESTORE_FINISHED : provide the "no more data" signpost callback at the end
    private void restoreFinished() {
        if (DEBUG) {
            Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName);
        }
        try {
            backupManagerService
                    .prepareOperationTimeout(mEphemeralOpToken,
                            TIMEOUT_RESTORE_FINISHED_INTERVAL, this,
                            OP_TYPE_RESTORE_WAIT);
            mAgent.doRestoreFinished(mEphemeralOpToken,
                    backupManagerService.getBackupManagerBinder());
            // If we get this far, the callback or timeout will schedule the
            // next restore state, so we're done
        } catch (Exception e) {
            final String packageName = mCurrentPackage.packageName;
            Slog.e(TAG, "Unable to finalize restore of " + packageName);
            EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                    packageName, e.toString());
            keyValueAgentErrorCleanup();
            executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
        }
    }

    class StreamFeederThread extends RestoreEngine implements Runnable, BackupRestoreTask {

        final String TAG = "StreamFeederThread";
        FullRestoreEngine mEngine;
        EngineThread mEngineThread;

        // pipe through which we read data from the transport. [0] read, [1] write
        ParcelFileDescriptor[] mTransportPipes;

        // pipe through which the engine will read data.  [0] read, [1] write
        ParcelFileDescriptor[] mEnginePipes;

        private final int mEphemeralOpToken;

        public StreamFeederThread() throws IOException {
            mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
            mTransportPipes = ParcelFileDescriptor.createPipe();
            mEnginePipes = ParcelFileDescriptor.createPipe();
            setRunning(true);
        }

        @Override
        public void run() {
            UnifiedRestoreState nextState = UnifiedRestoreState.RUNNING_QUEUE;
            int status = BackupTransport.TRANSPORT_OK;

            EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
                    mCurrentPackage.packageName);

            mEngine = new FullRestoreEngine(backupManagerService, this, null,
                    mMonitor, mCurrentPackage, false, false, mEphemeralOpToken);
            mEngineThread = new EngineThread(mEngine, mEnginePipes[0]);

            ParcelFileDescriptor eWriteEnd = mEnginePipes[1];
            ParcelFileDescriptor tReadEnd = mTransportPipes[0];
            ParcelFileDescriptor tWriteEnd = mTransportPipes[1];

            int bufferSize = 32 * 1024;
            byte[] buffer = new byte[bufferSize];
            FileOutputStream engineOut = new FileOutputStream(eWriteEnd.getFileDescriptor());
            FileInputStream transportIn = new FileInputStream(tReadEnd.getFileDescriptor());

            // spin up the engine and start moving data to it
            new Thread(mEngineThread, "unified-restore-engine").start();

            String callerLogString = "PerformUnifiedRestoreTask$StreamFeederThread.run()";
            try {
                IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString);
                while (status == BackupTransport.TRANSPORT_OK) {
                    // have the transport write some of the restoring data to us
                    int result = transport.getNextFullRestoreDataChunk(tWriteEnd);
                    if (result > 0) {
                        // The transport wrote this many bytes of restore data to the
                        // pipe, so pass it along to the engine.
                        if (MORE_DEBUG) {
                            Slog.v(TAG, "  <- transport provided chunk size " + result);
                        }
                        if (result > bufferSize) {
                            bufferSize = result;
                            buffer = new byte[bufferSize];
                        }
                        int toCopy = result;
                        while (toCopy > 0) {
                            int n = transportIn.read(buffer, 0, toCopy);
                            engineOut.write(buffer, 0, n);
                            toCopy -= n;
                            if (MORE_DEBUG) {
                                Slog.v(TAG, "  -> wrote " + n + " to engine, left=" + toCopy);
                            }
                        }
                    } else if (result == BackupTransport.NO_MORE_DATA) {
                        // Clean finish.  Wind up and we're done!
                        if (MORE_DEBUG) {
                            Slog.i(TAG, "Got clean full-restore EOF for "
                                    + mCurrentPackage.packageName);
                        }
                        status = BackupTransport.TRANSPORT_OK;
                        break;
                    } else {
                        // Transport reported some sort of failure; the fall-through
                        // handling will deal properly with that.
                        Slog.e(TAG, "Error " + result + " streaming restore for "
                                + mCurrentPackage.packageName);
                        EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
                        status = result;
                    }
                }
                if (MORE_DEBUG) {
                    Slog.v(TAG, "Done copying to engine, falling through");
                }
            } catch (IOException e) {
                // We lost our ability to communicate via the pipes.  That's worrying
                // but potentially recoverable; abandon this package's restore but
                // carry on with the next restore target.
                Slog.e(TAG, "Unable to route data for restore");
                EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                        mCurrentPackage.packageName, "I/O error on pipes");
                status = BackupTransport.AGENT_ERROR;
            } catch (Exception e) {
                // The transport threw; terminate the whole operation.  Closing
                // the sockets will wake up the engine and it will then tidy up the
                // remote end.
                Slog.e(TAG, "Transport failed during restore: " + e.getMessage());
                EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
                status = BackupTransport.TRANSPORT_ERROR;
            } finally {
                // Close the transport pipes and *our* end of the engine pipe,
                // but leave the engine thread's end open so that it properly
                // hits EOF and winds up its operations.
                IoUtils.closeQuietly(mEnginePipes[1]);
                IoUtils.closeQuietly(mTransportPipes[0]);
                IoUtils.closeQuietly(mTransportPipes[1]);

                // Don't proceed until the engine has wound up operations
                mEngineThread.waitForResult();

                // Now we're really done with this one too
                IoUtils.closeQuietly(mEnginePipes[0]);

                // In all cases we want to remember whether we launched
                // the target app as part of our work so far.
                mDidLaunch = (mEngine.getAgent() != null);

                // If we hit a transport-level error, we are done with everything;
                // if we hit an agent error we just go back to running the queue.
                if (status == BackupTransport.TRANSPORT_OK) {
                    // Clean finish means we issue the restore-finished callback
                    nextState = UnifiedRestoreState.RESTORE_FINISHED;

                    // the engine bound the target's agent, so recover that binding
                    // to use for the callback.
                    mAgent = mEngine.getAgent();

                    // and the restored widget data, if any
                    mWidgetData = mEngine.getWidgetData();
                } else {
                    // Something went wrong somewhere.  Whether it was at the transport
                    // level is immaterial; we need to tell the transport to bail
                    try {
                        IBackupTransport transport =
                                mTransportClient.connectOrThrow(callerLogString);
                        transport.abortFullRestore();
                    } catch (Exception e) {
                        // transport itself is dead; make sure we handle this as a
                        // fatal error
                        Slog.e(TAG, "Transport threw from abortFullRestore: " + e.getMessage());
                        status = BackupTransport.TRANSPORT_ERROR;
                    }

                    // We also need to wipe the current target's data, as it's probably
                    // in an incoherent state.
                    backupManagerService.clearApplicationDataSynchronous(
                            mCurrentPackage.packageName, false);

                    // Schedule the next state based on the nature of our failure
                    if (status == BackupTransport.TRANSPORT_ERROR) {
                        nextState = UnifiedRestoreState.FINAL;
                    } else {
                        nextState = UnifiedRestoreState.RUNNING_QUEUE;
                    }
                }
                executeNextState(nextState);
                setRunning(false);
            }
        }

        // BackupRestoreTask interface, specifically for timeout handling

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

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

        // The app has timed out handling a restoring file
        @Override
        public void handleCancel(boolean cancelAll) {
            backupManagerService.removeOperation(mEphemeralOpToken);
            if (DEBUG) {
                Slog.w(TAG, "Full-data restore target timed out; shutting down");
            }

            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT,
                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
            mEngineThread.handleTimeout();

            IoUtils.closeQuietly(mEnginePipes[1]);
            mEnginePipes[1] = null;
            IoUtils.closeQuietly(mEnginePipes[0]);
            mEnginePipes[0] = null;
        }
    }

    class EngineThread implements Runnable {

        FullRestoreEngine mEngine;
        FileInputStream mEngineStream;

        EngineThread(FullRestoreEngine engine, ParcelFileDescriptor engineSocket) {
            mEngine = engine;
            engine.setRunning(true);
            // We *do* want this FileInputStream to own the underlying fd, so that
            // when we are finished with it, it closes this end of the pipe in a way
            // that signals its other end.
            mEngineStream = new FileInputStream(engineSocket.getFileDescriptor(), true);
        }

        public boolean isRunning() {
            return mEngine.isRunning();
        }

        public int waitForResult() {
            return mEngine.waitForResult();
        }

        @Override
        public void run() {
            try {
                while (mEngine.isRunning()) {
                    // Tell it to be sure to leave the agent instance up after finishing
                    mEngine.restoreOneFile(mEngineStream, false, mEngine.mBuffer,
                            mEngine.mOnlyPackage, mEngine.mAllowApks, mEngine.mEphemeralOpToken,
                            mEngine.mMonitor);
                }
            } finally {
                // Because mEngineStream adopted its underlying FD, this also
                // closes this end of the pipe.
                IoUtils.closeQuietly(mEngineStream);
            }
        }

        public void handleTimeout() {
            IoUtils.closeQuietly(mEngineStream);
            mEngine.handleTimeout();
        }
    }

    // state FINAL : tear everything down and we're done.
    private void finalizeRestore() {
        if (MORE_DEBUG) {
            Slog.d(TAG, "finishing restore mObserver=" + mObserver);
        }

        String callerLogString = "PerformUnifiedRestoreTask.finalizeRestore()";
        try {
            IBackupTransport transport =
                    mTransportClient.connectOrThrow(callerLogString);
            transport.finishRestore();
        } catch (Exception e) {
            Slog.e(TAG, "Error finishing restore", e);
        }

        // Tell the observer we're done
        if (mObserver != null) {
            try {
                mObserver.restoreFinished(mStatus);
            } catch (RemoteException e) {
                Slog.d(TAG, "Restore observer died at restoreFinished");
            }
        }

        // Clear any ongoing session timeout.
        backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);

        // If we have a PM token, we must under all circumstances be sure to
        // handshake when we've finished.
        if (mPmToken > 0) {
            if (MORE_DEBUG) {
                Slog.v(TAG, "finishing PM token " + mPmToken);
            }
            try {
                backupManagerService.getPackageManagerBinder().finishPackageInstall(mPmToken,
                        mDidLaunch);
            } catch (RemoteException e) { /* can't happen */ }
        } else {
            // We were invoked via an active restore session, not by the Package
            // Manager, so start up the session timeout again.
            backupManagerService.getBackupHandler().sendEmptyMessageDelayed(
                    MSG_RESTORE_SESSION_TIMEOUT,
                    TIMEOUT_RESTORE_INTERVAL);
        }

        // Kick off any work that may be needed regarding app widget restores
        // TODO: http://b/22388012
        AppWidgetBackupBridge.restoreFinished(UserHandle.USER_SYSTEM);

        // If this was a full-system restore, record the ancestral
        // dataset information
        if (mIsSystemRestore && mPmAgent != null) {
            backupManagerService.setAncestralPackages(mPmAgent.getRestoredPackages());
            backupManagerService.setAncestralToken(mToken);
            backupManagerService.writeRestoreTokens();
        }

        synchronized (backupManagerService.getPendingRestores()) {
            if (backupManagerService.getPendingRestores().size() > 0) {
                if (DEBUG) {
                    Slog.d(TAG, "Starting next pending restore.");
                }
                PerformUnifiedRestoreTask task = backupManagerService.getPendingRestores().remove();
                backupManagerService.getBackupHandler().sendMessage(
                        backupManagerService.getBackupHandler().obtainMessage(
                                MSG_BACKUP_RESTORE_STEP, task));

            } else {
                backupManagerService.setRestoreInProgress(false);
                if (MORE_DEBUG) {
                    Slog.d(TAG, "No pending restores.");
                }
            }
        }

        Slog.i(TAG, "Restore complete.");
        mListener.onFinished(callerLogString);
    }

    void keyValueAgentErrorCleanup() {
        // If the agent fails restore, it might have put the app's data
        // into an incoherent state.  For consistency we wipe its data
        // again in this case before continuing with normal teardown
        backupManagerService.clearApplicationDataSynchronous(mCurrentPackage.packageName, false);
        keyValueAgentCleanup();
    }

    // TODO: clean up naming; this is now used at finish by both k/v and stream restores
    void keyValueAgentCleanup() {
        mBackupDataName.delete();
        mStageName.delete();
        try {
            if (mBackupData != null) {
                mBackupData.close();
            }
        } catch (IOException e) {
        }
        try {
            if (mNewState != null) {
                mNewState.close();
            }
        } catch (IOException e) {
        }
        mBackupData = mNewState = null;

        // if everything went okay, remember the recorded state now
        //
        // !!! TODO: the restored data could be migrated on the server
        // side into the current dataset.  In that case the new state file
        // we just created would reflect the data already extant in the
        // backend, so there'd be nothing more to do.  Until that happens,
        // however, we need to make sure that we record the data to the
        // current backend dataset.  (Yes, this means shipping the data over
        // the wire in both directions.  That's bad, but consistency comes
        // first, then efficiency.)  Once we introduce server-side data
        // migration to the newly-restored device's dataset, we will change
        // the following from a discard of the newly-written state to the
        // "correct" operation of renaming into the canonical state blob.
        mNewStateName.delete();                      // TODO: remove; see above comment
        //mNewStateName.renameTo(mSavedStateName);   // TODO: replace with this

        // If this wasn't the PM pseudopackage, tear down the agent side
        if (mCurrentPackage.applicationInfo != null) {
            // unbind and tidy up even on timeout or failure
            try {
                backupManagerService.getActivityManager().unbindBackupAgent(
                        mCurrentPackage.applicationInfo);

                // The agent was probably running with a stub Application object,
                // which isn't a valid run mode for the main app logic.  Shut
                // down the app so that next time it's launched, it gets the
                // usual full initialization.  Note that this is only done for
                // full-system restores: when a single app has requested a restore,
                // it is explicitly not killed following that operation.
                //
                // We execute this kill when these conditions hold:
                //    1. it's not a system-uid process,
                //    2. the app did not request its own restore (mTargetPackage == null), and
                // either
                //    3a. the app is a full-data target (TYPE_FULL_STREAM) or
                //     b. the app does not state android:killAfterRestore="false" in its manifest
                final int appFlags = mCurrentPackage.applicationInfo.flags;
                final boolean killAfterRestore =
                        (mCurrentPackage.applicationInfo.uid >= Process.FIRST_APPLICATION_UID)
                                && ((mRestoreDescription.getDataType()
                                == RestoreDescription.TYPE_FULL_STREAM)
                                || ((appFlags & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0));

                if (mTargetPackage == null && killAfterRestore) {
                    if (DEBUG) {
                        Slog.d(TAG, "Restore complete, killing host process of "
                                + mCurrentPackage.applicationInfo.processName);
                    }
                    backupManagerService.getActivityManager().killApplicationProcess(
                            mCurrentPackage.applicationInfo.processName,
                            mCurrentPackage.applicationInfo.uid);
                }
            } catch (RemoteException e) {
                // can't happen; we run in the same process as the activity manager
            }
        }

        // The caller is responsible for reestablishing the state machine; our
        // responsibility here is to clear the decks for whatever comes next.
        backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_OPERATION_TIMEOUT, this);
    }

    @Override
    public void operationComplete(long unusedResult) {
        backupManagerService.removeOperation(mEphemeralOpToken);
        if (MORE_DEBUG) {
            Slog.i(TAG, "operationComplete() during restore: target="
                    + mCurrentPackage.packageName
                    + " state=" + mState);
        }

        final UnifiedRestoreState nextState;
        switch (mState) {
            case INITIAL:
                // We've just (manually) restored the PMBA.  It doesn't need the
                // additional restore-finished callback so we bypass that and go
                // directly to running the queue.
                nextState = UnifiedRestoreState.RUNNING_QUEUE;
                break;

            case RESTORE_KEYVALUE:
            case RESTORE_FULL: {
                // Okay, we've just heard back from the agent that it's done with
                // the restore itself.  We now have to send the same agent its
                // doRestoreFinished() callback, so roll into that state.
                nextState = UnifiedRestoreState.RESTORE_FINISHED;
                break;
            }

            case RESTORE_FINISHED: {
                // Okay, we're done with this package.  Tidy up and go on to the next
                // app in the queue.
                int size = (int) mBackupDataName.length();
                EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE,
                        mCurrentPackage.packageName, size);

                // Just go back to running the restore queue
                keyValueAgentCleanup();

                // If there was widget state associated with this app, get the OS to
                // incorporate it into current bookeeping and then pass that along to
                // the app as part of the restore-time work.
                if (mWidgetData != null) {
                    backupManagerService.restoreWidgetData(mCurrentPackage.packageName,
                            mWidgetData);
                }

                nextState = UnifiedRestoreState.RUNNING_QUEUE;
                break;
            }

            default: {
                // Some kind of horrible semantic error; we're in an unexpected state.
                // Back off hard and wind up.
                Slog.e(TAG, "Unexpected restore callback into state " + mState);
                keyValueAgentErrorCleanup();
                nextState = UnifiedRestoreState.FINAL;
                break;
            }
        }

        executeNextState(nextState);
    }

    // A call to agent.doRestore() or agent.doRestoreFinished() has timed out
    @Override
    public void handleCancel(boolean cancelAll) {
        backupManagerService.removeOperation(mEphemeralOpToken);
        Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
                mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
        EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                mCurrentPackage.packageName, "restore timeout");
        // Handle like an agent that threw on invocation: wipe it and go on to the next
        keyValueAgentErrorCleanup();
        executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
    }

    void executeNextState(UnifiedRestoreState nextState) {
        if (MORE_DEBUG) {
            Slog.i(TAG, " => executing next step on "
                    + this + " nextState=" + nextState);
        }
        mState = nextState;
        Message msg = backupManagerService.getBackupHandler().obtainMessage(
                MSG_BACKUP_RESTORE_STEP, this);
        backupManagerService.getBackupHandler().sendMessage(msg);
    }

    // restore observer support
    void sendStartRestore(int numPackages) {
        if (mObserver != null) {
            try {
                mObserver.restoreStarting(numPackages);
            } catch (RemoteException e) {
                Slog.w(TAG, "Restore observer went away: startRestore");
                mObserver = null;
            }
        }
    }

    void sendOnRestorePackage(String name) {
        if (mObserver != null) {
            try {
                mObserver.onUpdate(mCount, name);
            } catch (RemoteException e) {
                Slog.d(TAG, "Restore observer died in onUpdate");
                mObserver = null;
            }
        }
    }

    void sendEndRestore() {
        if (mObserver != null) {
            try {
                mObserver.restoreFinished(mStatus);
            } catch (RemoteException e) {
                Slog.w(TAG, "Restore observer went away: endRestore");
                mObserver = null;
            }
        }
    }
}
