Java tutorial
/*************************************************************************************** * * * Copyright (c) 2015 Frank Oltmanns <frank.oltmanns@gmail.com> * * Copyright (c) 2015 Timothy Rae <timothy.rae@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.anki.provider; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.database.SQLException; import android.net.Uri; import com.ichi2.anki.AnkiDb; import com.ichi2.anki.AnkiDroidApp; import com.ichi2.anki.CollectionHelper; import com.ichi2.anki.FlashCardsContract; import com.ichi2.anki.FlashCardsContract.CardTemplate; import com.ichi2.anki.exception.ConfirmModSchemaException; import com.ichi2.libanki.Card; import com.ichi2.libanki.Collection; import com.ichi2.libanki.Models; import com.ichi2.libanki.Note; import com.ichi2.libanki.Sched; import com.ichi2.libanki.Utils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import timber.log.Timber; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Supported URIs: * .../notes (search for notes) * .../notes/# (direct access to note) * .../notes/#/cards (access cards of note) * .../notes/#/cards/# (access specific card of note) * .../models (search for models) * .../models/# (direct access to model). String id 'current' can be used in place of # for the current model * .../models/#/fields (access to field definitions of a model) * .../models/#/templates (access to card templates of a model) * .../schedule (access the study schedule) * .../decks (access the deck list) * .../decks/# (access the specified deck) * .../selected_deck (access the currently selected deck) * <p/> * Note that unlike Android's contact providers: * <ul> * <li>it's not possible to access cards of more than one note at a time</li> * <li>it's not possible to access cards of a note without providing the note's ID</li> * </ul> */ public class CardContentProvider extends ContentProvider { /* URI types */ private static final int NOTES = 1000; private static final int NOTES_ID = 1001; private static final int NOTES_ID_CARDS = 1003; private static final int NOTES_ID_CARDS_ORD = 1004; private static final int MODELS = 2000; private static final int MODELS_ID = 2001; private static final int MODELS_ID_TEMPLATES = 2003; private static final int MODELS_ID_TEMPLATES_ID = 2004; private static final int SCHEDULE = 3000; private static final int DECKS = 4000; private static final int DECK_SELECTED = 4001; private static final int DECKS_ID = 4002; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { // Here you can see all the URIs at a glance sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "notes", NOTES); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "notes/#", NOTES_ID); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "notes/#/cards", NOTES_ID_CARDS); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "notes/#/cards/#", NOTES_ID_CARDS_ORD); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models", MODELS); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*", MODELS_ID); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/templates", MODELS_ID_TEMPLATES); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/templates/#", MODELS_ID_TEMPLATES_ID); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "schedule/", SCHEDULE); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "decks/", DECKS); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "decks/#", DECKS_ID); sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "selected_deck/", DECK_SELECTED); } /** * The names of the columns returned by this content provider differ slightly from the names * given of the database columns. This list is used to convert the column names used in a * projection by the user into DB column names. * <p/> * This is currently only "_id" (projection) vs. "id" (Anki DB). But should probably be * applied to more columns. "MID", "USN", "MOD" are not really user friendly. */ private static final String[] sDefaultNoteProjectionDBAccess = FlashCardsContract.Note.DEFAULT_PROJECTION .clone(); static { for (int idx = 0; idx < sDefaultNoteProjectionDBAccess.length; idx++) { if (sDefaultNoteProjectionDBAccess[idx].equals(FlashCardsContract.Note._ID)) { sDefaultNoteProjectionDBAccess[idx] = "id as _id"; } } } @Override public boolean onCreate() { // Initialize content provider on startup. Timber.d("CardContentProvider: onCreate"); return true; } @Override public String getType(Uri uri) { // Find out what data the user is requesting int match = sUriMatcher.match(uri); switch (match) { case NOTES: return FlashCardsContract.Note.CONTENT_TYPE; case NOTES_ID: return FlashCardsContract.Note.CONTENT_ITEM_TYPE; case NOTES_ID_CARDS: return FlashCardsContract.Card.CONTENT_TYPE; case NOTES_ID_CARDS_ORD: return FlashCardsContract.Card.CONTENT_ITEM_TYPE; case MODELS: return FlashCardsContract.Model.CONTENT_TYPE; case MODELS_ID: return FlashCardsContract.Model.CONTENT_ITEM_TYPE; case MODELS_ID_TEMPLATES: return FlashCardsContract.CardTemplate.CONTENT_TYPE; case MODELS_ID_TEMPLATES_ID: return FlashCardsContract.CardTemplate.CONTENT_ITEM_TYPE; case SCHEDULE: return FlashCardsContract.ReviewInfo.CONTENT_TYPE; case DECKS: return FlashCardsContract.Deck.CONTENT_TYPE; case DECKS_ID: return FlashCardsContract.Deck.CONTENT_TYPE; case DECK_SELECTED: return FlashCardsContract.Deck.CONTENT_TYPE; default: // Unknown URI type throw new IllegalArgumentException("uri " + uri + " is not supported"); } } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Timber.d("CardContentProvider.query"); Collection col = CollectionHelper.getInstance().getCol(getContext()); if (col == null) { return null; } // Find out what data the user is requesting int match = sUriMatcher.match(uri); switch (match) { case NOTES: { /* Search for notes */ // TODO: Allow sort order, then also update description in FlashCardContract String columnsStr = proj2str(projection); String query = (selection != null) ? selection : ""; List<Long> noteIds = col.findNotes(query); if ((noteIds != null) && (!noteIds.isEmpty())) { String selectedIds = "id in " + Utils.ids2str(noteIds); Cursor cur; try { cur = col.getDb().getDatabase() .rawQuery("select " + columnsStr + " from notes where " + selectedIds, null); } catch (SQLException e) { throw new IllegalArgumentException("Not possible to query for data for IDs " + selectedIds, e); } return cur; } else { return null; } } case NOTES_ID: { /* Direct access note */ long noteId; noteId = Long.parseLong(uri.getPathSegments().get(1)); String columnsStr = proj2str(projection); String selectedIds = "id = " + noteId; Cursor cur; try { cur = col.getDb().getDatabase() .rawQuery("select " + columnsStr + " from notes where " + selectedIds, null); } catch (SQLException e) { throw new IllegalArgumentException("Not possible to query for data for ID \"" + noteId + "\"", e); } return cur; } case NOTES_ID_CARDS: { Note currentNote = getNoteFromUri(uri, col); String[] columns = ((projection != null) ? projection : FlashCardsContract.Card.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); for (Card currentCard : currentNote.cards()) { addCardToCursor(currentCard, rv, col, columns); } return rv; } case NOTES_ID_CARDS_ORD: { Card currentCard = getCardFromUri(uri, col); String[] columns = ((projection != null) ? projection : FlashCardsContract.Card.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); addCardToCursor(currentCard, rv, col, columns); return rv; } case MODELS: { HashMap<Long, JSONObject> models = col.getModels().getModels(); String[] columns = ((projection != null) ? projection : FlashCardsContract.Model.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); for (Long modelId : models.keySet()) { addModelToCursor(modelId, models, rv, columns); } return rv; } case MODELS_ID: { long modelId = getModelIdFromUri(uri, col); String[] columns = ((projection != null) ? projection : FlashCardsContract.Model.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); HashMap<Long, JSONObject> models = col.getModels().getModels(); addModelToCursor(modelId, models, rv, columns); return rv; } case MODELS_ID_TEMPLATES: { /* Direct access model templates */ JSONObject currentModel = col.getModels().get(getModelIdFromUri(uri, col)); String[] columns = ((projection != null) ? projection : CardTemplate.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); try { JSONArray templates = currentModel.getJSONArray("tmpls"); for (int idx = 0; idx < templates.length(); idx++) { JSONObject template = templates.getJSONObject(idx); addTemplateToCursor(template, currentModel, idx + 1, rv, columns); } } catch (JSONException e) { throw new IllegalArgumentException("Model is malformed", e); } return rv; } case MODELS_ID_TEMPLATES_ID: { /* Direct access model template with specific ID */ int ord = Integer.parseInt(uri.getLastPathSegment()); JSONObject currentModel = col.getModels().get(getModelIdFromUri(uri, col)); String[] columns = ((projection != null) ? projection : CardTemplate.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); try { JSONObject template = getTemplateFromUri(uri, col); addTemplateToCursor(template, currentModel, ord + 1, rv, columns); } catch (JSONException e) { throw new IllegalArgumentException("Model is malformed", e); } return rv; } case SCHEDULE: { String[] columns = ((projection != null) ? projection : FlashCardsContract.ReviewInfo.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); long selectedDeckBeforeQuery = col.getDecks().selected(); long deckIdOfTemporarilySelectedDeck = -1; int limit = 1; //the number of scheduled cards to return int selectionArgIndex = 0; //parsing the selection arguments if (selection != null) { String[] args = selection.split(","); //split selection to get arguments like "limit=?" for (String arg : args) { String[] keyAndValue = arg.split("="); //split arguments into key ("limit") and value ("?") try { //check if value is a placeholder ("?"), if so replace with the next value of selectionArgs String value = keyAndValue[1].trim().equals("?") ? selectionArgs[selectionArgIndex++] : keyAndValue[1]; if (keyAndValue[0].trim().equals("limit")) { limit = Integer.valueOf(value); } else if (keyAndValue[0].trim().equals("deckID")) { deckIdOfTemporarilySelectedDeck = Long.valueOf(value); if (!selectDeckWithCheck(col, deckIdOfTemporarilySelectedDeck)) { return rv; //if the provided deckID is wrong, return empty cursor. } } } catch (NumberFormatException nfe) { nfe.printStackTrace(); } } } //retrieve the number of cards provided by the selection parameter "limit" col.getSched().reset(); for (int k = 0; k < limit; k++) { Card currentCard = col.getSched().getCard(); if (currentCard != null) { int buttonCount = col.getSched().answerButtons(currentCard); JSONArray buttonTexts = new JSONArray(); for (int i = 0; i < buttonCount; i++) { buttonTexts.put(col.getSched().nextIvlStr(getContext(), currentCard, i + 1)); } addReviewInfoToCursor(currentCard, buttonTexts, buttonCount, rv, col, columns); } else { break; } } if (deckIdOfTemporarilySelectedDeck != -1) {//if the selected deck was changed //change the selected deck back to the one it was before the query col.getDecks().select(selectedDeckBeforeQuery); } return rv; } case DECKS: { List<Sched.DeckDueTreeNode> allDecks = col.getSched().deckDueList(); String[] columns = ((projection != null) ? projection : FlashCardsContract.Deck.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, allDecks.size()); for (Sched.DeckDueTreeNode deck : allDecks) { long id = deck.did; String name = deck.names[0]; addDeckToCursor(id, name, getDeckCountsFromDueTreeNode(deck), rv, col, columns); } return rv; } case DECKS_ID: { /* Direct access deck */ String[] columns = ((projection != null) ? projection : FlashCardsContract.Deck.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); List<Sched.DeckDueTreeNode> allDecks = col.getSched().deckDueList(); long deckId; deckId = Long.parseLong(uri.getPathSegments().get(1)); for (Sched.DeckDueTreeNode deck : allDecks) { if (deck.did == deckId) { addDeckToCursor(deckId, deck.names[0], getDeckCountsFromDueTreeNode(deck), rv, col, columns); return rv; } } return rv; } case DECK_SELECTED: { long id = col.getDecks().selected(); String name = col.getDecks().name(id); String[] columns = ((projection != null) ? projection : FlashCardsContract.Deck.DEFAULT_PROJECTION); MatrixCursor rv = new MatrixCursor(columns, 1); JSONArray counts = new JSONArray(Arrays.asList(col.getSched().counts())); addDeckToCursor(id, name, counts, rv, col, columns); return rv; } default: // Unknown URI type throw new IllegalArgumentException("uri " + uri + " is not supported"); } } private JSONArray getDeckCountsFromDueTreeNode(Sched.DeckDueTreeNode deck) { JSONArray deckCounts = new JSONArray(); deckCounts.put(deck.lrnCount); deckCounts.put(deck.revCount); deckCounts.put(deck.newCount); return deckCounts; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { Timber.d("CardContentProvider.update"); Collection col = CollectionHelper.getInstance().getCol(getContext()); if (col == null) { return 0; } // Find out what data the user is requesting int match = sUriMatcher.match(uri); int updated = 0; // Number of updated entries (return value) switch (match) { case NOTES: throw new IllegalArgumentException("Not possible to update notes directly (only through data URI)"); case NOTES_ID: { /* Direct access note details */ Note currentNote = getNoteFromUri(uri, col); // the key of the ContentValues contains the column name // the value of the ContentValues contains the row value. Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.Note.FLDS)) { // Update FLDS Timber.d("CardContentProvider: flds update..."); String newFldsEncoded = (String) entry.getValue(); String[] flds = Utils.splitFields(newFldsEncoded); // Check that correct number of flds specified if (flds.length != currentNote.getFields().length) { throw new IllegalArgumentException("Incorrect flds argument : " + newFldsEncoded); } // Update the note for (int idx = 0; idx < flds.length; idx++) { currentNote.setField(idx, flds[idx]); } updated++; } else if (key.equals(FlashCardsContract.Note.TAGS)) { // Update tags Timber.d("CardContentProvider: tags update..."); currentNote.setTagsFromStr((String) entry.getValue()); updated++; } else { // Unsupported column throw new IllegalArgumentException("Unsupported column: " + key); } } Timber.d("CardContentProvider: Saving note..."); currentNote.flush(); break; } case NOTES_ID_CARDS: // TODO: To be implemented throw new UnsupportedOperationException("Not yet implemented"); // break; case NOTES_ID_CARDS_ORD: { Card currentCard = getCardFromUri(uri, col); boolean isDeckUpdate = false; long did = -1; // the key of the ContentValues contains the column name // the value of the ContentValues contains the row value. Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { // Only updates on deck id is supported String key = entry.getKey(); isDeckUpdate = key.equals(FlashCardsContract.Card.DECK_ID); did = values.getAsLong(key); } /* now update the card */ if ((isDeckUpdate) && (did >= 0)) { Timber.d("CardContentProvider: Moving card to other deck..."); col.getDecks().flush(); currentCard.setDid(did); currentCard.flush(); updated++; } else { // User tries an operation that is not (yet?) supported. throw new IllegalArgumentException("Currently only updates of decks are supported"); } break; } case MODELS: throw new IllegalArgumentException("Cannot update models in bulk"); case MODELS_ID: // Get the input parameters String newModelName = values.getAsString(FlashCardsContract.Model.NAME); String newCss = values.getAsString(FlashCardsContract.Model.CSS); String newDid = values.getAsString(FlashCardsContract.Model.DECK_ID); String newFieldList = values.getAsString(FlashCardsContract.Model.FIELD_NAMES); if (newFieldList != null) { // Changing the field names would require a full-sync throw new IllegalArgumentException("Field names cannot be changed via provider"); } // Get the original note JSON JSONObject model = col.getModels().get(getModelIdFromUri(uri, col)); try { // Update model name and/or css if (newModelName != null) { model.put("name", newModelName); updated++; } if (newCss != null) { model.put("css", newCss); updated++; } if (newDid != null) { model.put("did", newDid); updated++; } col.getModels().save(model); } catch (JSONException e) { Timber.e(e, "JSONException updating model"); } break; case MODELS_ID_TEMPLATES: throw new IllegalArgumentException("Cannot update templates in bulk"); case MODELS_ID_TEMPLATES_ID: Long mid = values.getAsLong(CardTemplate.MODEL_ID); Integer ord = values.getAsInteger(CardTemplate.ORD); String name = values.getAsString(CardTemplate.NAME); String qfmt = values.getAsString(CardTemplate.QUESTION_FORMAT); String afmt = values.getAsString(CardTemplate.ANSWER_FORMAT); String bqfmt = values.getAsString(CardTemplate.BROWSER_QUESTION_FORMAT); String bafmt = values.getAsString(CardTemplate.BROWSER_ANSWER_FORMAT); // Throw exception if read-only fields are included if (mid != null || ord != null) { throw new IllegalArgumentException("Can update mid or ord"); } // Update the model try { Integer templateOrd = Integer.parseInt(uri.getLastPathSegment()); JSONObject existingModel = col.getModels().get(getModelIdFromUri(uri, col)); JSONArray templates = existingModel.getJSONArray("tmpls"); JSONObject template = templates.getJSONObject(templateOrd); if (name != null) { template.put("name", name); updated++; } if (qfmt != null) { template.put("qfmt", qfmt); updated++; } if (afmt != null) { template.put("afmt", afmt); updated++; } if (bqfmt != null) { template.put("bqfmt", bqfmt); updated++; } if (bafmt != null) { template.put("bafmt", bafmt); updated++; } // Save the model templates.put(templateOrd, template); existingModel.put("tmpls", templates); col.getModels().save(existingModel, true); } catch (JSONException e) { throw new IllegalArgumentException("Model is malformed", e); } break; case SCHEDULE: { Set<Map.Entry<String, Object>> valueSet = values.valueSet(); int cardOrd = -1; long noteID = -1; int ease = -1; long timeTaken = -1; for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.ReviewInfo.NOTE_ID)) { noteID = values.getAsLong(key); } else if (key.equals(FlashCardsContract.ReviewInfo.CARD_ORD)) { cardOrd = values.getAsInteger(key); } else if (key.equals(FlashCardsContract.ReviewInfo.EASE)) { ease = values.getAsInteger(key); } else if (key.equals(FlashCardsContract.ReviewInfo.TIME_TAKEN)) { timeTaken = values.getAsLong(key); } } if (cardOrd != -1 && noteID != -1) { Card cardToAnswer = getCard(noteID, cardOrd, col); if (cardToAnswer != null) { answerCard(col, col.getSched(), cardToAnswer, ease, timeTaken); updated++; } else { Timber.e( "Requested card with noteId %d and cardOrd %d was not found. Either the provided " + "noteId/cardOrd were wrong or the card has been deleted in the meantime.", noteID, cardOrd); } } break; } case DECKS: throw new IllegalArgumentException("Can't update decks in bulk"); case DECKS_ID: throw new UnsupportedOperationException("Not yet implemented"); case DECK_SELECTED: { Set<Map.Entry<String, Object>> valueSet = values.valueSet(); for (Map.Entry<String, Object> entry : valueSet) { String key = entry.getKey(); if (key.equals(FlashCardsContract.Deck.DECK_ID)) { long deckId = values.getAsLong(key); if (selectDeckWithCheck(col, deckId)) { updated++; } } } break; } default: // Unknown URI type throw new IllegalArgumentException("uri " + uri + " is not supported"); } return updated; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { Timber.d("CardContentProvider.delete"); Collection col = CollectionHelper.getInstance().getCol(getContext()); if (col == null) { return 0; } switch (sUriMatcher.match(uri)) { case NOTES_ID: col.remNotes(new long[] { Long.parseLong(uri.getPathSegments().get(1)) }); return 1; default: throw new UnsupportedOperationException(); } } @Override public Uri insert(Uri uri, ContentValues values) { Timber.d("CardContentProvider.insert"); Collection col = CollectionHelper.getInstance().getCol(getContext()); if (col == null) { return null; } // Find out what data the user is requesting int match = sUriMatcher.match(uri); switch (match) { case NOTES: { /* Insert new note with specified fields and tags */ Long modelId = values.getAsLong(FlashCardsContract.Note.MID); String flds = values.getAsString(FlashCardsContract.Note.FLDS); String tags = values.getAsString(FlashCardsContract.Note.TAGS); // Create empty note com.ichi2.libanki.Note newNote = new com.ichi2.libanki.Note(col, col.getModels().get(modelId)); // Set fields String[] fldsArray = Utils.splitFields(flds); // Check that correct number of flds specified if (fldsArray.length != newNote.getFields().length) { throw new IllegalArgumentException("Incorrect flds argument : " + flds); } for (int idx = 0; idx < fldsArray.length; idx++) { newNote.setField(idx, fldsArray[idx]); } // Set tags if (tags != null) { newNote.setTagsFromStr(tags); } // Add to collection col.addNote(newNote); return Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, Long.toString(newNote.getId())); } case NOTES_ID: // Note ID is generated automatically by libanki throw new IllegalArgumentException("Not possible to insert note with specific ID"); case NOTES_ID_CARDS: // Cards are generated automatically by libanki throw new IllegalArgumentException("Not possible to insert cards directly (only through NOTES)"); case NOTES_ID_CARDS_ORD: // Cards are generated automatically by libanki throw new IllegalArgumentException("Not possible to insert cards directly (only through NOTES)"); case MODELS: // Get input arguments String modelName = values.getAsString(FlashCardsContract.Model.NAME); String css = values.getAsString(FlashCardsContract.Model.CSS); Long did = values.getAsLong(FlashCardsContract.Model.DECK_ID); String fieldNames = values.getAsString(FlashCardsContract.Model.FIELD_NAMES); Integer numCards = values.getAsInteger(FlashCardsContract.Model.NUM_CARDS); // Throw exception if required fields empty if (modelName == null || fieldNames == null || numCards == null) { throw new IllegalArgumentException("Model name, field_names, and num_cards can't be empty"); } // Create a new model Models mm = col.getModels(); JSONObject newModel = mm.newModel(modelName); try { // Add the fields String[] allFields = Utils.splitFields(fieldNames); for (String f : allFields) { mm.addField(newModel, mm.newField(f)); } // Add some empty card templates for (int idx = 0; idx < numCards; idx++) { JSONObject t = mm.newTemplate("Card " + (idx + 1)); t.put("qfmt", String.format("{{%s}}", allFields[0])); String answerField = allFields[0]; if (allFields.length > 1) { answerField = allFields[1]; } t.put("afmt", String.format("{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{%s}}", answerField)); mm.addTemplate(newModel, t); } // Add the CSS if specified if (css != null) { newModel.put("css", css); } // Add the did if specified if (did != null) { newModel.put("did", did); } // Add the model to collection (from this point on edits will require a full-sync) mm.add(newModel); mm.save(newModel); // TODO: is this necessary? // Get the mid and return a URI String mid = Long.toString(newModel.getLong("id")); return Uri.withAppendedPath(FlashCardsContract.Model.CONTENT_URI, mid); } catch (ConfirmModSchemaException e) { // This exception should never be thrown when inserting new models Timber.e(e, "Unexpected ConfirmModSchema exception adding new model %s", modelName); throw new IllegalArgumentException("ConfirmModSchema exception adding new model " + modelName); } catch (JSONException e) { Timber.e(e, "Could not set a field of new model %s", modelName); return null; } case MODELS_ID: // Model ID is generated automatically by libanki throw new IllegalArgumentException("Not possible to insert model with specific ID"); case MODELS_ID_TEMPLATES: // Adding new templates after the model is created could require a full-sync throw new IllegalArgumentException("Templates can only be added at the time of model insertion"); case MODELS_ID_TEMPLATES_ID: // Adding new templates after the model is created could require a full-sync throw new IllegalArgumentException("Templates can only be added at the time of model insertion"); case SCHEDULE: // Doesn't make sense to insert an object into the schedule table throw new IllegalArgumentException("Not possible to perform insert operation on schedule"); case DECKS: // Insert new deck with specified name String deckName = values.getAsString(FlashCardsContract.Deck.DECK_NAME); did = col.getDecks().id(deckName); return Uri.withAppendedPath(FlashCardsContract.Deck.CONTENT_ALL_URI, Long.toString(did)); case DECK_SELECTED: // Can't have more than one selected deck throw new IllegalArgumentException("Selected deck can only be queried and updated"); case DECKS_ID: // Deck ID is generated automatically by libanki throw new IllegalArgumentException("Not possible to insert deck with specific ID"); default: // Unknown URI type throw new IllegalArgumentException("uri " + uri + " is not supported"); } } private static String proj2str(String[] projection) { StringBuilder rv = new StringBuilder(); if (projection != null) { for (String column : projection) { int idx = projSearch(FlashCardsContract.Note.DEFAULT_PROJECTION, column); if (idx >= 0) { rv.append(sDefaultNoteProjectionDBAccess[idx]); rv.append(","); } else { throw new IllegalArgumentException("Unknown column " + column); } } } else { for (String column : sDefaultNoteProjectionDBAccess) { rv.append(column); rv.append(","); } } rv.deleteCharAt(rv.length() - 1); return rv.toString(); } private static int projSearch(String[] projection, String column) { for (int i = 0; i < projection.length; i++) { if (projection[i].equals(column)) { return i; } } return -1; } private void addModelToCursor(Long modelId, HashMap<Long, JSONObject> models, MatrixCursor rv, String[] columns) { JSONObject jsonObject = models.get(modelId); MatrixCursor.RowBuilder rb = rv.newRow(); try { for (String column : columns) { if (column.equals(FlashCardsContract.Model._ID)) { rb.add(modelId); } else if (column.equals(FlashCardsContract.Model.NAME)) { rb.add(jsonObject.getString("name")); } else if (column.equals(FlashCardsContract.Model.FIELD_NAMES)) { JSONArray flds = jsonObject.getJSONArray("flds"); String[] allFlds = new String[flds.length()]; for (int idx = 0; idx < flds.length(); idx++) { allFlds[idx] = flds.getJSONObject(idx).optString("name", ""); } rb.add(Utils.joinFields(allFlds)); } else if (column.equals(FlashCardsContract.Model.NUM_CARDS)) { rb.add(jsonObject.getJSONArray("tmpls").length()); } else if (column.equals(FlashCardsContract.Model.CSS)) { rb.add(jsonObject.getString("css")); } else if (column.equals(FlashCardsContract.Model.DECK_ID)) { rb.add(jsonObject.getLong("did")); } else { throw new UnsupportedOperationException("Column \"" + column + "\" is unknown"); } } } catch (JSONException e) { Timber.e(e, "Error parsing JSONArray"); throw new IllegalArgumentException("Model " + modelId + " is malformed", e); } } private void addCardToCursor(Card currentCard, MatrixCursor rv, Collection col, String[] columns) { String cardName; try { cardName = currentCard.template().getString("name"); } catch (JSONException je) { throw new IllegalArgumentException("Card is using an invalid template", je); } String question = currentCard.q(); String answer = currentCard.a(); MatrixCursor.RowBuilder rb = rv.newRow(); for (String column : columns) { if (column.equals(FlashCardsContract.Card.NOTE_ID)) { rb.add(currentCard.note().getId()); } else if (column.equals(FlashCardsContract.Card.CARD_ORD)) { rb.add(currentCard.getOrd()); } else if (column.equals(FlashCardsContract.Card.CARD_NAME)) { rb.add(cardName); } else if (column.equals(FlashCardsContract.Card.DECK_ID)) { rb.add(currentCard.getDid()); } else if (column.equals(FlashCardsContract.Card.QUESTION)) { rb.add(question); } else if (column.equals(FlashCardsContract.Card.ANSWER)) { rb.add(answer); } else if (column.equals(FlashCardsContract.Card.QUESTION_SIMPLE)) { rb.add(currentCard.qSimple()); } else if (column.equals(FlashCardsContract.Card.ANSWER_SIMPLE)) { rb.add(currentCard._getQA(false).get("a")); } else if (column.equals(FlashCardsContract.Card.ANSWER_PURE)) { rb.add(currentCard.getPureAnswer()); } else { throw new UnsupportedOperationException("Column \"" + column + "\" is unknown"); } } } private void addReviewInfoToCursor(Card currentCard, JSONArray nextReviewTimesJson, int buttonCount, MatrixCursor rv, Collection col, String[] columns) { MatrixCursor.RowBuilder rb = rv.newRow(); for (String column : columns) { if (column.equals(FlashCardsContract.Card.NOTE_ID)) { rb.add(currentCard.note().getId()); } else if (column.equals(FlashCardsContract.ReviewInfo.CARD_ORD)) { rb.add(currentCard.getOrd()); } else if (column.equals(FlashCardsContract.ReviewInfo.BUTTON_COUNT)) { rb.add(buttonCount); } else if (column.equals(FlashCardsContract.ReviewInfo.NEXT_REVIEW_TIMES)) { rb.add(nextReviewTimesJson.toString()); } else if (column.equals(FlashCardsContract.ReviewInfo.MEDIA_FILES)) { rb.add(new JSONArray( col.getMedia().filesInStr(currentCard.note().getMid(), currentCard.q() + currentCard.a()))); } else { throw new UnsupportedOperationException("Column \"" + column + "\" is unknown"); } } } private void answerCard(Collection col, Sched sched, Card cardToAnswer, int ease, long timeTaken) { try { AnkiDb ankiDB = col.getDb(); ankiDB.getDatabase().beginTransaction(); try { if (cardToAnswer != null) { if (timeTaken != -1) { cardToAnswer.setTimerStarted(Utils.now() - timeTaken / 1000); } sched.answerCard(cardToAnswer, ease); } ankiDB.getDatabase().setTransactionSuccessful(); } finally { ankiDB.getDatabase().endTransaction(); } } catch (RuntimeException e) { Timber.e(e, "answerCard - RuntimeException on answering card"); AnkiDroidApp.sendExceptionReport(e, "doInBackgroundAnswerCard"); return; } } private void addTemplateToCursor(JSONObject tmpl, JSONObject model, int id, MatrixCursor rv, String[] columns) { try { MatrixCursor.RowBuilder rb = rv.newRow(); for (String column : columns) { if (column.equals(CardTemplate._ID)) { rb.add(id); } else if (column.equals(CardTemplate.MODEL_ID)) { rb.add(model.getLong("id")); } else if (column.equals(CardTemplate.ORD)) { rb.add(tmpl.getInt("ord")); } else if (column.equals(CardTemplate.NAME)) { rb.add(tmpl.getString("name")); } else if (column.equals(CardTemplate.QUESTION_FORMAT)) { rb.add(tmpl.getString("qfmt")); } else if (column.equals(CardTemplate.ANSWER_FORMAT)) { rb.add(tmpl.getString("afmt")); } else if (column.equals(CardTemplate.BROWSER_QUESTION_FORMAT)) { rb.add(tmpl.getString("bqfmt")); } else if (column.equals(CardTemplate.BROWSER_ANSWER_FORMAT)) { rb.add(tmpl.getString("bafmt")); } else { throw new UnsupportedOperationException( "Support for column \"" + column + "\" is not implemented"); } } } catch (JSONException e) { Timber.e(e, "Error adding template to cursor"); throw new IllegalArgumentException("Template is malformed", e); } } private void addDeckToCursor(long id, String name, JSONArray deckCounts, MatrixCursor rv, Collection col, String[] columns) { MatrixCursor.RowBuilder rb = rv.newRow(); for (String column : columns) { if (column.equals(FlashCardsContract.Deck.DECK_NAME)) { rb.add(name); } else if (column.equals(FlashCardsContract.Deck.DECK_ID)) { rb.add(id); } else if (column.equals(FlashCardsContract.Deck.DECK_COUNTS)) { rb.add(deckCounts); } else if (column.equals(FlashCardsContract.Deck.OPTIONS)) { String config = col.getDecks().confForDid(id).toString(); rb.add(config); } } } private boolean selectDeckWithCheck(Collection col, long did) { if (col.getDecks().get(did, false) != null) { col.getDecks().select(did); return true; } else { Timber.e("Requested deck with id %d was not found in deck list. Either the deckID provided was wrong" + "or the deck has been deleted in the meantime.", did); return false; } } private Card getCardFromUri(Uri uri, Collection col) { long noteId; int ord; noteId = Long.parseLong(uri.getPathSegments().get(1)); ord = Integer.parseInt(uri.getPathSegments().get(3)); return getCard(noteId, ord, col); } private Card getCard(long noteId, int ord, Collection col) { Note currentNote = col.getNote(noteId); Card currentCard = null; for (Card card : currentNote.cards()) { if (card.getOrd() == ord) { currentCard = card; } } if (currentCard == null) { throw new IllegalArgumentException("Card with ord " + ord + " does not exist for note " + noteId); } return currentCard; } private Note getNoteFromUri(Uri uri, Collection col) { long noteId; noteId = Long.parseLong(uri.getPathSegments().get(1)); return col.getNote(noteId); } private long getModelIdFromUri(Uri uri, Collection col) { String modelIdSegment = uri.getPathSegments().get(1); long id; if (modelIdSegment.equals(FlashCardsContract.Model.CURRENT_MODEL_ID)) { id = col.getModels().current().optLong("id", -1); } else { try { id = Long.parseLong(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { throw new IllegalArgumentException( "Model ID must be either numeric or the String CURRENT_MODEL_ID"); } } return id; } private JSONObject getTemplateFromUri(Uri uri, Collection col) throws JSONException { JSONObject model = col.getModels().get(getModelIdFromUri(uri, col)); Integer ord = Integer.parseInt(uri.getLastPathSegment()); return model.getJSONArray("tmpls").getJSONObject(ord); } }