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

import static com.android.server.backup.BackupManagerService.TAG;

import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.backup.BackupTransport;
import android.app.backup.IBackupObserver;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.EventLog;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.server.EventLogTags;
import com.android.server.backup.BackupManagerService;
import com.android.server.backup.TransportManager;
import com.android.server.backup.transport.TransportClient;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * Attempts to call {@link BackupTransport#initializeDevice()} followed by
 * {@link BackupTransport#finishBackup()} for the transport names passed in with the intent of
 * wiping backup data from the transport.
 *
 * If the transport returns error, it will record the operation as pending and schedule it to run in
 * a future time according to {@link BackupTransport#requestBackupTime()}. The result status
 * reported to observers will be the last unsuccessful status reported by the transports. If every
 * operation was successful then it's {@link BackupTransport#TRANSPORT_OK}.
 */
public class PerformInitializeTask implements Runnable {
    private final BackupManagerService mBackupManagerService;
    private final TransportManager mTransportManager;
    private final String[] mQueue;
    private final File mBaseStateDir;
    private final OnTaskFinishedListener mListener;
    @Nullable private IBackupObserver mObserver;

    public PerformInitializeTask(
            BackupManagerService backupManagerService,
            String[] transportNames,
            @Nullable IBackupObserver observer,
            OnTaskFinishedListener listener) {
        this(
                backupManagerService,
                backupManagerService.getTransportManager(),
                transportNames,
                observer,
                listener,
                backupManagerService.getBaseStateDir());
    }

    @VisibleForTesting
    PerformInitializeTask(
            BackupManagerService backupManagerService,
            TransportManager transportManager,
            String[] transportNames,
            @Nullable IBackupObserver observer,
            OnTaskFinishedListener listener,
            File baseStateDir) {
        mBackupManagerService = backupManagerService;
        mTransportManager = transportManager;
        mQueue = transportNames;
        mObserver = observer;
        mListener = listener;
        mBaseStateDir = baseStateDir;
    }

    private void notifyResult(String target, int status) {
        try {
            if (mObserver != null) {
                mObserver.onResult(target, status);
            }
        } catch (RemoteException ignored) {
            mObserver = null;       // don't try again
        }
    }

    private void notifyFinished(int status) {
        try {
            if (mObserver != null) {
                mObserver.backupFinished(status);
            }
        } catch (RemoteException ignored) {
            mObserver = null;
        }
    }

    public void run() {
        // mWakelock is *acquired* when execution begins here
        String callerLogString = "PerformInitializeTask.run()";
        List<TransportClient> transportClientsToDisposeOf = new ArrayList<>(mQueue.length);
        int result = BackupTransport.TRANSPORT_OK;
        try {
            for (String transportName : mQueue) {
                TransportClient transportClient =
                        mTransportManager.getTransportClient(transportName, callerLogString);
                if (transportClient == null) {
                    Slog.e(TAG, "Requested init for " + transportName + " but not found");
                    continue;
                }
                transportClientsToDisposeOf.add(transportClient);

                Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName);
                String transportDirName =
                        mTransportManager.getTransportDirName(
                                transportClient.getTransportComponent());
                EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
                long startRealtime = SystemClock.elapsedRealtime();

                IBackupTransport transport = transportClient.connectOrThrow(callerLogString);
                int status = transport.initializeDevice();

                if (status == BackupTransport.TRANSPORT_OK) {
                    status = transport.finishBackup();
                }

                // Okay, the wipe really happened.  Clean up our local bookkeeping.
                if (status == BackupTransport.TRANSPORT_OK) {
                    Slog.i(TAG, "Device init successful");
                    int millis = (int) (SystemClock.elapsedRealtime() - startRealtime);
                    EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
                    File stateFileDir = new File(mBaseStateDir, transportDirName);
                    mBackupManagerService.resetBackupState(stateFileDir);
                    EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis);
                    mBackupManagerService.recordInitPending(false, transportName, transportDirName);
                    notifyResult(transportName, BackupTransport.TRANSPORT_OK);
                } else {
                    // If this didn't work, requeue this one and try again
                    // after a suitable interval
                    Slog.e(TAG, "Transport error in initializeDevice()");
                    EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
                    mBackupManagerService.recordInitPending(true, transportName, transportDirName);
                    notifyResult(transportName, status);
                    result = status;

                    // do this via another alarm to make sure of the wakelock states
                    long delay = transport.requestBackupTime();
                    Slog.w(TAG, "Init failed on " + transportName + " resched in " + delay);
                    mBackupManagerService.getAlarmManager().set(
                            AlarmManager.RTC_WAKEUP,
                            System.currentTimeMillis() + delay,
                            mBackupManagerService.getRunInitIntent());
                }
            }
        } catch (Exception e) {
            Slog.e(TAG, "Unexpected error performing init", e);
            result = BackupTransport.TRANSPORT_ERROR;
        } finally {
            for (TransportClient transportClient : transportClientsToDisposeOf) {
                mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
            }
            notifyFinished(result);
            mListener.onFinished(callerLogString);
        }
    }
}
