uk.ac.ucl.excites.sapelli.collector.tasks.Backup.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ucl.excites.sapelli.collector.tasks.Backup.java

Source

/**
 * Sapelli data collection platform: http://sapelli.org
 * 
 * Copyright 2012-2016 University College London - ExCiteS group
 * 
 * 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 uk.ac.ucl.excites.sapelli.collector.tasks;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.FileUtils;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.view.ContextThemeWrapper;
import uk.ac.ucl.excites.sapelli.collector.CollectorApp;
import uk.ac.ucl.excites.sapelli.collector.CollectorClient;
import uk.ac.ucl.excites.sapelli.collector.R;
import uk.ac.ucl.excites.sapelli.collector.activities.BaseActivity;
import uk.ac.ucl.excites.sapelli.collector.fragments.ExportFragment;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider;
import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider.Folder;
import uk.ac.ucl.excites.sapelli.collector.util.AsyncTaskWithWaitingDialog;
import uk.ac.ucl.excites.sapelli.shared.db.StoreBackupper;
import uk.ac.ucl.excites.sapelli.shared.io.FileHelpers;
import uk.ac.ucl.excites.sapelli.shared.io.Zipper;
import uk.ac.ucl.excites.sapelli.shared.util.ExceptionHelpers;
import uk.ac.ucl.excites.sapelli.shared.util.android.Debug;
import uk.ac.ucl.excites.sapelli.storage.eximport.ExportResult;
import uk.ac.ucl.excites.sapelli.storage.model.Record;
import uk.ac.ucl.excites.sapelli.storage.queries.Order;
import uk.ac.ucl.excites.sapelli.storage.queries.RecordsQuery;
import uk.ac.ucl.excites.sapelli.storage.queries.sources.Source;

/**
 * Sapelli Collector Back-up procedure 
 * 
 * @author Michalis Vitos, mstevens
 */
public class Backup implements RecordsTasks.QueryCallback, RecordsTasks.ExportCallback {

    // STATIC -----------------------------------------------------------------
    static public final Folder[] BACKUPABLE_FOLDERS = { Folder.Attachments, Folder.Crashes, Folder.Export,
            Folder.Logs, Folder.Projects };
    static public final String EMPTY_FILE = ".empty";

    static private int getFolderStringID(Folder folder) {
        switch (folder) {
        case Crashes:
            return R.string.folderCrashes;
        case Export:
            return R.string.folderExports;
        case Logs:
            return R.string.folderLogs;
        case Attachments:
            return R.string.folderAttachments;
        case Projects:
            return R.string.folderProjects;
        // Not back-upable:
        case Downloads:
        case Temp:
        case DB: // in fact this is will always included in back-up but not directly, only after DB(s) has/have been copied to a temp folder
        case OldDBVersions: // in fact this is will always included in back-up, but it isn't offered as a user choice
        default:
            throw new IllegalArgumentException("This folder (" + folder.name() + ") cannot be backed-up!");
        }
    }

    static private boolean isFolderDefaultSelected(Folder folder) {
        switch (folder) {
        case Attachments:
        case Crashes:
        case Export:
        case Logs:
            return true;
        case Projects:
            return false;
        // Not back-upable:
        case Downloads:
        case Temp:
        case DB: // (see comment above)
        case OldDBVersions: // (see comment above)
        default:
            throw new IllegalArgumentException("This folder (" + folder.name() + ") cannot be backed-up!");
        }
    }

    static public void Run(BaseActivity activity, FileStorageProvider fileStorageProvider) {
        // create Backup instance and start the back-up process by showing the selection diagram...
        new Backup(activity, fileStorageProvider).showSelectionDialog();
    }

    // DYNAMIC ----------------------------------------------------------------
    private final BaseActivity activity;
    private final FileStorageProvider fileStorageProvider;
    private final Set<Folder> foldersToExport;

    private final Runnable runBackup;

    private Backup(BaseActivity activity, FileStorageProvider fileStorageProvider) {
        // Initialise:
        this.activity = activity;
        this.fileStorageProvider = fileStorageProvider;
        foldersToExport = new HashSet<Folder>();
        // Already add the OldDBVersion folder (not user-selectable but always included):
        foldersToExport.add(Folder.OldDBVersions);

        // Create runnable:
        runBackup = new Runnable() {
            @Override
            public void run() {
                doBackup(); // go straight to back-up
            }
        };
    }

    /**
     * Brings up the selection dialog (= start of back-up procedure)
     * 
     * Note:
     *    Due to an Android bug (reported by mstevens: https://code.google.com/p/android/issues/detail?id=187416)
     *    we cannot insert a header into the ListView on this dialog as it causes Android to use incorrect list
     *    item indexes. Therefore we use the message as the title of the dialog (ugly). A future alternative (TODO)
     *    may be to refactor this class as a DialogFragment in which the message is shown in a separate TextView
     *    above the list instead of a headerView "in" the ListView.
     */
    private void showSelectionDialog() {
        // Initialise folder selection:
        CharSequence[] checkboxItems = new CharSequence[BACKUPABLE_FOLDERS.length];
        boolean[] checkedItems = new boolean[BACKUPABLE_FOLDERS.length];
        int f = 0;
        for (Folder folder : BACKUPABLE_FOLDERS) {
            checkboxItems[f] = activity.getString(getFolderStringID(folder));
            if (checkedItems[f] = isFolderDefaultSelected(folder))
                foldersToExport.add(folder);
            f++;
        }

        // Get dialog builder & configure the dialog...
        AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme))
                //   Set icon:
                .setIcon(R.drawable.ic_content_save_black_36dp)
                //   Set title:
                .setTitle(R.string.selectForBackup) // R.string.backup)
                //   Set multiple choice:
                .setMultiChoiceItems(checkboxItems, checkedItems,
                        // Choice click event handler:
                        new DialogInterface.OnMultiChoiceClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                                if (isChecked)
                                    // If the user checked the item, add it to the selected items:
                                    foldersToExport.add(BACKUPABLE_FOLDERS[which]);
                                else
                                    // Else, if the item is already in the array, remove it:
                                    foldersToExport.remove(BACKUPABLE_FOLDERS[which]);
                            }
                        })
                // Set OK button:
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {
                        if (foldersToExport.contains(Folder.Export))
                            showExportYesNoDialog(); // propose creation of a fresh full project data export
                        else
                            doBackup(); // go straight to back-up
                    }
                })
                // Set Cancel button:
                .setNegativeButton(android.R.string.cancel, null);
        // Create the dialog:
        AlertDialog dialog = builder.create();
        // Add message above list:
        /*TextView lblMsg = new TextView(activity);
        int lrPadding = ViewHelpers.getDefaultDialogPaddingPx(activity);
        lblMsg.setPadding(lrPadding, 0, lrPadding, 0);
        lblMsg.setTextAppearance(activity, android.R.style.TextAppearance_Medium);
        lblMsg.setText(R.string.selectForBackup);
        dialog.getListView().addHeaderView(lblMsg);*/
        // Show the dialog:
        dialog.show();
    }

    private void showExportYesNoDialog() {
        // TODO query _before_ asking!?
        // Get dialog builder & configure the dialog...
        new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme))
                //   Set title:
                .setTitle(R.string.preBackupExportTitle)
                //   Set message:
                .setMessage(R.string.preBackupExportMsg)
                // Set "Yes button:
                .setPositiveButton(R.string.preBackupExportYes, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // Query to see if there are any records to export:
                        new RecordsTasks.QueryTask(activity, Backup.this).execute(new RecordsQuery(
                                Source.With(CollectorClient.SCHEMA_FLAGS_COLLECTOR_DATA), Order.UNDEFINED));
                        // TODO order by form, deviceid, timestamp
                        // TODO let Backup & ExportFragment share this code somehow
                    }
                })
                // Set "No" button:
                .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) { // go straight to back-up:
                        doBackup();
                    }
                })
                // Create & show the dialog:
                .create().show();
    }

    @Override
    public void querySuccess(final List<Record> result) {
        if (result != null && !result.isEmpty()) {
            String title = activity.getString(R.string.preBackupExportTitle);
            ExportFragment.ShowChoseFormatDialog(activity, title,
                    activity.getString(R.string.preBackupExportFormatMsg, result.size()), false,
                    new ExportFragment.FormatDialogCallback() {
                        @Override
                        public void onFormatChosen(ExportFragment formatFragment) {
                            RecordsTasks.runExportTask(activity, result, formatFragment,
                                    fileStorageProvider.getExportFolder(true), activity.getString(R.string.backup),
                                    Backup.this);
                        }
                    });
        } else {
            activity.showOKDialog(R.string.preBackupExportTitle, activity.getString(R.string.exportNoRecordsFound)
                    + "\n" + activity.getString(R.string.backup_continue), false, runBackup);
        }
    }

    @Override
    public void queryFailure(Exception reason) {
        activity.showOKDialog(R.string.preBackupExportTitle,
                activity.getString(R.string.exportQueryFailed, ExceptionHelpers.getMessageAndCause(reason)) + "\n"
                        + activity.getString(R.string.backup_continue),
                false, runBackup);
    }

    @Override
    public void exportDone(ExportResult result) {
        if (!result.wasSuccessful()) {
            activity.showOKDialog(R.string.preBackupExportTitle, (result.getNumberedOfExportedRecords() > 0
                    ? activity.getString(R.string.exportPartialSuccessMsg, result.getNumberedOfExportedRecords(),
                            result.getDestination(), result.getNumberOfUnexportedRecords(),
                            ExceptionHelpers.getMessageAndCause(result.getFailureReason()))
                    : activity.getString(R.string.exportFailureMsg, result.getDestination(),
                            ExceptionHelpers.getMessageAndCause(result.getFailureReason())))
                    + "\n" + activity.getString(R.string.backup_continue), false, runBackup);
        } else
            doBackup();
    }

    @SuppressWarnings("unchecked")
    private void doBackup() {
        new AsyncBackup(activity).execute(foldersToExport);
    }

    private void showSuccessDialog(final File destZipFile) {
        // Get dialog builder & configure the dialog...
        new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme))
                //   Set title:
                .setTitle(R.string.successful_backup)
                //   Set message:
                .setMessage(activity.getString(R.string.backup_in) + "\n" + destZipFile.getAbsolutePath())
                // Set "OK" button:
                .setPositiveButton(R.string.done, null)
                // Set "Share" button:
                .setNegativeButton(R.string.share, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent sendIntent = new Intent();
                        sendIntent.setAction(Intent.ACTION_SEND);
                        sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(destZipFile));
                        sendIntent.setType("application/zip");
                        activity.startActivity(sendIntent);
                    }
                })
                // Create & show the dialog:
                .create().show();
    }

    private void showFailureDialog(Exception cause) {
        activity.showErrorDialog(
                activity.getString(R.string.backupFailDueTo, ExceptionHelpers.getMessageAndCause(cause)), false);
    }

    /**
     * 
     * @author Michalis Vitos, mstevens
     */
    private class AsyncBackup extends AsyncTaskWithWaitingDialog<BaseActivity, Set<Folder>, File> {

        private final CollectorApp app;
        private Exception failure = null;

        public AsyncBackup(BaseActivity activity) {
            super(activity);
            app = activity.getCollectorApp();
        }

        @Override
        protected File runInBackground(@SuppressWarnings("unchecked") Set<Folder>... params) {
            File destZipFile = null;
            File tmpFolder = null;
            try {
                // Phase 1: Preparation
                publishProgress(activity.getString(R.string.backup_progress_init));
                //   Create array with file paths of the selected items as well as the Temp/DB folder:
                Set<Folder> selectedFolders = params[0];
                File[] toZip = new File[selectedFolders.size() + 1]; // +1 for tmp/DB folder!
                tmpFolder = fileStorageProvider.getTempSubFolder("Backup_" + System.currentTimeMillis());
                int z = 0;
                for (Folder folder : foldersToExport)
                    toZip[z++] = getFolderFile(folder, tmpFolder); // add folders as File objects
                toZip[z] = FileHelpers.getSubDirectory(tmpFolder, Folder.DB.name(), true); // add Temp/Backup_[timestamp]/DB/ folder

                // Phase 2: Back-up database(s)
                publishProgress(activity.getString(R.string.backup_progress_db));
                //    Create backups in the Temp/Backup_[timestamp]/DB/ folder and use original file names (not labelled as backups):            
                StoreBackupper.Backup(toZip[z], false, app.getStoreHandlesForBackup());

                // Phase 3: Create ZIP archive
                publishProgress(activity.getString(R.string.backup_progress_zipping));
                destZipFile = fileStorageProvider.getNewBackupFile();
                Zipper.Zip(destZipFile, toZip);
            } catch (Exception e) {
                Debug.e(e);
                failure = e;
                return null;
            } finally { // Clean-up
                FileUtils.deleteQuietly(tmpFolder);
            }
            return destZipFile;
        }

        private File getFolderFile(Folder folder, File tmpFolder) throws IOException {
            File folderFile = fileStorageProvider.getFolder(folder, false);
            if (!folderFile.exists() || !folderFile.isDirectory() || folderFile.listFiles().length == 0) {
                // Create matching folder in tmpFolder:
                folderFile = FileHelpers.getSubDirectory(tmpFolder, folder.name(), true); // Temp/Backup_[timestamp]/[folder]/
                // Create .empty file (to make sure folder is included in ZIP):
                (new File(folderFile, EMPTY_FILE)).createNewFile();
            }
            return folderFile;
        }

        @Override
        protected void onPostExecute(File destZipFile) {
            // Dismiss progress dialog:
            super.onPostExecute(destZipFile);

            // Show success or failure dialog:
            if (destZipFile != null && failure == null)
                showSuccessDialog(destZipFile);
            else
                showFailureDialog(failure);
        }

    }

}