gt:                 Long mid = values.getAsLong(CardTemplate.MODEL_ID);
context: /***************************************************************************************
   *                                                                                      *
   * 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.database.sqlite.SQLiteDatabase;
  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.Map;
  import java.util.Set;
  
  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:
  