/**************************************************************************************** * Copyright (c) 2009 Daniel Svärd <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.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.DB; 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.AnkiPackageImporter; 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 = 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().getColSafe(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: 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 { DB db = col.getDb(); db.getDatabase().beginTransaction(); try { publishProgress(new TaskData(col.addNote(note))); db.getDatabase().setTransactionSuccessful(); } finally { db.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 { DB db = col.getDb(); db.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)); db.getDatabase().setTransactionSuccessful(); } finally { db.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(); Object[] data = params[0].getObjArray(); Card card = (Card) data[0]; Collection.DismissType type = (Collection.DismissType) data[1]; Note note = card.note(); try { col.getDb().getDatabase().beginTransaction(); try { switch (type) { case BURY_CARD: // collect undo information col.markUndo(type, new Object[] { col.getDirty(), note.cards(), card.getId() }); // then bury sched.buryCards(new long[] { card.getId() }); sHadCardQueue = true; break; case BURY_NOTE: // collect undo information col.markUndo(type, new Object[] { col.getDirty(), note.cards(), card.getId() }); // then bury sched.buryNote(note.getId()); sHadCardQueue = true; break; case SUSPEND_CARD: // collect undo information col.markUndo(type, 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 SUSPEND_NOTE: // 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(type, new Object[] { cards, card.getId() }); // suspend note sched.suspendCards(cids); sHadCardQueue = true; break; case DELETE_NOTE: // collect undo information ArrayList<Card> allCs = note.cards(); col.markUndo(type, 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); newCard.startTimer(); 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); Map<String, String> deckNames = (HashMap<String, String>) params[0].getObjArray()[0]; String query = (String) params[0].getObjArray()[1]; Boolean order = (Boolean) params[0].getObjArray()[2]; int numCardsToRender = (int) params[0].getObjArray()[3]; List<Map<String,String>> searchResult = col.findCardsForCardBrowser(query, order, deckNames); // Render the first few items for (int i = 0; i < Math.min(numCardsToRender, searchResult.size()); i++) { Card c = col.getCard(Long.parseLong(searchResult.get(i).get("id"), 10)); CardBrowser.updateSearchItemQA(searchResult.get(i), c); } // Finish off the task 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); List<Map<String, String>> items = (List<Map<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(); return new TaskData(new Object[]{counts[0], counts[1], counts[2], totalNewCount, totalCount, 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(); AnkiPackageImporter imp = new AnkiPackageImporter(col, path); imp.setProgressCallback(new ProgressCallback(this, res)); imp.run(); return new TaskData(new Object[] {imp}); } 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); } // 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); } try { Utils.unzipFiles(zip, dir.getAbsolutePath(), new String[] { "collection.anki2", "media" }, null); } catch (IOException e) { return new TaskData(-2, null, false); } if (!(new File(colFile)).exists()) { return new TaskData(-2, null, false); } Collection tmpCol = null; try { tmpCol = Storage.Collection(mContext, 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 { 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<>(); HashMap<String, String> numToName = new HashMap<>(); 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, mContext); } 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); col.save(); 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); col.save(); 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); } col.save(); 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); col.save(); } 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); } col.save(); } 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(); col.save(); 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 = col.getModels().all(); ArrayList<Integer> cardCount = new ArrayList<>(); 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)); col.save(); } 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); col.save(); } 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); col.save(); } 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)); col.save(); } 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); col.save(); } catch(Exception e){ Timber.e(e, "Error changing sort field"); return new TaskData(false); } 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 List<Map<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(List<Map<String, String>> cards) { mCards = cards; } public TaskData(List<Map<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 List<Map<String, String>> getCards() { return mCards; } public void setCards(List<Map<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; } }