{"action":"created","comment":{"url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/comments/25994505","id":25994505,"diff_hunk":"@@ -0,0 +1,616 @@\n+/***************************************************************************************\n+ *                                                                                      *\n+ * Copyright (c) 2015 Frank Oltmanns <frank.oltmanns@gmail.com>                         *\n+ *                                                                                      *\n+ * This program is free software; you can redistribute it and/or modify it under        *\n+ * the terms of the GNU General Public License as published by the Free Software        *\n+ * Foundation; either version 3 of the License, or (at your option) any later           *\n+ * version.                                                                             *\n+ *                                                                                      *\n+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *\n+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *\n+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *\n+ *                                                                                      *\n+ * You should have received a copy of the GNU General Public License along with         *\n+ * this program.  If not, see <http://www.gnu.org/licenses/>.                           *\n+ ****************************************************************************************/\n+\n+package com.ichi2.anki.provider;\n+\n+import android.content.ContentProvider;\n+import android.content.ContentValues;\n+import android.content.UriMatcher;\n+import android.database.Cursor;\n+import android.database.MatrixCursor;\n+import android.database.SQLException;\n+import android.net.Uri;\n+\n+import com.ichi2.anki.AnkiDroidApp;\n+import com.ichi2.anki.provider.FlashCardsContract.DataColumns;\n+import com.ichi2.anki.provider.FlashCardsContract.Data.Field;\n+import com.ichi2.libanki.Card;\n+import com.ichi2.libanki.Collection;\n+import com.ichi2.libanki.Note;\n+import com.ichi2.libanki.Utils;\n+\n+import org.json.JSONException;\n+import org.json.JSONObject;\n+\n+import java.util.ArrayList;\n+import java.util.Arrays;\n+import java.util.HashMap;\n+import java.util.List;\n+import java.util.Map;\n+import java.util.Set;\n+\n+import timber.log.Timber;\n+\n+/** Supported URIs:\n+ * .../notes (search for notes)\n+ * .../notes/# (direct access to note)\n+ * .../notes/#/data (access to detail data of note)\n+ * .../notes/#/cards (access cards of note)\n+ * .../notes/#/cards/# (access specific card of note)\n+ * .../models (search for models)\n+ * .../models/# (direct access to model)\n+ *\n+ * Note that unlike Android's contact providers:\n+ * <ul>\n+ *     <li>it's not possible to access detail data of more than one note</li>\n+ *     <li>it's not possible to access cards of more than one note</li>\n+ *     <li>it's not possible to access detail data of a note without providing its ID</li>\n+ *     <li>it's not possible to access cards of a note without providing the note's ID</li>\n+ *     <li>detail data of a notes only have fake temporary numeric ID (i.e. column \"_ID\"), this\n+ *         is due to the storage system of the AnkiDatabase.</li>\n+ * </ul>\n+ */\n+public class CardContentProvider extends ContentProvider {\n+\n+    /* URI types */\n+    private static final int NOTES = 1000;\n+    private static final int NOTES_ID = 1001;\n+    private static final int NOTES_ID_DATA = 1002;\n+    private static final int NOTES_ID_CARDS = 1003;\n+    private static final int NOTES_ID_CARDS_ORD = 1004;\n+    private static final int MODELS = 2000;\n+    private static final int MODELS_ID = 2001;\n+\n+    private static final UriMatcher sUriMatcher =\n+            new UriMatcher(UriMatcher.NO_MATCH);\n+\n+    /* Based on proposal from Tim's comment on https://github.com/ankidroid/Anki-Android/pull/725\n+     */\n+    static {\n+        //Here you can see all the URIs at a glance\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"notes\", NOTES);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"notes/#\", NOTES_ID);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"notes/#/data\", NOTES_ID_DATA);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"notes/#/cards\", NOTES_ID_CARDS);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"notes/#/cards/#\", NOTES_ID_CARDS_ORD);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"models\", MODELS);\n+        sUriMatcher.addURI(FlashCardsContract.AUTHORITY, \"models/#\", MODELS_ID);\n+    }\n+\n+    /** The names of the columns returned by this content provider differ slightly from the names\n+     * given of the database columns. This list is used to convert the column names used in a\n+     * projection by the user into DB column names.\n+     *\n+     * This is currently only \"_id\" (projection) vs. \"id\" (Anki DB). But should probably be\n+     * applied to more columns. \"MID\", \"USN\", \"MOD\" are not really user friendly.\n+     */\n+    private static String[] defaultNoteProjectionDBAccess = FlashCardsContract.Note.DEFAULT_PROJECTION.clone();\n+    static{\n+        for(int idx = 0; idx < defaultNoteProjectionDBAccess.length; idx++){\n+            if(defaultNoteProjectionDBAccess[idx].equals(FlashCardsContract.Note._ID)){\n+                defaultNoteProjectionDBAccess[idx] = \"id as _id\";\n+            }\n+        }\n+    }\n+\n+    @Override\n+    public int delete(Uri uri, String selection, String[] selectionArgs) {\n+        throw new UnsupportedOperationException(\"Not yet implemented\");\n+    }\n+\n+    @Override\n+    public String getType(Uri uri) {\n+        //Find out what data the user is requesting\n+        int match = sUriMatcher.match(uri);\n+\n+        switch (match){\n+            case NOTES:\n+                return FlashCardsContract.Note.CONTENT_TYPE;\n+            case NOTES_ID:\n+                return FlashCardsContract.Note.CONTENT_ITEM_TYPE;\n+            case NOTES_ID_DATA:\n+                return FlashCardsContract.Data.CONTENT_TYPE;\n+            case NOTES_ID_CARDS:\n+                return FlashCardsContract.Card.CONTENT_TYPE;\n+            case NOTES_ID_CARDS_ORD:\n+                return FlashCardsContract.Card.CONTENT_ITEM_TYPE;\n+            case MODELS:\n+                return FlashCardsContract.Model.CONTENT_TYPE;\n+            case MODELS_ID:\n+                return FlashCardsContract.Model.CONTENT_ITEM_TYPE;\n+            default:\n+                //Unknown URI type\n+                throw new IllegalArgumentException(\"uri \"+ uri +\" is not supported\");\n+        }\n+    }\n+\n+    @Override\n+    public Uri insert(Uri uri, ContentValues values) {\n+        Timber.d(\"CardContentProvider.insert\");\n+        Collection col = getCol();\n+        if(col==null){\n+            return null;\n+        }\n+\n+        //Find out what data the user is requesting\n+        int match = sUriMatcher.match(uri);\n+\n+        switch (match){\n+            case NOTES: {\n+                /* Insert new note\n+                 */\n+                Long modelId = values.getAsLong(FlashCardsContract.Note.MID);\n+                com.ichi2.libanki.Note newNote = new com.ichi2.libanki.Note(col, col.getModels().get(modelId));\n+                String[] fields = newNote.getFields();\n+                for (int i = 0; i < fields.length; i++){\n+                    newNote.setField(i, \"temp\");\n+                }\n+                int insertCount = col.addNote(newNote);\n+                return Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, Long.toString(newNote.getId()));\n+            }\n+            case NOTES_ID: {\n+                /* Direct access note\n+                 */\n+                throw new IllegalArgumentException(\"Not possible to insert note with specific ID\");\n+            }\n+            case NOTES_ID_DATA:\n+                throw new IllegalArgumentException(\"Not possible to insert note data\");\n+            case NOTES_ID_CARDS:\n+                throw new IllegalArgumentException(\"Not possible to insert cards directly (only through inserting notes)\");\n+            case NOTES_ID_CARDS_ORD:\n+                throw new IllegalArgumentException(\"Not possible to insert cards directly (only through inserting notes)\");\n+            case MODELS:\n+                //TODO: To be implemented\n+                throw new UnsupportedOperationException(\"Not yet implemented\");\n+//                break;\n+            case MODELS_ID:\n+                throw new IllegalArgumentException(\"Not possible to insert model with specific ID\");\n+            default:\n+                //Unknown URI type\n+                throw new IllegalArgumentException(\"uri \"+ uri +\" is not supported\");\n+        }\n+    }\n+\n+    @Override\n+    public boolean onCreate() {\n+        //Initialize content provider on startup.\n+        Timber.d(\"CardContentProvider: onCreate\");\n+\n+        return true;\n+    }\n+\n+    @Override\n+    public Cursor query(Uri uri, String[] projection, String selection,\n+                        String[] selectionArgs, String sortOrder) {\n+        Timber.d(\"CardContentProvider.query\");\n+        Collection col = getCol();\n+        if(col==null){\n+            return null;\n+        }\n+\n+        //Find out what data the user is requesting\n+        int match = sUriMatcher.match(uri);\n+\n+        switch (match){\n+            case NOTES: {\n+                /* Search for notes\n+                 */\n+                //TODO: Allow sort order, then also update description in FlashCardContract\n+                String columnsStr = proj2str(projection);\n+                String query = (selection!=null) ? selection : \"\";\n+                List<Long> noteIds = col.findNotes(query);\n+                if((noteIds != null) && (noteIds.size()>0)) {\n+                    String selectedIds = \"id in \" + Utils.ids2str(noteIds);\n+                    Cursor cur;\n+                    try {\n+                        cur = col.getDb().getDatabase().rawQuery(\"select \" + columnsStr + \" from notes where \" + selectedIds, null);\n+                    } catch (SQLException e) {\n+                        throw new IllegalArgumentException(\"Not possible to query for data for IDs \" +\n+                                selectedIds, e);\n+                    }\n+                    return cur;\n+                }\n+                else{\n+                    return null;\n+                }\n+            }\n+            case NOTES_ID: {\n+                /* Direct access note\n+                 */\n+                long noteId;\n+                try {\n+                    noteId = Long.parseLong(uri.getPathSegments().get(1));\n+                } catch (NumberFormatException e) {\n+                    throw new IllegalArgumentException(\"Invalid ID format\", e);\n+                }\n+                String columnsStr = proj2str(projection);\n+                String selectedIds = \"id = \" + noteId;\n+                Cursor cur;\n+                try {\n+                    cur = col.getDb().getDatabase().rawQuery(\"select \" + columnsStr + \" from notes where \" + selectedIds, null);\n+                } catch (SQLException e) {\n+                    throw new IllegalArgumentException(\"Not possible to query for data for ID \\\"\" +\n+                            noteId + \"\\\"\", e);\n+                }\n+                return cur;\n+            }\n+            case NOTES_ID_DATA: {\n+                /* Direct access note details\n+                 */\n+                Note currentNote = getNoteFromUri(uri, col);\n+                MatrixCursor rv;\n+                String[] columns = ((projection!= null) ? projection : DataColumns.DEFAULT_PROJECTION);\n+                rv = new MatrixCursor(columns, 1);\n+                long id =0;\n+\n+                /* First add the data from all the fields ...\n+                 */\n+                String[][] currentNoteItems = currentNote.items();\n+                for (String[] field : currentNoteItems) {\n+                    id++;\n+                    MatrixCursor.RowBuilder rb = rv.newRow();\n+                    for(String column : columns) {\n+                        if(column.equals(DataColumns._ID)){\n+                            rb.add(id);\n+                        } else if (column.equals(DataColumns.NOTE_ID)) {\n+                            rb.add(currentNote.getId());\n+                        } else if (column.equals(DataColumns.MIMETYPE)) {\n+                            rb.add(FlashCardsContract.Data.Field.CONTENT_ITEM_TYPE);\n+                        } else if (column.equals(Field.FIELD_NAME)) {\n+                            rb.add(field[0]);\n+                        } else if (column.equals(Field.FIELD_CONTENT)) {\n+                            rb.add(field[1]);\n+                        } else {\n+                            throw new UnsupportedOperationException(\"Support for column \\\"\"+ column +\"\\\" is not yet implemented for MIME type \" + Field.CONTENT_ITEM_TYPE);\n+                        }\n+                    }\n+                }\n+\n+                /* ... and then the tags. */\n+                {\n+                    id++;\n+                    MatrixCursor.RowBuilder rb = rv.newRow();\n+                    for(String column : columns) {\n+                        if(column.equals(DataColumns._ID)){\n+                            rb.add(id);\n+                        } else if (column.equals(DataColumns.NOTE_ID)) {\n+                            rb.add(currentNote.getId());\n+                        } else if (column.equals(DataColumns.MIMETYPE)) {\n+                            rb.add(FlashCardsContract.Data.Tags.CONTENT_ITEM_TYPE);\n+                        } else if (column.equals(FlashCardsContract.Data.Tags.TAG_CONTENT)) {\n+                            rb.add(currentNote.stringTags().trim());\n+                        } else if (column.equals(DataColumns.DATA2)) {\n+                            /* Ignore: This column is not used for tags */\n+                        } else {\n+                            throw new UnsupportedOperationException(\"Support for column \\\"\"+ column +\"\\\" is not implemented for MIME type \" + Field.CONTENT_ITEM_TYPE);\n+                        }\n+                    }\n+                }\n+                return rv;\n+            }\n+            case NOTES_ID_CARDS: {\n+                Note currentNote = getNoteFromUri(uri, col);\n+                MatrixCursor rv;\n+                String[] columns = ((projection!= null) ? projection : FlashCardsContract.Card.DEFAULT_PROJECTION);\n+                rv = new MatrixCursor(columns, 1);\n+                for(Card currentCard : currentNote.cards()){\n+                    addCardToCursor(currentCard, rv, col, columns);\n+                }\n+                return rv;\n+            }\n+            case NOTES_ID_CARDS_ORD: {\n+                Card currentCard = getCardFromUri(uri, col);\n+                MatrixCursor rv;\n+                String[] columns = ((projection!= null) ? projection : FlashCardsContract.Card.DEFAULT_PROJECTION);\n+                rv = new MatrixCursor(columns, 1);\n+                addCardToCursor(currentCard, rv, col, columns);\n+                return rv;\n+            }\n+            case MODELS: {\n+                HashMap<Long, JSONObject> models = col.getModels().getModels();\n+                MatrixCursor rv;\n+                String[] columns = ((projection!= null) ? projection : FlashCardsContract.Model.DEFAULT_PROJECTION);\n+                rv = new MatrixCursor(columns, 1);\n+\n+                for (Long modelId : models.keySet()) {\n+                    try {\n+                        addModelToCursor(modelId, models, rv, col, columns);\n+                    } catch (JSONException e) {\n+                        throw new IllegalArgumentException(\"Model \"+modelId+\" is malformed\", e);\n+                    }\n+                }\n+                return rv;\n+            }\n+            case MODELS_ID: {\n+                long modelId;\n+                MatrixCursor rv;\n+                String[] columns = ((projection!= null) ? projection : FlashCardsContract.Model.DEFAULT_PROJECTION);\n+                rv = new MatrixCursor(columns, 1);\n+                try {\n+                    modelId = Long.parseLong(uri.getPathSegments().get(1));\n+                } catch (NumberFormatException e) {\n+                    throw new IllegalArgumentException(\"Invalid ID format\", e);\n+                }\n+                HashMap<Long, JSONObject> models = col.getModels().getModels();\n+                try {\n+                    addModelToCursor(modelId, models, rv, col, columns);\n+                } catch (JSONException e) {\n+                    throw new IllegalArgumentException(\"Model \"+modelId+\" is malformed\", e);\n+                }\n+                return rv;\n+            }\n+            default:\n+                //Unknown URI type\n+                throw new IllegalArgumentException(\"uri \"+ uri +\" is not supported\");\n+        }\n+    }\n+\n+    private void addModelToCursor(Long modelId, HashMap<Long, JSONObject> models, MatrixCursor rv, Collection col, String[] columns) throws JSONException {\n+        JSONObject jsonObject = models.get(modelId);\n+        MatrixCursor.RowBuilder rb = rv.newRow();\n+        for (String column : columns) {\n+            if (column.equals(FlashCardsContract.Model._ID)) {\n+                rb.add(modelId);\n+            } else if (column.equals(FlashCardsContract.Model.NAME)) {\n+                rb.add(jsonObject.getString(\"name\"));\n+            } else if (column.equals(FlashCardsContract.Model.JSONOBJECT)) {\n+                rb.add(jsonObject);\n+            } else {\n+                throw new UnsupportedOperationException(\"Column \\\"\" + column + \"\\\" is unknown\");\n+            }\n+        }\n+\n+    }\n+\n+    private void addCardToCursor(Card currentCard, MatrixCursor rv, Collection col, String[] columns) {\n+        String cardName;\n+        String deckName;\n+        try {\n+            cardName = currentCard.template().getString(\"name\");\n+            deckName = col.getDecks().get(currentCard.getDid()).getString(\"name\");\n+        } catch (JSONException je) {\n+            throw new IllegalArgumentException(\"Card is using an invalid template\", je);\n+        }\n+        String question = currentCard.q();\n+        String answer = currentCard.a();\n+        MatrixCursor.RowBuilder rb = rv.newRow();\n+        for (String column : columns) {\n+            if (column.equals(FlashCardsContract.Card.NOTE_ID)) {\n+                rb.add(currentCard.note().getId());\n+            } else if (column.equals(FlashCardsContract.Card.CARD_ORD)) {\n+                rb.add(currentCard.getOrd());\n+            } else if (column.equals(FlashCardsContract.Card.CARD_NAME)) {\n+                rb.add(cardName);\n+            } else if (column.equals(FlashCardsContract.Card.DECK_NAME)) {\n+                rb.add(deckName);\n+            } else if (column.equals(FlashCardsContract.Card.QUESTION)) {\n+                rb.add(question);\n+            } else if (column.equals(FlashCardsContract.Card.ANSWER)) {\n+                rb.add(answer);\n+            } else {\n+                throw new UnsupportedOperationException(\"Column \\\"\" + column + \"\\\" is unknown\");\n+            }\n+        }\n+    }\n+\n+    private static String proj2str(String[] projection) {\n+        StringBuilder rv = new StringBuilder();\n+        if(projection != null){\n+            for(String column : projection){\n+                int idx = projSearch(FlashCardsContract.Note.DEFAULT_PROJECTION, column);\n+                if(idx>=0){\n+                    rv.append(defaultNoteProjectionDBAccess[idx]);\n+                    rv.append(\",\");\n+                }\n+                else{\n+                    throw new IllegalArgumentException(\"Unknown column \"+column);\n+                }\n+            }\n+        }\n+        else{\n+            for(String column : defaultNoteProjectionDBAccess){\n+                rv.append(column);\n+                rv.append(\",\");\n+            }\n+        }\n+        rv.deleteCharAt(rv.length()-1);\n+        return rv.toString();\n+    }\n+\n+    private static int projSearch(String[] projection, String column) {\n+        for(int i = 0; i < projection.length; i++){\n+            if(projection[i].equals(column)){\n+                return i;\n+            }\n+        }\n+        return -1;\n+    }\n+\n+    @Override\n+    public int update(Uri uri, ContentValues values, String selection,\n+                      String[] selectionArgs) {\n+        Timber.d(\"CardContentProvider.update\");\n+        Collection col = getCol();\n+        if(col==null){\n+            return 0;\n+        }\n+\n+        //Find out what data the user is requesting\n+        int match = sUriMatcher.match(uri);\n+\n+        int updated = 0; //Number of updated entries (return value)\n+        switch (match){\n+            case NOTES:\n+                throw new IllegalArgumentException(\"Not possible to update notes directly (only through data URI)\");\n+            case NOTES_ID:\n+                throw new IllegalArgumentException(\"Not possible to update notes directly (only through data URI)\");\n+            case NOTES_ID_DATA: {\n+                /* Direct access note details\n+                 */\n+                Note currentNote = getNoteFromUri(uri, col);\n+                boolean isField = false;\n+                boolean isTag = false;\n+                String data1 = null;\n+                String data2 = null;\n+                //the key of the ContentValues contains the column name\n+                //the value of the ContentValues contains the row value.\n+                Set<Map.Entry<String, Object>> valueSet = values.valueSet();\n+                for (Map.Entry<String, Object> entry : valueSet) {\n+                    String key = entry.getKey();\n+                    if (key.equals(DataColumns.MIMETYPE)) {\n+                        isField = values.getAsString(key).equals(Field.CONTENT_ITEM_TYPE);\n+                        isTag = values.getAsString(key).equals(FlashCardsContract.Data.Tags.CONTENT_ITEM_TYPE);\n+                    } else if (key.equals(DataColumns.DATA1)) {\n+                        data1 = values.getAsString(key);\n+                    } else if (key.equals(DataColumns.DATA2)) {\n+                        data2 = values.getAsString(key);\n+                    } else {\n+                        //Unknown column\n+                        throw new IllegalArgumentException(\"Unknown/unsupported column: \"+key);\n+                    }\n+                }\n+\n+                /* now update the note\n+                 */\n+                if ((isField) && (data1 != null) && (data2 != null)) {\n+                    Timber.d(\"CardContentProvider: Saving note after field update...\");\n+                    currentNote.setitem(data1, data2);\n+                    currentNote.flush();\n+                    updated++;\n+                } else if((isTag) && (data1 != null)) {\n+                    Timber.d(\"CardContentProvider: Saving note after tags update...\");\n+                    currentNote.setTagsFromStr(data1);\n+                    currentNote.flush();\n+                    updated++;\n+                } else {\n+                    //User tries an operation that is not (yet?) supported.\n+                    throw new IllegalArgumentException(\"Currently only updates of fields are supported\");\n+                }\n+                break;\n+            }\n+            case NOTES_ID_CARDS:\n+                //TODO: To be implemented\n+                throw new UnsupportedOperationException(\"Not yet implemented\");\n+//                break;\n+            case NOTES_ID_CARDS_ORD: {\n+                Card currentCard = getCardFromUri(uri, col);\n+                boolean isDeckUpdate = false;\n+                String newDeckName = null;\n+                //the key of the ContentValues contains the column name\n+                //the value of the ContentValues contains the row value.\n+                Set<Map.Entry<String, Object>> valueSet = values.valueSet();\n+                for (Map.Entry<String, Object> entry : valueSet) {\n+                    //Only updates on deck name is supported\n+                    String key = entry.getKey();\n+                    isDeckUpdate = key.equals(FlashCardsContract.Card.DECK_NAME);\n+                    newDeckName = values.getAsString(key);\n+                }\n+\n+                /* now update the card\n+                 */\n+                if ((isDeckUpdate) && (newDeckName != null)) {\n+                    Timber.d(\"CardContentProvider: Moving card to other deck...\");\n+                    long did = col.getDecks().id(newDeckName); //create new deck if not exists\n+                    col.getDecks().flush();\n+                    currentCard.setDid(did);\n+                    currentCard.flush();\n+                    updated++;\n+                } else {\n+                    //User tries an operation that is not (yet?) supported.\n+                    throw new IllegalArgumentException(\"Currently only updates of decks are supported\");\n+                }\n+                break;\n+            }\n+            case MODELS:\n+                throw new UnsupportedOperationException(\"Not yet implemented\");\n+//                break;","path":"AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.java","position":540,"original_position":540,"commit_id":"9c74792cb4f8a010cc6a47a915691f0b5e996ed6","original_commit_id":"9c74792cb4f8a010cc6a47a915691f0b5e996ed6","user":{"login":"federvieh","id":5159306,"avatar_url":"https://avatars.githubusercontent.com/u/5159306?v=3","gravatar_id":"","url":"https://api.github.com/users/federvieh","html_url":"https://github.com/federvieh","followers_url":"https://api.github.com/users/federvieh/followers","following_url":"https://api.github.com/users/federvieh/following{/other_user}","gists_url":"https://api.github.com/users/federvieh/gists{/gist_id}","starred_url":"https://api.github.com/users/federvieh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/federvieh/subscriptions","organizations_url":"https://api.github.com/users/federvieh/orgs","repos_url":"https://api.github.com/users/federvieh/repos","events_url":"https://api.github.com/users/federvieh/events{/privacy}","received_events_url":"https://api.github.com/users/federvieh/received_events","type":"User","site_admin":false},"body":"As I wrote earlier, I think someone (maybe me) should implement `insert()` and `update()` for models. So, this is a placeholder/reminder and I prefer to keep it as it is unless it prevents you from merging.","created_at":"2015-03-07T08:25:00Z","updated_at":"2015-03-07T08:25:00Z","html_url":"https://github.com/ankidroid/Anki-Android/pull/725#discussion_r25994505","pull_request_url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725","_links":{"self":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/comments/25994505"},"html":{"href":"https://github.com/ankidroid/Anki-Android/pull/725#discussion_r25994505"},"pull_request":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725"}}},"pull_request":{"url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725","id":28531848,"html_url":"https://github.com/ankidroid/Anki-Android/pull/725","diff_url":"https://github.com/ankidroid/Anki-Android/pull/725.diff","patch_url":"https://github.com/ankidroid/Anki-Android/pull/725.patch","issue_url":"https://api.github.com/repos/ankidroid/Anki-Android/issues/725","number":725,"state":"open","locked":false,"title":"Contentprovider","user":{"login":"federvieh","id":5159306,"avatar_url":"https://avatars.githubusercontent.com/u/5159306?v=3","gravatar_id":"","url":"https://api.github.com/users/federvieh","html_url":"https://github.com/federvieh","followers_url":"https://api.github.com/users/federvieh/followers","following_url":"https://api.github.com/users/federvieh/following{/other_user}","gists_url":"https://api.github.com/users/federvieh/gists{/gist_id}","starred_url":"https://api.github.com/users/federvieh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/federvieh/subscriptions","organizations_url":"https://api.github.com/users/federvieh/orgs","repos_url":"https://api.github.com/users/federvieh/repos","events_url":"https://api.github.com/users/federvieh/events{/privacy}","received_events_url":"https://api.github.com/users/federvieh/received_events","type":"User","site_admin":false},"body":"This is a pull request for a content provider as discussed here:\r\nhttps://groups.google.com/forum/#!msg/anki-android/SitMpj7J6fw/swqcuJKcld8J\r\n\r\nIt currently supports getting the unprocessed data from the Anki DB using the following URI:\r\ncontent://com.ichi2.anki.flashcard/flashcards\r\n\r\nand fields and cards/decks using\r\n\r\ncontent://com.ichi2.anki.flashcard/flashcards/<note_id>/data\r\n\r\nThe contract is described in FlashCardsContract.java. I release this file into the public domain so that it can be used by any third party software without any attribution or royalty. I.e. this file (and this file only) is not licensed under GPL.\r\n\r\nThe content provider currently only supports notes/cards, but not models.\r\n\r\nIt also only supports the query() function, no insert() or update() yet.\r\n\r\nFor update() on fields I intended to use the same mechanism as NoteEditor, but there is some kind of check that prevents updates from any other Thread than the main thread.\r\n\r\nThe first intention of this PR is to discuss if the proposed data structure is acceptable/understandable for the AnkiDroid developers and to discuss implementation details such as the above (concerning the main thread problem). Of course, I hope that it will later merged into the main branch.","created_at":"2015-02-03T07:27:12Z","updated_at":"2015-03-07T08:25:00Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f8f539ac109cb06098501764e1c9f17cc35f20cf","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725/commits","review_comments_url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725/comments","review_comment_url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ankidroid/Anki-Android/issues/725/comments","statuses_url":"https://api.github.com/repos/ankidroid/Anki-Android/statuses/9c74792cb4f8a010cc6a47a915691f0b5e996ed6","head":{"label":"federvieh:contentprovider","ref":"contentprovider","sha":"9c74792cb4f8a010cc6a47a915691f0b5e996ed6","user":{"login":"federvieh","id":5159306,"avatar_url":"https://avatars.githubusercontent.com/u/5159306?v=3","gravatar_id":"","url":"https://api.github.com/users/federvieh","html_url":"https://github.com/federvieh","followers_url":"https://api.github.com/users/federvieh/followers","following_url":"https://api.github.com/users/federvieh/following{/other_user}","gists_url":"https://api.github.com/users/federvieh/gists{/gist_id}","starred_url":"https://api.github.com/users/federvieh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/federvieh/subscriptions","organizations_url":"https://api.github.com/users/federvieh/orgs","repos_url":"https://api.github.com/users/federvieh/repos","events_url":"https://api.github.com/users/federvieh/events{/privacy}","received_events_url":"https://api.github.com/users/federvieh/received_events","type":"User","site_admin":false},"repo":{"id":28517534,"name":"Anki-Android","full_name":"federvieh/Anki-Android","owner":{"login":"federvieh","id":5159306,"avatar_url":"https://avatars.githubusercontent.com/u/5159306?v=3","gravatar_id":"","url":"https://api.github.com/users/federvieh","html_url":"https://github.com/federvieh","followers_url":"https://api.github.com/users/federvieh/followers","following_url":"https://api.github.com/users/federvieh/following{/other_user}","gists_url":"https://api.github.com/users/federvieh/gists{/gist_id}","starred_url":"https://api.github.com/users/federvieh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/federvieh/subscriptions","organizations_url":"https://api.github.com/users/federvieh/orgs","repos_url":"https://api.github.com/users/federvieh/repos","events_url":"https://api.github.com/users/federvieh/events{/privacy}","received_events_url":"https://api.github.com/users/federvieh/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/federvieh/Anki-Android","description":"Anki on Android","fork":true,"url":"https://api.github.com/repos/federvieh/Anki-Android","forks_url":"https://api.github.com/repos/federvieh/Anki-Android/forks","keys_url":"https://api.github.com/repos/federvieh/Anki-Android/keys{/key_id}","collaborators_url":"https://api.github.com/repos/federvieh/Anki-Android/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/federvieh/Anki-Android/teams","hooks_url":"https://api.github.com/repos/federvieh/Anki-Android/hooks","issue_events_url":"https://api.github.com/repos/federvieh/Anki-Android/issues/events{/number}","events_url":"https://api.github.com/repos/federvieh/Anki-Android/events","assignees_url":"https://api.github.com/repos/federvieh/Anki-Android/assignees{/user}","branches_url":"https://api.github.com/repos/federvieh/Anki-Android/branches{/branch}","tags_url":"https://api.github.com/repos/federvieh/Anki-Android/tags","blobs_url":"https://api.github.com/repos/federvieh/Anki-Android/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/federvieh/Anki-Android/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/federvieh/Anki-Android/git/refs{/sha}","trees_url":"https://api.github.com/repos/federvieh/Anki-Android/git/trees{/sha}","statuses_url":"https://api.github.com/repos/federvieh/Anki-Android/statuses/{sha}","languages_url":"https://api.github.com/repos/federvieh/Anki-Android/languages","stargazers_url":"https://api.github.com/repos/federvieh/Anki-Android/stargazers","contributors_url":"https://api.github.com/repos/federvieh/Anki-Android/contributors","subscribers_url":"https://api.github.com/repos/federvieh/Anki-Android/subscribers","subscription_url":"https://api.github.com/repos/federvieh/Anki-Android/subscription","commits_url":"https://api.github.com/repos/federvieh/Anki-Android/commits{/sha}","git_commits_url":"https://api.github.com/repos/federvieh/Anki-Android/git/commits{/sha}","comments_url":"https://api.github.com/repos/federvieh/Anki-Android/comments{/number}","issue_comment_url":"https://api.github.com/repos/federvieh/Anki-Android/issues/comments{/number}","contents_url":"https://api.github.com/repos/federvieh/Anki-Android/contents/{+path}","compare_url":"https://api.github.com/repos/federvieh/Anki-Android/compare/{base}...{head}","merges_url":"https://api.github.com/repos/federvieh/Anki-Android/merges","archive_url":"https://api.github.com/repos/federvieh/Anki-Android/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/federvieh/Anki-Android/downloads","issues_url":"https://api.github.com/repos/federvieh/Anki-Android/issues{/number}","pulls_url":"https://api.github.com/repos/federvieh/Anki-Android/pulls{/number}","milestones_url":"https://api.github.com/repos/federvieh/Anki-Android/milestones{/number}","notifications_url":"https://api.github.com/repos/federvieh/Anki-Android/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/federvieh/Anki-Android/labels{/name}","releases_url":"https://api.github.com/repos/federvieh/Anki-Android/releases{/id}","created_at":"2014-12-26T18:03:13Z","updated_at":"2015-03-04T20:24:25Z","pushed_at":"2015-03-05T21:08:16Z","git_url":"git://github.com/federvieh/Anki-Android.git","ssh_url":"git@github.com:federvieh/Anki-Android.git","clone_url":"https://github.com/federvieh/Anki-Android.git","svn_url":"https://github.com/federvieh/Anki-Android","homepage":"https://code.google.com/p/ankidroid/","size":64432,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop"}},"base":{"label":"ankidroid:develop","ref":"develop","sha":"9eef614822c6f27458a5d593fa0a5b9bb5bdd9d7","user":{"login":"ankidroid","id":3320903,"avatar_url":"https://avatars.githubusercontent.com/u/3320903?v=3","gravatar_id":"","url":"https://api.github.com/users/ankidroid","html_url":"https://github.com/ankidroid","followers_url":"https://api.github.com/users/ankidroid/followers","following_url":"https://api.github.com/users/ankidroid/following{/other_user}","gists_url":"https://api.github.com/users/ankidroid/gists{/gist_id}","starred_url":"https://api.github.com/users/ankidroid/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ankidroid/subscriptions","organizations_url":"https://api.github.com/users/ankidroid/orgs","repos_url":"https://api.github.com/users/ankidroid/repos","events_url":"https://api.github.com/users/ankidroid/events{/privacy}","received_events_url":"https://api.github.com/users/ankidroid/received_events","type":"Organization","site_admin":false},"repo":{"id":7716883,"name":"Anki-Android","full_name":"ankidroid/Anki-Android","owner":{"login":"ankidroid","id":3320903,"avatar_url":"https://avatars.githubusercontent.com/u/3320903?v=3","gravatar_id":"","url":"https://api.github.com/users/ankidroid","html_url":"https://github.com/ankidroid","followers_url":"https://api.github.com/users/ankidroid/followers","following_url":"https://api.github.com/users/ankidroid/following{/other_user}","gists_url":"https://api.github.com/users/ankidroid/gists{/gist_id}","starred_url":"https://api.github.com/users/ankidroid/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ankidroid/subscriptions","organizations_url":"https://api.github.com/users/ankidroid/orgs","repos_url":"https://api.github.com/users/ankidroid/repos","events_url":"https://api.github.com/users/ankidroid/events{/privacy}","received_events_url":"https://api.github.com/users/ankidroid/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ankidroid/Anki-Android","description":"Anki on Android","fork":true,"url":"https://api.github.com/repos/ankidroid/Anki-Android","forks_url":"https://api.github.com/repos/ankidroid/Anki-Android/forks","keys_url":"https://api.github.com/repos/ankidroid/Anki-Android/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ankidroid/Anki-Android/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ankidroid/Anki-Android/teams","hooks_url":"https://api.github.com/repos/ankidroid/Anki-Android/hooks","issue_events_url":"https://api.github.com/repos/ankidroid/Anki-Android/issues/events{/number}","events_url":"https://api.github.com/repos/ankidroid/Anki-Android/events","assignees_url":"https://api.github.com/repos/ankidroid/Anki-Android/assignees{/user}","branches_url":"https://api.github.com/repos/ankidroid/Anki-Android/branches{/branch}","tags_url":"https://api.github.com/repos/ankidroid/Anki-Android/tags","blobs_url":"https://api.github.com/repos/ankidroid/Anki-Android/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ankidroid/Anki-Android/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ankidroid/Anki-Android/git/refs{/sha}","trees_url":"https://api.github.com/repos/ankidroid/Anki-Android/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ankidroid/Anki-Android/statuses/{sha}","languages_url":"https://api.github.com/repos/ankidroid/Anki-Android/languages","stargazers_url":"https://api.github.com/repos/ankidroid/Anki-Android/stargazers","contributors_url":"https://api.github.com/repos/ankidroid/Anki-Android/contributors","subscribers_url":"https://api.github.com/repos/ankidroid/Anki-Android/subscribers","subscription_url":"https://api.github.com/repos/ankidroid/Anki-Android/subscription","commits_url":"https://api.github.com/repos/ankidroid/Anki-Android/commits{/sha}","git_commits_url":"https://api.github.com/repos/ankidroid/Anki-Android/git/commits{/sha}","comments_url":"https://api.github.com/repos/ankidroid/Anki-Android/comments{/number}","issue_comment_url":"https://api.github.com/repos/ankidroid/Anki-Android/issues/comments{/number}","contents_url":"https://api.github.com/repos/ankidroid/Anki-Android/contents/{+path}","compare_url":"https://api.github.com/repos/ankidroid/Anki-Android/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ankidroid/Anki-Android/merges","archive_url":"https://api.github.com/repos/ankidroid/Anki-Android/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ankidroid/Anki-Android/downloads","issues_url":"https://api.github.com/repos/ankidroid/Anki-Android/issues{/number}","pulls_url":"https://api.github.com/repos/ankidroid/Anki-Android/pulls{/number}","milestones_url":"https://api.github.com/repos/ankidroid/Anki-Android/milestones{/number}","notifications_url":"https://api.github.com/repos/ankidroid/Anki-Android/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ankidroid/Anki-Android/labels{/name}","releases_url":"https://api.github.com/repos/ankidroid/Anki-Android/releases{/id}","created_at":"2013-01-20T13:55:53Z","updated_at":"2015-03-04T08:04:47Z","pushed_at":"2015-03-04T08:04:43Z","git_url":"git://github.com/ankidroid/Anki-Android.git","ssh_url":"git@github.com:ankidroid/Anki-Android.git","clone_url":"https://github.com/ankidroid/Anki-Android.git","svn_url":"https://github.com/ankidroid/Anki-Android","homepage":"https://code.google.com/p/ankidroid/","size":123891,"stargazers_count":234,"watchers_count":234,"language":"Java","has_issues":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":294,"mirror_url":null,"open_issues_count":11,"forks":294,"open_issues":11,"watchers":234,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725"},"html":{"href":"https://github.com/ankidroid/Anki-Android/pull/725"},"issue":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/issues/725"},"comments":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/issues/725/comments"},"review_comments":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725/comments"},"review_comment":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/pulls/725/commits"},"statuses":{"href":"https://api.github.com/repos/ankidroid/Anki-Android/statuses/9c74792cb4f8a010cc6a47a915691f0b5e996ed6"}}}}