/***************************************************************************************
* *
* Copyright (c) 2015 Frank Oltmanns <frank.oltmanns@gmail.com> *
* Copyright (c) 2015 Timothy Rae <timothy.rae@gmail.com> *
* Copyright (c) 2016 Mark Carter <mark@marcardar.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.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.ichi2.anki.AnkiDroidApp;
import com.ichi2.anki.BuildConfig;
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.compat.CompatHelper;
import com.ichi2.libanki.Card;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.DB;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import io.requery.android.database.sqlite.SQLiteDatabase;
import timber.log.Timber;
import static com.ichi2.anki.FlashCardsContract.READ_WRITE_PERMISSION;
/**
* 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 {
private Context mContext;
/* 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 NOTES_V2 = 1005;
private static final int MODELS = 2000;
private static final int MODELS_ID = 2001;
private static final int MODELS_ID_EMPTY_CARDS = 2002;
private static final int MODELS_ID_TEMPLATES = 2003;
private static final int MODELS_ID_TEMPLATES_ID = 2004;
private static final int MODELS_ID_FIELDS = 2005;
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_v2", NOTES_V2);
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); // the model ID can also be "current"
sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/empty_cards", MODELS_ID_EMPTY_CARDS);
sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/templates", MODELS_ID_TEMPLATES);
sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/templates/#", MODELS_ID_TEMPLATES_ID);
sUriMatcher.addURI(FlashCardsContract.AUTHORITY, "models/*/fields", MODELS_ID_FIELDS);
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();
private static final String COL_NULL_ERROR_MSG = "AnkiDroid database inaccessible. Open AnkiDroid to see what's wrong.";
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");
mContext = getContext();
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_V2:
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_EMPTY_CARDS:
return FlashCardsContract.Card.CONTENT_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");
}
}
/** Only enforce permissions for queries and inserts on Android M and above, or if its a 'rogue client' **/
private boolean shouldEnforceQueryOrInsertSecurity() {
return CompatHelper.isMarshmallow() || knownRogueClient();
}
/** Enforce permissions for all updates on Android M and above. Otherwise block depending on URI and client app **/
private boolean shouldEnforceUpdateSecurity(Uri uri) {
final List<Integer> WHITELIST = Arrays.asList(NOTES_ID_CARDS_ORD, MODELS_ID, MODELS_ID_TEMPLATES_ID, SCHEDULE, DECK_SELECTED);
return CompatHelper.isMarshmallow() || !WHITELIST.contains(sUriMatcher.match(uri)) || knownRogueClient();
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String order) {
if (!hasReadWritePermission() && shouldEnforceQueryOrInsertSecurity()) {
throwSecurityException("query", uri);
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
Timber.d(getLogMessage("query", uri));
// Find out what data the user is requesting
int match = sUriMatcher.match(uri);
switch (match) {
case NOTES_V2: {
/* Search for notes using direct SQL query */
String[] proj = sanitizeNoteProjection(projection);
return col.getDb().getDatabase().query("notes", proj, selection, selectionArgs, null, null, order);
}
case NOTES: {
/* Search for notes using the libanki browser syntax */
String[] proj = sanitizeNoteProjection(projection);
String query = (selection != null) ? selection : "";
List<Long> noteIds = col.findNotes(query);
if ((noteIds != null) && (!noteIds.isEmpty())) {
String sel = String.format("id in (%s)", TextUtils.join(",", noteIds));
return col.getDb().getDatabase().query("notes", proj, sel, null, null, null, order);
} else {
return null;
}
}
case NOTES_ID: {
/* Direct access note with specific ID*/
String noteId = uri.getPathSegments().get(1);
String[] proj = sanitizeNoteProjection(projection);
return col.getDb().getDatabase().query("notes", proj, "id=?", new String[]{noteId}, null, null, order);
}
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: {
Models models = col.getModels();
String[] columns = ((projection != null) ? projection : FlashCardsContract.Model.DEFAULT_PROJECTION);
MatrixCursor rv = new MatrixCursor(columns, 1);
for (Long modelId : models.getModels().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);
addModelToCursor(modelId, col.getModels(), rv, columns);
return rv;
}
case MODELS_ID_TEMPLATES: {
/* Direct access model templates */
Models models = col.getModels();
JSONObject currentModel = models.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, models, 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 */
Models models = col.getModels();
int ord = Integer.parseInt(uri.getLastPathSegment());
JSONObject currentModel = models.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, models, 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(mContext, 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) {
if (!hasReadWritePermission() && shouldEnforceUpdateSecurity(uri)) {
throwSecurityException("update", uri);
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
col.log(getLogMessage("update", uri));
// 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_V2:
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();
// when the client does not specify FLDS, then don't update the FLDS
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...");
Object tags = entry.getValue();
if (tags != null) {
currentNote.setTagsFromStr(String.valueOf(tags));
}
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);
}
if (col.getDecks().isDyn(did)) {
throw new IllegalArgumentException("Cards cannot be moved to a filtered deck");
}
/* now update the card
*/
if ((isDeckUpdate) && (did >= 0)) {
Timber.d("CardContentProvider: Moving card to other deck...");
col.getDecks().flush();
currentCard.setDid(did);
currentCard.flush();
col.save();
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");
}
Integer newSortf = values.getAsInteger(FlashCardsContract.Model.SORT_FIELD_INDEX);
Integer newType = values.getAsInteger(FlashCardsContract.Model.TYPE);
String newLatexPost = values.getAsString(FlashCardsContract.Model.LATEX_POST);
String newLatexPre = values.getAsString(FlashCardsContract.Model.LATEX_PRE);
// 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) {
if (col.getDecks().isDyn(Long.parseLong(newDid))) {
throw new IllegalArgumentException("Cannot set a filtered deck as default deck for a model");
}
model.put("did", newDid);
updated++;
}
if (newSortf != null) {
model.put("sortf", newSortf);
updated++;
}
if (newType != null) {
model.put("type", newType);
updated++;
}
if (newLatexPost != null) {
model.put("latexPost", newLatexPost);
updated++;
}
if (newLatexPre != null) {
model.put("latexPre", newLatexPre);
updated++;
}
col.getModels().save(model);
col.save();
} 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("Updates to mid or ord are not allowed");
}
// 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);
col.save();
} 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:
// TODO: be sure to throw exception if change to the dyn value of a deck is requested
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 ++;
}
}
}
col.save();
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) {
if (!hasReadWritePermission()) {
throwSecurityException("delete", uri);
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
col.log(getLogMessage("delete", uri));
switch (sUriMatcher.match(uri)) {
case NOTES_ID:
col.remNotes(new long[]{Long.parseLong(uri.getPathSegments().get(1))});
return 1;
case MODELS_ID_EMPTY_CARDS:
JSONObject model = col.getModels().get(getModelIdFromUri(uri, col));
if (model == null) {
return -1;
}
List<Long> cids = col.genCards(col.getModels().nids(model));
col.remCards(Utils.arrayList2array(cids));
return cids.size();
default:
throw new UnsupportedOperationException();
}
}
/**
* This can be used to insert multiple notes into a single deck. The deck is specified as a query parameter.
*
* For example: content://com.ichi2.anki.flashcards/notes?deckId=1234567890123
*
* @param uri content Uri
* @param values for notes uri, it is acceptable for values to contain null items. Such items will be skipped
* @return number of notes added (does not include existing notes that were updated)
*/
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
if (!hasReadWritePermission() && shouldEnforceQueryOrInsertSecurity()) {
throwSecurityException("bulkInsert", uri);
}
// by default, #bulkInsert simply calls insert for each item in #values
// but in some cases, we want to override this behavior
int match = sUriMatcher.match(uri);
if (match == NOTES) {
String deckIdStr = uri.getQueryParameter(FlashCardsContract.Note.DECK_ID_QUERY_PARAM);
if (deckIdStr != null) {
try {
long deckId = Long.valueOf(deckIdStr);
return bulkInsertNotes(values, deckId);
} catch (NumberFormatException e) {
Timber.d("Invalid %s: %s", FlashCardsContract.Note.DECK_ID_QUERY_PARAM, deckIdStr);
}
}
// deckId not specified, so default to #super implementation (as in spec version 1)
}
return super.bulkInsert(uri, values);
}
/**
* This implementation optimizes for when the notes are grouped according to model
*/
private int bulkInsertNotes(ContentValues[] valuesArr, long deckId) {
if (valuesArr == null || valuesArr.length == 0) {
return 0;
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
if (col.getDecks().isDyn(deckId)) {
throw new IllegalArgumentException("A filtered deck cannot be specified as the deck in bulkInsertNotes");
}
col.log(String.format(Locale.US, "bulkInsertNotes: %d items.\n%s", valuesArr.length, getLogMessage("bulkInsert", null)));
// for caching model information (so we don't have to query for each note)
long modelId = -1L;
JSONObject model = null;
col.getDecks().flush(); // is it okay to move this outside the for-loop? Is it needed at all?
SQLiteDatabase sqldb = col.getDb().getDatabase();
try {
int result = 0;
sqldb.beginTransaction();
for (int i = 0; i < valuesArr.length; i++) {
ContentValues values = valuesArr[i];
if (values == null) {
continue;
}
String flds = values.getAsString(FlashCardsContract.Note.FLDS);
if (flds == null) {
continue;
}
Long thisModelId = values.getAsLong(FlashCardsContract.Note.MID);
if (thisModelId == null || thisModelId < 0) {
Timber.d("Unable to get model at index: " + i);
continue;
}
String[] fldsArray = Utils.splitFields(flds);
if (model == null || thisModelId != modelId) {
// new modelId so need to recalculate model, modelId and invalidate duplicateChecker (which is based on previous model)
model = col.getModels().get(thisModelId);
modelId = thisModelId;
}
// Create empty note
com.ichi2.libanki.Note newNote = new com.ichi2.libanki.Note(col, model); // for some reason we cannot pass modelId in here
// Set fields
// 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
String tags = values.getAsString(FlashCardsContract.Note.TAGS);
if (tags != null) {
newNote.setTagsFromStr(tags);
}
// Add to collection
col.addNote(newNote);
for (Card card : newNote.cards()) {
card.setDid(deckId);
card.flush();
}
result++;
}
col.save();
sqldb.setTransactionSuccessful();
return result;
} finally {
sqldb.endTransaction();
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (!hasReadWritePermission() && shouldEnforceQueryOrInsertSecurity()) {
throwSecurityException("insert", uri);
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
col.log(getLogMessage("insert", uri));
// 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);
col.save();
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);
Integer sortf = values.getAsInteger(FlashCardsContract.Model.SORT_FIELD_INDEX);
Integer type = values.getAsInteger(FlashCardsContract.Model.TYPE);
String latexPost = values.getAsString(FlashCardsContract.Model.LATEX_POST);
String latexPre = values.getAsString(FlashCardsContract.Model.LATEX_PRE);
// 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");
}
if (did != null && col.getDecks().isDyn(did)) {
throw new IllegalArgumentException("Cannot set a filtered deck as default deck for a model");
}
// 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);
}
if (sortf != null && sortf < allFields.length) {
newModel.put("sortf", sortf);
}
if (type != null) {
newModel.put("type", type);
}
if (latexPost != null) {
newModel.put("latexPost", latexPost);
}
if (latexPre != null) {
newModel.put("latexPre", latexPre);
}
// Add the model to collection (from this point on edits will require a full-sync)
mm.add(newModel);
col.save();
// 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: {
Models models = col.getModels();
Long mid = getModelIdFromUri(uri, col);
JSONObject existingModel = models.get(mid);
if (existingModel == null) {
throw new IllegalArgumentException("model missing: " + mid);
}
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);
try {
JSONObject t = models.newTemplate(name);
try {
t.put("qfmt", qfmt);
t.put("afmt", afmt);
t.put("bqfmt", bqfmt);
t.put("bafmt", bafmt);
} catch (JSONException e) {
throw new RuntimeException(e);
}
models.addTemplate(existingModel, t);
models.save(existingModel);
col.save();
return ContentUris.withAppendedId(uri, t.getInt("ord"));
} catch (ConfirmModSchemaException e) {
throw new IllegalArgumentException("Unable to add template", e);
} catch (JSONException e) {
throw new IllegalArgumentException("Unable to get ord from new template", e);
}
}
case MODELS_ID_TEMPLATES_ID:
throw new IllegalArgumentException("Not possible to insert template with specific ORD");
case MODELS_ID_FIELDS: {
Models models = col.getModels();
Long mid = getModelIdFromUri(uri, col);
JSONObject existingModel = models.get(mid);
if (existingModel == null) {
throw new IllegalArgumentException("model missing: " + mid);
}
String name = values.getAsString(FlashCardsContract.Model.FIELD_NAME);
if (name == null) {
throw new IllegalArgumentException("field name missing for model: " + mid);
}
JSONObject field = models.newField(name);
try {
models.addField(existingModel, field);
col.save();
JSONArray ja = existingModel.getJSONArray("flds");
return ContentUris.withAppendedId(uri, ja.length() - 1);
} catch (ConfirmModSchemaException e) {
throw new IllegalArgumentException("Unable to insert field: " + name, e);
} catch (JSONException e) {
throw new IllegalArgumentException("Unable to get newly created field: " + name, e);
}
}
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, false);
if (did != null) {
throw new IllegalArgumentException("Deck name already exists: " + deckName);
}
did = col.getDecks().id(deckName, true);
JSONObject deck = col.getDecks().get(did);
if (deck != null) {
try {
String deckDesc = values.getAsString(FlashCardsContract.Deck.DECK_DESC);
if (deckDesc != null) {
deck.put("desc", deckDesc);
}
} catch (JSONException e) {
Timber.e(e, "Could not set a field of new deck %s", deckName);
return null;
}
}
col.getDecks().flush();
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[] sanitizeNoteProjection(String[] projection) {
if (projection == null || projection.length == 0) {
return sDefaultNoteProjectionDBAccess;
}
List<String> sanitized = new ArrayList<>();
for (String column : projection) {
int idx = projSearch(FlashCardsContract.Note.DEFAULT_PROJECTION, column);
if (idx >= 0) {
sanitized.add(sDefaultNoteProjectionDBAccess[idx]);
} else {
throw new IllegalArgumentException("Unknown column " + column);
}
}
return sanitized.toArray(new String[sanitized.size()]);
}
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, Models 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 if (column.equals(FlashCardsContract.Model.SORT_FIELD_INDEX)) {
rb.add(jsonObject.getLong("sortf"));
} else if (column.equals(FlashCardsContract.Model.TYPE)) {
rb.add(jsonObject.getLong("type"));
} else if (column.equals(FlashCardsContract.Model.LATEX_POST)) {
rb.add(jsonObject.getString("latexPost"));
} else if (column.equals(FlashCardsContract.Model.LATEX_PRE)) {
rb.add(jsonObject.getString("latexPre"));
} else if (column.equals(FlashCardsContract.Model.NOTE_COUNT)) {
rb.add(models.useCount(jsonObject));
} 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 {
DB db = col.getDb();
db.getDatabase().beginTransaction();
try {
if (cardToAnswer != null) {
if(timeTaken != -1){
cardToAnswer.setTimerStarted(Utils.now()-timeTaken/1000);
}
sched.answerCard(cardToAnswer, ease);
}
db.getDatabase().setTransactionSuccessful();
} finally {
db.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, Models models, 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 if (column.equals(CardTemplate.CARD_COUNT)) {
rb.add(models.tmplUseCount(model, tmpl.getInt("ord")));
} 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);
}else if (column.equals(FlashCardsContract.Deck.DECK_DYN)) {
rb.add(col.getDecks().isDyn(id));
}else if (column.equals(FlashCardsContract.Deck.DECK_DESC)) {
String desc = col.getDecks().getActualDescription();
rb.add(desc);
}
}
}
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);
}
private void throwSecurityException(String methodName, Uri uri) {
String msg = String.format("Permission not granted for: %s", getLogMessage(methodName, uri));
Timber.e(msg);
throw new SecurityException(msg);
}
private String getLogMessage(String methodName, Uri uri) {
final String format = "%s.%s %s (%s)";
String path = uri == null ? null : uri.getPath();
return String.format(format, getClass().getSimpleName(), methodName, path, getCallingPackageSafe());
}
private boolean hasReadWritePermission() {
if (BuildConfig.DEBUG) { // Allow self-calling of the provider only in debug builds (e.g. for unit tests)
return mContext.checkCallingOrSelfPermission(READ_WRITE_PERMISSION) == PackageManager.PERMISSION_GRANTED;
}
return mContext.checkCallingPermission(READ_WRITE_PERMISSION) == PackageManager.PERMISSION_GRANTED;
}
/** Returns true if the calling package is known to be "rogue" and should be blocked.
Calling package might be rogue if it has not declared #READ_WRITE_PERMISSION in its manifest, or if blacklisted **/
private boolean knownRogueClient() {
final PackageManager pm = mContext.getPackageManager();
try {
PackageInfo callingPi = pm.getPackageInfo(getCallingPackageSafe(), PackageManager.GET_PERMISSIONS);
if (callingPi == null || callingPi.requestedPermissions == null) {
return false;
}
return !Arrays.asList(callingPi.requestedPermissions).contains(READ_WRITE_PERMISSION);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
@Nullable
private String getCallingPackageSafe() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return getCallingPackage();
}
String[] pkgs = mContext.getPackageManager().getPackagesForUid(Binder.getCallingUid());
if (pkgs.length == 1) {
return pkgs[0]; // This is usual case, unless multiple packages signed with same key & using "sharedUserId"
}
return null;
}
}