com.ichi2.async.DeckTask.java Source code

Java tutorial

Introduction

Here is the source code for com.ichi2.async.DeckTask.java

Source

/****************************************************************************************
 * Copyright (c) 2009 Daniel Svrd <daniel.svard@gmail.com>                             *
 * Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com>                                   *
 * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com>                         *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 3 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

package com.ichi2.async;

import android.content.Context;
import android.content.res.Resources;
import android.os.AsyncTask;

import com.google.gson.stream.JsonReader;
import com.ichi2.anki.AnkiDb;
import com.ichi2.anki.AnkiDroidApp;
import com.ichi2.anki.BackupManager;
import com.ichi2.anki.CardBrowser;
import com.ichi2.anki.CollectionHelper;
import com.ichi2.anki.R;
import com.ichi2.anki.exception.ConfirmModSchemaException;
import com.ichi2.libanki.AnkiPackageExporter;
import com.ichi2.libanki.Card;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Note;
import com.ichi2.libanki.Sched;
import com.ichi2.libanki.Storage;
import com.ichi2.libanki.Utils;
import com.ichi2.libanki.importer.Anki2Importer;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipFile;

import timber.log.Timber;

/**
 * Loading in the background, so that AnkiDroid does not look like frozen.
 */
public class DeckTask extends BaseAsyncTask<DeckTask.TaskData, DeckTask.TaskData, DeckTask.TaskData> {

    public static final int TASK_TYPE_SAVE_COLLECTION = 2;
    public static final int TASK_TYPE_ANSWER_CARD = 3;
    public static final int TASK_TYPE_ADD_FACT = 6;
    public static final int TASK_TYPE_UPDATE_FACT = 7;
    public static final int TASK_TYPE_UNDO = 8;
    public static final int TASK_TYPE_DISMISS_NOTE = 11;
    public static final int TASK_TYPE_CHECK_DATABASE = 14;
    public static final int TASK_TYPE_REPAIR_DECK = 20;
    public static final int TASK_TYPE_LOAD_DECK_COUNTS = 22;
    public static final int TASK_TYPE_UPDATE_VALUES_FROM_DECK = 23;
    public static final int TASK_TYPE_DELETE_DECK = 25;
    public static final int TASK_TYPE_REBUILD_CRAM = 26;
    public static final int TASK_TYPE_EMPTY_CRAM = 27;
    public static final int TASK_TYPE_IMPORT = 28;
    public static final int TASK_TYPE_IMPORT_REPLACE = 29;
    public static final int TASK_TYPE_SEARCH_CARDS = 30;
    public static final int TASK_TYPE_EXPORT_APKG = 31;
    public static final int TASK_TYPE_REORDER = 32;
    public static final int TASK_TYPE_CONF_CHANGE = 33;
    public static final int TASK_TYPE_CONF_RESET = 34;
    public static final int TASK_TYPE_CONF_REMOVE = 35;
    public static final int TASK_TYPE_CONF_SET_SUBDECKS = 36;
    public static final int TASK_TYPE_RENDER_BROWSER_QA = 37;
    public static final int TASK_TYPE_CHECK_MEDIA = 38;
    public static final int TASK_TYPE_ADD_TEMPLATE = 39;
    public static final int TASK_TYPE_REMOVE_TEMPLATE = 40;
    public static final int TASK_TYPE_COUNT_MODELS = 41;
    public static final int TASK_TYPE_DELETE_MODEL = 42;
    public static final int TASK_TYPE_DELETE_FIELD = 43;
    public static final int TASK_TYPE_REPOSITION_FIELD = 44;
    public static final int TASK_TYPE_ADD_FIELD = 45;
    public static final int TASK_TYPE_CHANGE_SORT_FIELD = 46;
    public static final int TASK_TYPE_SAVE_MODEL = 47;
    public static final int TASK_TYPE_FIND_EMPTY_CARDS = 48;

    /**
     * A reference to the application context to use to fetch the current Collection object.
     */
    private Context mContext;

    /**
     * The most recently started {@link DeckTask} instance.
     */
    private static DeckTask sLatestInstance;

    private static boolean sHadCardQueue = false;

    /**
     * Starts a new {@link DeckTask}.
     * <p>
     * Tasks will be executed serially, in the order in which they are started.
     * <p>
     * This method must be called on the main thread.
     *
     * @param type of the task to start
     * @param listener to the status and result of the task
     * @param params to pass to the task
     * @return the newly created task
     */
    public static DeckTask launchDeckTask(int type, Listener listener, TaskData... params) {
        // Start new task
        DeckTask newTask = new DeckTask(type, listener, sLatestInstance);
        newTask.execute(params);
        return newTask;
    }

    /**
     * Block the current thread until the currently running DeckTask instance (if any) has finished.
     */
    public static void waitToFinish() {
        waitToFinish(null);
    }

    /**
     * Block the current thread until the currently running DeckTask instance (if any) has finished.
     * @param timeout timeout in seconds
     * @return whether or not the previous task was successful or not
     */
    public static boolean waitToFinish(Integer timeout) {
        try {
            if ((sLatestInstance != null) && (sLatestInstance.getStatus() != AsyncTask.Status.FINISHED)) {
                Timber.d("DeckTask: waiting for task %d to finish...", sLatestInstance.mType);
                if (timeout != null) {
                    sLatestInstance.get(timeout, TimeUnit.SECONDS);
                } else {
                    sLatestInstance.get();
                }

            }
            return true;
        } catch (Exception e) {
            Timber.e(e, "Exception waiting for task to finish");
            return false;
        }
    }

    public static void cancelTask() {
        //cancel the current task
        try {
            if ((sLatestInstance != null) && (sLatestInstance.getStatus() != AsyncTask.Status.FINISHED)) {
                sLatestInstance.cancel(true);
                Timber.i("Cancelled task %d", sLatestInstance.mType);
            }
        } catch (Exception e) {
            return;
        }
    }

    public static void cancelTask(int taskType) {
        // cancel the current task only if it's of type taskType
        if (sLatestInstance != null && sLatestInstance.mType == taskType) {
            cancelTask();
        }
    }

    private final int mType;
    private final Listener mListener;
    private DeckTask mPreviousTask;

    public DeckTask(int type, Listener listener, DeckTask previousTask) {
        mType = type;
        mListener = listener;
        mPreviousTask = previousTask;
    }

    // This method and those that are called here are executed in a new thread
    @Override
    protected TaskData doInBackground(TaskData... params) {
        super.doInBackground(params);
        // Wait for previous thread (if any) to finish before continuing
        if (mPreviousTask != null && mPreviousTask.getStatus() != AsyncTask.Status.FINISHED) {
            Timber.d("Waiting for %d to finish before starting %d", mPreviousTask.mType, mType);
            try {
                mPreviousTask.get();
                Timber.d("Finished waiting for %d to finish. Status= %s", mPreviousTask.mType,
                        mPreviousTask.getStatus());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                // We have been interrupted, return immediately.
                Timber.e(e, "interrupted while waiting for previous task: %d", mPreviousTask.mType);
                return null;
            } catch (ExecutionException e) {
                // Ignore failures in the previous task.
                Timber.e(e, "previously running task failed with exception: %d", mPreviousTask.mType);
            } catch (CancellationException e) {
                // Ignore cancellation of previous task
                Timber.e(e, "previously running task was cancelled: %d", mPreviousTask.mType);
            }
        }
        sLatestInstance = this;
        mContext = AnkiDroidApp.getInstance().getApplicationContext();

        // Skip the task if the collection cannot be opened
        if (mType != TASK_TYPE_REPAIR_DECK && CollectionHelper.getInstance().getCol(mContext) == null) {
            Timber.e("Aborting DeckTask %d as Collection could not be opened", mType);
            return null;
        }
        // Actually execute the task now that we are at the front of the queue.
        switch (mType) {
        case TASK_TYPE_LOAD_DECK_COUNTS:
            return doInBackgroundLoadDeckCounts(params);

        case TASK_TYPE_SAVE_COLLECTION:
            return doInBackgroundSaveCollection(params);

        case TASK_TYPE_ANSWER_CARD:
            return doInBackgroundAnswerCard(params);

        case TASK_TYPE_ADD_FACT:
            return doInBackgroundAddNote(params);

        case TASK_TYPE_UPDATE_FACT:
            return doInBackgroundUpdateNote(params);

        case TASK_TYPE_UNDO:
            return doInBackgroundUndo(params);

        case TASK_TYPE_SEARCH_CARDS:
            return doInBackgroundSearchCards(params);

        case TASK_TYPE_DISMISS_NOTE:
            return doInBackgroundDismissNote(params);

        case TASK_TYPE_CHECK_DATABASE:
            return doInBackgroundCheckDatabase(params);

        case TASK_TYPE_REPAIR_DECK:
            return doInBackgroundRepairDeck(params);

        case TASK_TYPE_UPDATE_VALUES_FROM_DECK:
            return doInBackgroundUpdateValuesFromDeck(params);

        case TASK_TYPE_DELETE_DECK:
            return doInBackgroundDeleteDeck(params);

        case TASK_TYPE_REBUILD_CRAM:
            return doInBackgroundRebuildCram(params);

        case TASK_TYPE_EMPTY_CRAM:
            return doInBackgroundEmptyCram(params);

        case TASK_TYPE_IMPORT:
            return doInBackgroundImportAdd(params);

        case TASK_TYPE_IMPORT_REPLACE:
            return doInBackgroundImportReplace(params);

        case TASK_TYPE_EXPORT_APKG:
            return doInBackgroundExportApkg(params);

        case TASK_TYPE_REORDER:
            return doInBackgroundReorder(params);

        case TASK_TYPE_CONF_CHANGE:
            return doInBackgroundConfChange(params);

        case TASK_TYPE_CONF_RESET:
            return doInBackgroundConfReset(params);

        case TASK_TYPE_CONF_REMOVE:
            return doInBackgroundConfRemove(params);

        case TASK_TYPE_CONF_SET_SUBDECKS:
            return doInBackgroundConfSetSubdecks(params);

        case TASK_TYPE_RENDER_BROWSER_QA:
            return doInBackgroundRenderBrowserQA(params);

        case TASK_TYPE_CHECK_MEDIA:
            return doInBackgroundCheckMedia(params);

        case TASK_TYPE_ADD_TEMPLATE:
            return doInBackgroundAddTemplate(params);

        case TASK_TYPE_REMOVE_TEMPLATE:
            return doInBackgroundRemoveTemplate(params);

        case TASK_TYPE_COUNT_MODELS:
            return doInBackgroundCountModels(params);

        case TASK_TYPE_DELETE_MODEL:
            return doInBackGroundDeleteModel(params);

        case TASK_TYPE_DELETE_FIELD:
            return doInBackGroundDeleteField(params);

        case TASK_TYPE_REPOSITION_FIELD:
            return doInBackGroundRepositionField(params);

        case TASK_TYPE_ADD_FIELD:
            return doInBackGroundAddField(params);

        case TASK_TYPE_CHANGE_SORT_FIELD:
            return doInBackgroundChangeSortField(params);

        case TASK_TYPE_SAVE_MODEL:
            return doInBackgroundSaveModel(params);
        case TASK_TYPE_FIND_EMPTY_CARDS:
            return doInBackGroundFindEmptyCards(params);

        default:
            Timber.e("unknown task type: %d", mType);
            return null;
        }
    }

    /** Delegates to the {@link TaskListener} for this task. */
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mListener.onPreExecute(this);
    }

    /** Delegates to the {@link TaskListener} for this task. */
    @Override
    protected void onProgressUpdate(TaskData... values) {
        super.onProgressUpdate(values);
        mListener.onProgressUpdate(this, values);
    }

    /** Delegates to the {@link TaskListener} for this task. */
    @Override
    protected void onPostExecute(TaskData result) {
        super.onPostExecute(result);
        mListener.onPostExecute(this, result);
        Timber.d("enabling garbage collection of mPreviousTask...");
        mPreviousTask = null;
    }

    @Override
    protected void onCancelled() {
        mListener.onCancelled();
    }

    private TaskData doInBackgroundAddNote(TaskData[] params) {
        Timber.d("doInBackgroundAddNote");
        Note note = params[0].getNote();
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            AnkiDb ankiDB = col.getDb();
            ankiDB.getDatabase().beginTransaction();
            try {
                publishProgress(new TaskData(col.addNote(note)));
                ankiDB.getDatabase().setTransactionSuccessful();
            } finally {
                ankiDB.getDatabase().endTransaction();
            }
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundAddNote - RuntimeException on adding fact");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundAddNote");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    private TaskData doInBackgroundUpdateNote(TaskData[] params) {
        Timber.d("doInBackgroundUpdateNote");
        // Save the note
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Sched sched = col.getSched();
        Card editCard = params[0].getCard();
        Note editNote = editCard.note();
        boolean fromReviewer = params[0].getBoolean();

        try {
            col.getDb().getDatabase().beginTransaction();
            try {
                // TODO: undo integration
                editNote.flush();
                // flush card too, in case, did has been changed
                editCard.flush();
                if (fromReviewer) {
                    Card newCard;
                    if (col.getDecks().active().contains(editCard.getDid())) {
                        newCard = editCard;
                        newCard.load();
                        // reload qa-cache
                        newCard.q(true);
                    } else {
                        newCard = getCard(sched);
                    }
                    publishProgress(new TaskData(newCard));
                } else {
                    publishProgress(new TaskData(editCard, editNote.stringTags()));
                }
                col.getDb().getDatabase().setTransactionSuccessful();
            } finally {
                col.getDb().getDatabase().endTransaction();
            }
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundUpdateNote - RuntimeException on updating fact");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundUpdateNote");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    private TaskData doInBackgroundAnswerCard(TaskData... params) {
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Sched sched = col.getSched();
        Card oldCard = params[0].getCard();
        int ease = params[0].getInt();
        Card newCard = null;
        try {
            AnkiDb ankiDB = col.getDb();
            ankiDB.getDatabase().beginTransaction();
            try {
                if (oldCard != null) {
                    sched.answerCard(oldCard, ease);
                }
                if (newCard == null) {
                    newCard = getCard(sched);
                }
                if (newCard != null) {
                    // render cards before locking database
                    newCard._getQA(true);
                }
                publishProgress(new TaskData(newCard));
                ankiDB.getDatabase().setTransactionSuccessful();
            } finally {
                ankiDB.getDatabase().endTransaction();
            }
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundAnswerCard - RuntimeException on answering card");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundAnswerCard");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    private Card getCard(Sched sched) {
        if (sHadCardQueue) {
            sched.reset();
            sHadCardQueue = false;
        }
        return sched.getCard();
    }

    private TaskData doInBackgroundLoadDeckCounts(TaskData... params) {
        Timber.d("doInBackgroundLoadDeckCounts");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            // Get due tree
            Object[] o = new Object[] { col.getSched().deckDueTree() };
            return new TaskData(o);
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundLoadDeckCounts - error");
            return null;
        }
    }

    private TaskData doInBackgroundSaveCollection(TaskData... params) {
        Timber.d("doInBackgroundSaveCollection");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        if (col != null) {
            try {
                col.save();
            } catch (RuntimeException e) {
                Timber.e(e, "Error on saving deck in background");
            }
        }
        return null;
    }

    private TaskData doInBackgroundDismissNote(TaskData... params) {
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Sched sched = col.getSched();
        Card card = params[0].getCard();
        Note note = card.note();
        int type = params[0].getInt();
        try {
            col.getDb().getDatabase().beginTransaction();
            try {
                switch (type) {
                case 4:
                    // collect undo information
                    col.markUndo(Collection.UNDO_BURY_CARD,
                            new Object[] { col.getDirty(), note.cards(), card.getId() });
                    // then bury
                    sched.buryCards(new long[] { card.getId() });
                    sHadCardQueue = true;
                    break;
                case 0:
                    // collect undo information
                    col.markUndo(Collection.UNDO_BURY_NOTE,
                            new Object[] { col.getDirty(), note.cards(), card.getId() });
                    // then bury
                    sched.buryNote(note.getId());
                    sHadCardQueue = true;
                    break;
                case 1:
                    // collect undo information
                    col.markUndo(Collection.UNDO_SUSPEND_CARD, new Object[] { card });
                    // suspend card
                    if (card.getQueue() == -1) {
                        sched.unsuspendCards(new long[] { card.getId() });
                    } else {
                        sched.suspendCards(new long[] { card.getId() });
                    }
                    sHadCardQueue = true;
                    break;
                case 2:
                    // collect undo information
                    ArrayList<Card> cards = note.cards();
                    long[] cids = new long[cards.size()];
                    for (int i = 0; i < cards.size(); i++) {
                        cids[i] = cards.get(i).getId();
                    }
                    col.markUndo(Collection.UNDO_SUSPEND_NOTE, new Object[] { cards, card.getId() });
                    // suspend note
                    sched.suspendCards(cids);
                    sHadCardQueue = true;
                    break;
                case 3:
                    // collect undo information
                    ArrayList<Card> allCs = note.cards();
                    col.markUndo(Collection.UNDO_DELETE_NOTE, new Object[] { note, allCs, card.getId() });
                    // delete note
                    col.remNotes(new long[] { note.getId() });
                    sHadCardQueue = true;
                    break;
                }
                publishProgress(new TaskData(getCard(col.getSched()), 0));
                col.getDb().getDatabase().setTransactionSuccessful();
            } finally {
                col.getDb().getDatabase().endTransaction();
            }
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundSuspendCard - RuntimeException on suspending card");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundSuspendCard");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    private TaskData doInBackgroundUndo(TaskData... params) {
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Sched sched = col.getSched();
        try {
            col.getDb().getDatabase().beginTransaction();
            Card newCard;
            try {
                long cid = col.undo();
                if (cid != 0) {
                    // a review was undone,
                    newCard = col.getCard(cid);
                    col.reset();
                    col.getSched().decrementCounts(newCard);
                    sHadCardQueue = true;
                } else {
                    // TODO: do not fetch new card if a non review operation has
                    // been undone
                    col.reset();
                    newCard = getCard(sched);
                }
                // TODO: handle leech undoing properly
                publishProgress(new TaskData(newCard, 0));
                col.getDb().getDatabase().setTransactionSuccessful();
            } finally {
                col.getDb().getDatabase().endTransaction();
            }
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundUndo - RuntimeException on undoing");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundUndo");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    private TaskData doInBackgroundSearchCards(TaskData... params) {
        Timber.d("doInBackgroundSearchCards");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        HashMap<String, String> deckNames = (HashMap<String, String>) params[0].getObjArray()[0];
        String query = (String) params[0].getObjArray()[1];
        Boolean order = (Boolean) params[0].getObjArray()[2];
        ArrayList<HashMap<String, String>> searchResult = col.findCardsForCardBrowser(query, order, deckNames);
        if (isCancelled()) {
            Timber.d("doInBackgroundSearchCards was cancelled so return null");
            return null;
        } else {
            publishProgress(new TaskData(searchResult));
        }
        return new TaskData(col.cardCount(col.getDecks().allIds()));
    }

    private TaskData doInBackgroundRenderBrowserQA(TaskData... params) {
        Timber.d("doInBackgroundRenderBrowserQA");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        ArrayList<HashMap<String, String>> items = (ArrayList<HashMap<String, String>>) params[0].getObjArray()[0];
        Integer startPos = (Integer) params[0].getObjArray()[1];
        Integer n = (Integer) params[0].getObjArray()[2];

        // for each specified card in the browser list
        for (int i = startPos; i < startPos + n; i++) {
            if (i >= 0 && i < items.size() && items.get(i).get("answer").equals("")) {
                // Extract card item
                Card c = col.getCard(Long.parseLong(items.get(i).get("id"), 10));
                // Update item
                CardBrowser.updateSearchItemQA(items.get(i), c);
                // Stop if cancelled
                if (isCancelled()) {
                    Timber.d("doInBackgroundRenderBrowserQA was aborted");
                    return null;
                } else {
                    float progress = (float) i / n * 100;
                    publishProgress(new TaskData((int) progress));
                }
            }
        }
        return new TaskData(items);
    }

    private TaskData doInBackgroundCheckDatabase(TaskData... params) {
        Timber.d("doInBackgroundCheckDatabase");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        // Don't proceed if collection closed
        if (col == null) {
            Timber.e("doInBackgroundCheckDatabase :: supplied collection was null");
            return new TaskData(false);
        }

        long result = col.fixIntegrity();
        if (result == -1) {
            return new TaskData(false);
        } else {
            // Close the collection and we restart the app to reload
            CollectionHelper.getInstance().closeCollection(true);
            return new TaskData(0, result, true);
        }
    }

    private TaskData doInBackgroundRepairDeck(TaskData... params) {
        Timber.d("doInBackgroundRepairDeck");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        if (col != null) {
            col.close(false);
        }
        return new TaskData(BackupManager.repairCollection(col));
    }

    private TaskData doInBackgroundUpdateValuesFromDeck(TaskData... params) {
        Timber.d("doInBackgroundUpdateValuesFromDeck");
        try {
            Collection col = CollectionHelper.getInstance().getCol(mContext);
            Sched sched = col.getSched();
            Object[] obj = params[0].getObjArray();
            boolean reset = (Boolean) obj[0];
            if (reset) {
                sched.reset();
            }
            int[] counts = sched.counts();
            int totalNewCount = sched.totalNewForCurrentDeck();
            int totalCount = sched.cardCount();
            double progressMature = ((double) sched.matureCount()) / ((double) totalCount);
            double progressAll = 1 - (((double) (totalNewCount + counts[1])) / ((double) totalCount));
            return new TaskData(new Object[] { counts[0], counts[1], counts[2], totalNewCount, totalCount,
                    progressMature, progressAll, sched.eta(counts) });
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundUpdateValuesFromDeck - an error occurred");
            return null;
        }
    }

    private TaskData doInBackgroundDeleteDeck(TaskData... params) {
        Timber.d("doInBackgroundDeleteDeck");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        long did = params[0].getLong();
        col.getDecks().rem(did, true);
        return new TaskData(true);
    }

    private TaskData doInBackgroundRebuildCram(TaskData... params) {
        Timber.d("doInBackgroundRebuildCram");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        col.getSched().rebuildDyn(col.getDecks().selected());
        return doInBackgroundUpdateValuesFromDeck(new DeckTask.TaskData(new Object[] { true }));
    }

    private TaskData doInBackgroundEmptyCram(TaskData... params) {
        Timber.d("doInBackgroundEmptyCram");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        col.getSched().emptyDyn(col.getDecks().selected());
        return doInBackgroundUpdateValuesFromDeck(new DeckTask.TaskData(new Object[] { true }));
    }

    private TaskData doInBackgroundImportAdd(TaskData... params) {
        Timber.d("doInBackgroundImportAdd");
        Resources res = AnkiDroidApp.getInstance().getBaseContext().getResources();
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        String path = params[0].getString();
        boolean sharedDeckImport = params[0].getBoolean();

        ProgressCallback pc = null;
        // don't report progress on shared deck import (or maybe should we?)
        if (!sharedDeckImport) {
            pc = new ProgressCallback(this, res);
        }

        int addedCount = -1;
        try {
            Anki2Importer imp = new Anki2Importer(col, path, pc);
            AnkiDb ankiDB = col.getDb();
            ankiDB.getDatabase().beginTransaction();
            try {
                addedCount = imp.run();
                // Rollback db transaction if an error occured (TODO: doesn't appear to be working)
                if (addedCount >= 0) {
                    ankiDB.getDatabase().setTransactionSuccessful();
                }
            } finally {
                ankiDB.getDatabase().endTransaction();
                if (sharedDeckImport) {
                    File tmpFile = new File(path);
                    tmpFile.delete();
                }
            }
            if (addedCount >= 0) {
                ankiDB.execute("VACUUM");
                ankiDB.execute("ANALYZE");
            }

            publishProgress(new TaskData(res.getString(R.string.import_update_counts)));
            return new TaskData(addedCount, null, true);
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundImportAdd - RuntimeException on importing cards");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportAdd");
            return new TaskData(false);
        } catch (IOException e) {
            Timber.e(e, "doInBackgroundImportAdd - IOException on importing cards: ");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportAdd");
            return new TaskData(false);
        }
    }

    private TaskData doInBackgroundImportReplace(TaskData... params) {
        Timber.d("doInBackgroundImportReplace");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        String path = params[0].getString();
        Resources res = AnkiDroidApp.getInstance().getBaseContext().getResources();

        // extract the deck from the zip file
        String colPath = col.getPath();
        File dir = new File(new File(colPath).getParentFile(), "tmpzip");
        if (dir.exists()) {
            BackupManager.removeDir(dir);
        }

        publishProgress(new TaskData(res.getString(R.string.import_unpacking)));
        // from anki2.py
        String colFile = new File(dir, "collection.anki2").getAbsolutePath();
        ZipFile zip;
        try {
            zip = new ZipFile(new File(path), ZipFile.OPEN_READ);
        } catch (IOException e) {
            Timber.e(e, "doInBackgroundImportReplace - Error while unzipping");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportReplace0");
            return new TaskData(false);
        }
        if (!Utils.unzipFiles(zip, dir.getAbsolutePath(), new String[] { "collection.anki2", "media" }, null)
                || !(new File(colFile)).exists()) {
            return new TaskData(-2, null, false);
        }

        Collection tmpCol = null;
        try {
            tmpCol = Storage.Collection(colFile);
            if (!tmpCol.validCollection()) {
                tmpCol.close();
                return new TaskData(-2, null, false);
            }
        } catch (Exception e) {
            Timber.e("Error opening new collection file... probably it's invalid");
            try {
                tmpCol.close();
            } catch (Exception e2) {
                // do nothing
            }
            return new TaskData(-2, null, false);
        } finally {
            if (tmpCol != null) {
                tmpCol.close();
            }
        }

        publishProgress(new TaskData(res.getString(R.string.importing_collection)));
        if (col != null) {
            // unload collection and trigger a backup
            CollectionHelper.getInstance().closeCollection(true);
            CollectionHelper.getInstance().lockCollection();
            BackupManager.performBackupInBackground(colPath, true);
        }
        // overwrite collection
        File f = new File(colFile);
        if (!f.renameTo(new File(colPath))) {
            // Exit early if this didn't work
            return new TaskData(-2, null, false);
        }
        int addedCount = -1;
        try {
            col = CollectionHelper.getInstance().reopenCollection();
            CollectionHelper.getInstance().unlockCollection();

            // because users don't have a backup of media, it's safer to import new
            // data and rely on them running a media db check to get rid of any
            // unwanted media. in the future we might also want to duplicate this step
            // import media
            HashMap<String, String> nameToNum = new HashMap<String, String>();
            HashMap<String, String> numToName = new HashMap<String, String>();
            File mediaMapFile = new File(dir.getAbsolutePath(), "media");
            if (mediaMapFile.exists()) {
                JsonReader jr = new JsonReader(new FileReader(mediaMapFile));
                jr.beginObject();
                String name;
                String num;
                while (jr.hasNext()) {
                    num = jr.nextName();
                    name = jr.nextString();
                    nameToNum.put(name, num);
                    numToName.put(num, name);
                }
                jr.endObject();
                jr.close();
            }
            String mediaDir = col.getMedia().dir();
            int total = nameToNum.size();
            int i = 0;
            for (Map.Entry<String, String> entry : nameToNum.entrySet()) {
                String file = entry.getKey();
                String c = entry.getValue();
                File of = new File(mediaDir, file);
                if (!of.exists()) {
                    Utils.unzipFiles(zip, mediaDir, new String[] { c }, numToName);
                }
                ++i;
                publishProgress(new TaskData(res.getString(R.string.import_media_count, (i + 1) * 100 / total)));
            }
            zip.close();
            // delete tmp dir
            BackupManager.removeDir(dir);
            return new TaskData(true);
        } catch (RuntimeException e) {
            Timber.e(e, "doInBackgroundImportReplace - RuntimeException");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportReplace1");
            return new TaskData(false);
        } catch (FileNotFoundException e) {
            Timber.e(e, "doInBackgroundImportReplace - FileNotFoundException");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportReplace2");
            return new TaskData(false);
        } catch (IOException e) {
            Timber.e(e, "doInBackgroundImportReplace - IOException");
            AnkiDroidApp.sendExceptionReport(e, "doInBackgroundImportReplace3");
            return new TaskData(false);
        }
    }

    private TaskData doInBackgroundExportApkg(TaskData... params) {
        Timber.d("doInBackgroundExportApkg");
        Object[] data = params[0].getObjArray();
        Collection col = (Collection) data[0];
        String apkgPath = (String) data[1];
        Long did = (Long) data[2];
        boolean includeSched = (Boolean) data[3];
        boolean includeMedia = (Boolean) data[4];

        try {
            AnkiPackageExporter exporter = new AnkiPackageExporter(col);
            exporter.setIncludeSched(includeSched);
            exporter.setIncludeMedia(includeMedia);
            exporter.setDid(did);
            exporter.exportInto(apkgPath);
        } catch (FileNotFoundException e) {
            Timber.e(e, "FileNotFoundException in doInBackgroundExportApkg");
            return new TaskData(false);
        } catch (IOException e) {
            Timber.e(e, "IOException in doInBackgroundExportApkg");
            return new TaskData(false);
        } catch (JSONException e) {
            Timber.e(e, "JSOnException in doInBackgroundExportApkg");
            return new TaskData(false);
        }
        return new TaskData(apkgPath);
    }

    private TaskData doInBackgroundReorder(TaskData... params) {
        Timber.d("doInBackgroundReorder");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] data = params[0].getObjArray();
        JSONObject conf = (JSONObject) data[0];
        col.getSched().resortConf(conf);
        return new TaskData(true);
    }

    private TaskData doInBackgroundConfChange(TaskData... params) {
        Timber.d("doInBackgroundConfChange");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] data = params[0].getObjArray();
        JSONObject deck = (JSONObject) data[0];
        JSONObject conf = (JSONObject) data[1];
        try {
            long newConfId = conf.getLong("id");
            // If new config has a different sorting order, reorder the cards
            int oldOrder = col.getDecks().getConf(deck.getLong("conf")).getJSONObject("new").getInt("order");
            int newOrder = col.getDecks().getConf(newConfId).getJSONObject("new").getInt("order");
            if (oldOrder != newOrder) {
                switch (newOrder) {
                case 0:
                    col.getSched().randomizeCards(deck.getLong("id"));
                    break;
                case 1:
                    col.getSched().orderCards(deck.getLong("id"));
                    break;
                }
            }
            col.getDecks().setConf(deck, newConfId);
            return new TaskData(true);
        } catch (JSONException e) {
            return new TaskData(false);
        }
    }

    private TaskData doInBackgroundConfReset(TaskData... params) {
        Timber.d("doInBackgroundConfReset");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] data = params[0].getObjArray();
        JSONObject conf = (JSONObject) data[0];
        col.getDecks().restoreToDefault(conf);
        return new TaskData(true);
    }

    private TaskData doInBackgroundConfRemove(TaskData... params) {
        Timber.d("doInBackgroundConfRemove");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] data = params[0].getObjArray();
        JSONObject conf = (JSONObject) data[0];
        try {
            // Note: We do the actual removing of the options group in the main thread so that we 
            // can ask the user to confirm if they're happy to do a full sync, and just do the resorting here

            // When a conf is deleted, all decks using it revert to the default conf.
            // Cards must be reordered according to the default conf.
            int order = conf.getJSONObject("new").getInt("order");
            int defaultOrder = col.getDecks().getConf(1).getJSONObject("new").getInt("order");
            if (order != defaultOrder) {
                conf.getJSONObject("new").put("order", defaultOrder);
                col.getSched().resortConf(conf);
            }
            return new TaskData(true);
        } catch (JSONException e) {
            return new TaskData(false);
        }
    }

    private TaskData doInBackgroundConfSetSubdecks(TaskData... params) {
        Timber.d("doInBackgroundConfSetSubdecks");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] data = params[0].getObjArray();
        JSONObject deck = (JSONObject) data[0];
        JSONObject conf = (JSONObject) data[1];
        try {
            TreeMap<String, Long> children = col.getDecks().children(deck.getLong("id"));
            for (Map.Entry<String, Long> entry : children.entrySet()) {
                JSONObject child = col.getDecks().get(entry.getValue());
                if (child.getInt("dyn") == 1) {
                    continue;
                }
                TaskData newParams = new TaskData(new Object[] { child, conf });
                boolean changed = doInBackgroundConfChange(newParams).getBoolean();
                if (!changed) {
                    return new TaskData(false);
                }
            }
            return new TaskData(true);
        } catch (JSONException e) {
            return new TaskData(false);
        }
    }

    /**
     * @return The results list from the check, or false if any errors.
     */
    private TaskData doInBackgroundCheckMedia(TaskData... params) {
        Timber.d("doInBackgroundCheckMedia");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        // A media check on AnkiDroid will also update the media db
        col.getMedia().findChanges(true);
        // Then do the actual check
        List<List<String>> result = col.getMedia().check();
        return new TaskData(0, new Object[] { result }, true);
    }

    /**
     * Add a new card template
     */
    private TaskData doInBackgroundAddTemplate(TaskData... params) {
        Timber.d("doInBackgroundAddTemplate");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] args = params[0].getObjArray();
        JSONObject model = (JSONObject) args[0];
        JSONObject template = (JSONObject) args[1];
        // add the new template
        try {
            col.getModels().addTemplate(model, template);
        } catch (ConfirmModSchemaException e) {
            Timber.e("doInBackgroundAddTemplate :: ConfirmModSchemaException");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Remove a card template. Note: it's necessary to call save model after this to re-generate the cards
     */
    private TaskData doInBackgroundRemoveTemplate(TaskData... params) {
        Timber.d("doInBackgroundRemoveTemplate");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] args = params[0].getObjArray();
        JSONObject model = (JSONObject) args[0];
        JSONObject template = (JSONObject) args[1];
        try {
            boolean success = col.getModels().remTemplate(model, template);
            if (!success) {
                return new TaskData("removeTemplateFailed", false);
            }
        } catch (ConfirmModSchemaException e) {
            Timber.e("doInBackgroundRemoveTemplate :: ConfirmModSchemaException");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Regenerate all the cards in a model
     */
    private TaskData doInBackgroundSaveModel(TaskData... params) {
        Timber.d("doInBackgroundSaveModel");
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        Object[] args = params[0].getObjArray();
        JSONObject model = (JSONObject) args[0];
        col.getModels().save(model, true);
        col.reset();
        return new TaskData(true);
    }

    /*
     * Async task for the ModelBrowser Class
     * Returns an ArrayList of all models alphabetically ordered and the number of notes
     * associated with each model.
     *
     * @return {ArrayList<JSONObject> models, ArrayList<Integer> cardCount}
     */
    private TaskData doInBackgroundCountModels(TaskData... params) {
        Timber.d("doInBackgroundLoadModels");
        Collection col = CollectionHelper.getInstance().getCol(mContext);

        ArrayList<JSONObject> models = (ArrayList<JSONObject>) col.getModels().all();
        ArrayList<Integer> cardCount = new ArrayList<Integer>();
        Collections.sort(models, new Comparator<JSONObject>() {
            @Override
            public int compare(JSONObject a, JSONObject b) {
                try {
                    return a.getString("name").compareTo(b.getString("name"));
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        try {
            for (JSONObject n : models) {
                long modID = n.getLong("id");
                cardCount.add(col.getModels().nids(col.getModels().get(modID)).size());
            }
        } catch (JSONException e) {
            Timber.e("doInBackgroundLoadModels :: JSONException");
            return new TaskData(false);
        }

        Object[] data = new Object[2];
        data[0] = models;
        data[1] = cardCount;
        return (new TaskData(0, data, true));
    }

    /**
     * Deletes the given model (stored in the long field of TaskData)
     * and all notes associated with it
     */
    private TaskData doInBackGroundDeleteModel(TaskData... params) {
        Timber.d("doInBackGroundDeleteModel");
        long modID = params[0].getLong();
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            col.getModels().rem(col.getModels().get(modID));
        } catch (ConfirmModSchemaException e) {
            Timber.e("doInBackGroundDeleteModel :: ConfirmModSchemaException");
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Deletes thje given field in the given model
     */
    private TaskData doInBackGroundDeleteField(TaskData... params) {
        Timber.d("doInBackGroundDeleteField");
        Object[] objects = params[0].getObjArray();

        JSONObject model = (JSONObject) objects[0];
        JSONObject field = (JSONObject) objects[1];

        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            col.getModels().remField(model, field);
        } catch (ConfirmModSchemaException e) {
            //Should never be reached
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Repositions the given field in the given model
     */
    private TaskData doInBackGroundRepositionField(TaskData... params) {
        Timber.d("doInBackgroundRepositionField");
        Object[] objects = params[0].getObjArray();

        JSONObject model = (JSONObject) objects[0];
        JSONObject field = (JSONObject) objects[1];
        int index = (Integer) objects[2];

        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            col.getModels().moveField(model, field, index);
        } catch (ConfirmModSchemaException e) {
            //Should never be reached
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Adds a field of with name in given model
     */
    private TaskData doInBackGroundAddField(TaskData... params) {
        Timber.d("doInBackgroundRepositionField");
        Object[] objects = params[0].getObjArray();

        JSONObject model = (JSONObject) objects[0];
        String fieldName = (String) objects[1];

        Collection col = CollectionHelper.getInstance().getCol(mContext);
        try {
            col.getModels().addField(model, col.getModels().newField(fieldName));
        } catch (ConfirmModSchemaException e) {
            //Should never be reached
            return new TaskData(false);
        }
        return new TaskData(true);
    }

    /**
     * Adds a field of with name in given model
     */
    private TaskData doInBackgroundChangeSortField(TaskData... params) {
        try {
            Timber.d("doInBackgroundChangeSortField");
            Object[] objects = params[0].getObjArray();

            JSONObject model = (JSONObject) objects[0];
            int idx = (int) objects[1];

            Collection col = CollectionHelper.getInstance().getCol(mContext);
            col.getModels().setSortIdx(model, idx);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new TaskData(true);
    }

    public TaskData doInBackGroundFindEmptyCards(TaskData... params) {
        Collection col = CollectionHelper.getInstance().getCol(mContext);
        List<Long> cids = col.emptyCids();
        return new TaskData(new Object[] { cids });
    }

    /**
     * Listener for the status and result of a {@link DeckTask}.
     * <p>
     * Its methods are guaranteed to be invoked on the main thread.
     * <p>
     * Their semantics is equivalent to the methods of {@link AsyncTask}.
     */
    public interface Listener {

        /** Invoked before the task is started. */
        void onPreExecute(DeckTask task);

        /**
         * Invoked after the task has completed.
         * <p>
         * The semantics of the result depends on the task itself.
         */
        void onPostExecute(DeckTask task, TaskData result);

        /**
         * Invoked when the background task publishes an update.
         * <p>
         * The semantics of the update data depends on the task itself.
         */
        void onProgressUpdate(DeckTask task, TaskData... values);

        /**
         * Invoked when the background task is cancelled.
         */
        void onCancelled();

    }

    /**
     * Adapter for the old interface, where the DeckTask itself was not passed to the listener.
     * <p>
     * All methods are invoked on the main thread.
     * <p>
     * The semantics of the methods is equivalent to the semantics of the methods in the regular {@link Listener}.
     */
    public static abstract class TaskListener implements Listener {

        /** Invoked before the task is started. */
        public abstract void onPreExecute();

        /**
         * Invoked after the task has completed.
         * <p>
         * The semantics of the result depends on the task itself.
         */
        public abstract void onPostExecute(TaskData result);

        /**
         * Invoked when the background task publishes an update.
         * <p>
         * The semantics of the update data depends on the task itself.
         */
        public abstract void onProgressUpdate(TaskData... values);

        @Override
        public void onPreExecute(DeckTask task) {
            onPreExecute();
        }

        @Override
        public void onPostExecute(DeckTask task, TaskData result) {
            onPostExecute(result);
        }

        @Override
        public void onProgressUpdate(DeckTask task, TaskData... values) {
            onProgressUpdate(values);
        }

    }

    /**
     * Helper class for allowing inner function to publish progress of an AsyncTask.
     */
    public class ProgressCallback {
        private Resources res;
        private DeckTask task;

        public ProgressCallback(DeckTask task, Resources res) {
            this.res = res;
            if (res != null) {
                this.task = task;
            } else {
                this.task = null;
            }
        }

        public Resources getResources() {
            return res;
        }

        public void publishProgress(TaskData values) {
            if (task != null) {
                task.doProgress(values);
            }
        }
    }

    public void doProgress(TaskData values) {
        publishProgress(values);
    }

    public static class TaskData {
        private Card mCard;
        private Note mNote;
        private int mInteger;
        private String mMsg;
        private boolean mBool = false;
        private ArrayList<HashMap<String, String>> mCards;
        private long mLong;
        private Context mContext;
        private int mType;
        private Comparator mComparator;
        private Object[] mObjects;

        public TaskData(Object[] obj) {
            mObjects = obj;
        }

        public TaskData(int value, Object[] obj, boolean bool) {
            mObjects = obj;
            mInteger = value;
            mBool = bool;
        }

        public TaskData(int value, Card card) {
            this(value);
            mCard = card;
        }

        public TaskData(int value, long cardId, boolean bool) {
            this(value);
            mLong = cardId;
            mBool = bool;
        }

        public TaskData(Card card) {
            mCard = card;
        }

        public TaskData(Card card, String tags) {
            mCard = card;
            mMsg = tags;
        }

        public TaskData(Card card, int integer) {
            mCard = card;
            mInteger = integer;
        }

        public TaskData(Context context, int type, int period) {
            mContext = context;
            mType = type;
            mInteger = period;
        }

        public TaskData(ArrayList<HashMap<String, String>> cards) {
            mCards = cards;
        }

        public TaskData(ArrayList<HashMap<String, String>> cards, Comparator comparator) {
            mCards = cards;
            mComparator = comparator;
        }

        public TaskData(boolean bool) {
            mBool = bool;
        }

        public TaskData(String string, boolean bool) {
            mMsg = string;
            mBool = bool;
        }

        public TaskData(long value, boolean bool) {
            mLong = value;
            mBool = bool;
        }

        public TaskData(int value, boolean bool) {
            mInteger = value;
            mBool = bool;
        }

        public TaskData(Card card, boolean bool) {
            mBool = bool;
            mCard = card;
        }

        public TaskData(int value) {
            mInteger = value;
        }

        public TaskData(long l) {
            mLong = l;
        }

        public TaskData(String msg) {
            mMsg = msg;
        }

        public TaskData(Note note) {
            mNote = note;
        }

        public TaskData(int value, String msg) {
            mMsg = msg;
            mInteger = value;
        }

        public TaskData(String msg, long cardId, boolean bool) {
            mMsg = msg;
            mLong = cardId;
            mBool = bool;
        }

        public ArrayList<HashMap<String, String>> getCards() {
            return mCards;
        }

        public void setCards(ArrayList<HashMap<String, String>> cards) {
            mCards = cards;
        }

        public Comparator getComparator() {
            return mComparator;
        }

        public Card getCard() {
            return mCard;
        }

        public Note getNote() {
            return mNote;
        }

        public long getLong() {
            return mLong;
        }

        public int getInt() {
            return mInteger;
        }

        public String getString() {
            return mMsg;
        }

        public boolean getBoolean() {
            return mBool;
        }

        public Context getContext() {
            return mContext;
        }

        public int getType() {
            return mType;
        }

        public Object[] getObjArray() {
            return mObjects;
        }
    }

    public static synchronized DeckTask getInstance() {
        return sLatestInstance;
    }
}