Java tutorial
/*************************************************************************************** * Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.anki; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteStatement; import android.util.Log; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.sql.Date; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; public class SyncClient { private enum Keys { models, facts, cards, media }; /** * Constants used on the multipart message */ private static final String MIME_BOUNDARY = "Anki-sync-boundary"; private static final String END = "\r\n"; private static final String TWO_HYPHENS = "--"; private Deck mDeck; private AnkiDroidProxy mServer; private double mLocalTime; private double mRemoteTime; public SyncClient(Deck deck) { mDeck = deck; mServer = null; mLocalTime = 0; mRemoteTime = 0; } public AnkiDroidProxy getServer() { return mServer; } public void setServer(AnkiDroidProxy server) { mServer = server; } public void setDeck(Deck deck) { mDeck = deck; } /** * Anki Desktop -> libanki/anki/sync.py, prepareSync * * @return */ public boolean prepareSync() { Log.i(AnkiDroidApp.TAG, "prepareSync = " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync())); mLocalTime = mDeck.getModified(); mRemoteTime = mServer.modified(); Log.i(AnkiDroidApp.TAG, "localTime = " + mLocalTime); Log.i(AnkiDroidApp.TAG, "remoteTime = " + mRemoteTime); if (mLocalTime == mRemoteTime) { return false; } double l = mDeck.getLastSync(); Log.i(AnkiDroidApp.TAG, "lastSync local = " + String.format(Utils.ENGLISH_LOCALE, "%f", l)); double r = mServer.lastSync(); Log.i(AnkiDroidApp.TAG, "lastSync remote = " + String.format(Utils.ENGLISH_LOCALE, "%f", r)); if (l != r) { mDeck.setLastSync(java.lang.Math.min(l, r) - 600); Log.i(AnkiDroidApp.TAG, "deck.lastSync = min(l,r) - 600"); } else { mDeck.setLastSync(l); } Log.i(AnkiDroidApp.TAG, "deck.lastSync = " + mDeck.getLastSync()); return true; } public JSONArray summaries() { Log.i(AnkiDroidApp.TAG, "summaries = " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync())); JSONArray summaries = new JSONArray(); summaries.put(summary(mDeck.getLastSync())); summaries.put(mServer.summary(mDeck.getLastSync())); return summaries; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - summary * * @param lastSync */ public JSONObject summary(double lastSync) { Log.i(AnkiDroidApp.TAG, "Summary Local"); mDeck.setLastSync(lastSync); mDeck.commitToDB(); AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); String lastSyncString = String.format(Utils.ENGLISH_LOCALE, "%f", lastSync); // Cards JSONArray cards = cursorToJSONArray(ankiDB.getDatabase() .rawQuery("SELECT id, modified FROM cards WHERE modified > " + lastSyncString, null)); // Cards - delcards JSONArray delcards = cursorToJSONArray(ankiDB.getDatabase().rawQuery( "SELECT cardId, deletedTime FROM cardsDeleted WHERE deletedTime > " + lastSyncString, null)); // Facts JSONArray facts = cursorToJSONArray(ankiDB.getDatabase() .rawQuery("SELECT id, modified FROM facts WHERE modified > " + lastSyncString, null)); // Facts - delfacts JSONArray delfacts = cursorToJSONArray(ankiDB.getDatabase().rawQuery( "SELECT factId, deletedTime FROM factsDeleted WHERE deletedTime > " + lastSyncString, null)); // Models JSONArray models = cursorToJSONArray(ankiDB.getDatabase() .rawQuery("SELECT id, modified FROM models WHERE modified > " + lastSyncString, null)); // Models - delmodels JSONArray delmodels = cursorToJSONArray(ankiDB.getDatabase().rawQuery( "SELECT modelId, deletedTime FROM modelsDeleted WHERE deletedTime > " + lastSyncString, null)); // Media JSONArray media = cursorToJSONArray(ankiDB.getDatabase() .rawQuery("SELECT id, created FROM media WHERE created > " + lastSyncString, null)); // Media - delmedia JSONArray delmedia = cursorToJSONArray(ankiDB.getDatabase().rawQuery( "SELECT mediaId, deletedTime FROM mediaDeleted WHERE deletedTime > " + lastSyncString, null)); JSONObject summary = new JSONObject(); try { summary.put("cards", cards); summary.put("delcards", delcards); summary.put("facts", facts); summary.put("delfacts", delfacts); summary.put("models", models); summary.put("delmodels", delmodels); summary.put("media", media); summary.put("delmedia", delmedia); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } Log.i(AnkiDroidApp.TAG, "Summary Local = "); Utils.printJSONObject(summary, false); return summary; } private JSONArray cursorToJSONArray(Cursor cursor) { JSONArray jsonArray = new JSONArray(); while (cursor.moveToNext()) { JSONArray element = new JSONArray(); try { element.put(cursor.getLong(0)); element.put(cursor.getDouble(1)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } jsonArray.put(element); } cursor.close(); return jsonArray; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - genPayload */ public JSONObject genPayload(JSONArray summaries) { // Log.i(AnkiDroidApp.TAG, "genPayload"); // Ensure global stats are available (queue may not be built) preSyncRefresh(); JSONObject payload = new JSONObject(); Keys[] keys = Keys.values(); for (int i = 0; i < keys.length; i++) { // Log.i(AnkiDroidApp.TAG, "Key " + keys[i].name()); String key = keys[i].name(); try { // Handle models, facts, cards and media JSONArray diff = diffSummary((JSONObject) summaries.get(0), (JSONObject) summaries.get(1), key); payload.put("added-" + key, getObjsFromKey((JSONArray) diff.get(0), key)); payload.put("deleted-" + key, diff.get(1)); payload.put("missing-" + key, diff.get(2)); deleteObjsFromKey((JSONArray) diff.get(3), key); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } // If the last modified deck was the local one, handle the remainder if (mLocalTime > mRemoteTime) { try { payload.put("deck", bundleDeck()); payload.put("stats", bundleStats()); payload.put("history", bundleHistory()); payload.put("sources", bundleSources()); mDeck.setLastSync(mDeck.getModified()); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } Log.i(AnkiDroidApp.TAG, "Payload ="); Utils.printJSONObject(payload, true); return payload; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - payloadChanges */ private Object[] payloadChanges(JSONObject payload) { Object[] h = new Object[8]; try { h[0] = payload.getJSONObject("added-facts").getJSONArray("facts").length(); h[1] = payload.getJSONArray("missing-facts").length(); h[2] = payload.getJSONArray("added-cards").length(); h[3] = payload.getJSONArray("missing-cards").length(); h[4] = payload.getJSONArray("added-models").length(); h[5] = payload.getJSONArray("missing-models").length(); if (mLocalTime > mRemoteTime) { h[6] = "all"; h[7] = 0; } else { h[6] = 0; h[7] = "all"; } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } return h; } public String payloadChangeReport(JSONObject payload) { return AnkiDroidApp.getAppResources().getString(R.string.change_report_format, payloadChanges(payload)); } public void applyPayloadReply(JSONObject payloadReply) throws JSONException { Log.i(AnkiDroidApp.TAG, "applyPayloadReply"); Keys[] keys = Keys.values(); for (int i = 0; i < keys.length; i++) { String key = keys[i].name(); updateObjsFromKey(payloadReply, key); // JSONObject crasher = null; // crasher.get("nothing"); } try { if (!payloadReply.isNull("deck")) { updateDeck(payloadReply.getJSONObject("deck")); updateStats(payloadReply.getJSONObject("stats")); updateHistory(payloadReply.getJSONArray("history")); if (!payloadReply.isNull("sources")) { updateSources(payloadReply.getJSONArray("sources")); } } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } mDeck.commitToDB(); // Rebuild priorities on client // Get card ids try { JSONArray cards = payloadReply.getJSONArray("added-cards"); int len = cards.length(); long[] cardIds = new long[len]; for (int i = 0; i < len; i++) { cardIds[i] = cards.getJSONArray(i).getLong(0); } // TODO: updateCardTags rebuildPriorities(cardIds); // Rebuild due counts mDeck.rebuildCounts(false); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - preSyncRefresh */ private void preSyncRefresh() { Stats.globalStats(mDeck); } private void rebuildPriorities(long[] cardIds) { try { // TODO: Implement updateAllPriorities // deck.updateAllPriorities(true, false); mDeck.updatePriorities(cardIds, null, false); } catch (SQLException e) { Log.e(AnkiDroidApp.TAG, "SQLException e = " + e.getMessage()); } } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - diffSummary */ private JSONArray diffSummary(JSONObject summaryLocal, JSONObject summaryServer, String key) { JSONArray locallyEdited = new JSONArray(); JSONArray locallyDeleted = new JSONArray(); JSONArray remotelyEdited = new JSONArray(); JSONArray remotelyDeleted = new JSONArray(); Log.i(AnkiDroidApp.TAG, "\ndiffSummary - Key = " + key); Log.i(AnkiDroidApp.TAG, "\nSummary local = "); Utils.printJSONObject(summaryLocal, false); Log.i(AnkiDroidApp.TAG, "\nSummary server = "); Utils.printJSONObject(summaryServer, false); // Hash of all modified ids HashSet<Long> ids = new HashSet<Long>(); try { // Build a hash (id item key, modification time) of the modifications on server (null -> deleted) HashMap<Long, Double> remoteMod = new HashMap<Long, Double>(); putExistingItems(ids, remoteMod, summaryServer.getJSONArray(key)); HashMap<Long, Double> rdeletedIds = putDeletedItems(ids, remoteMod, summaryServer.getJSONArray("del" + key)); // Build a hash (id item, modification time) of the modifications on client (null -> deleted) HashMap<Long, Double> localMod = new HashMap<Long, Double>(); putExistingItems(ids, localMod, summaryLocal.getJSONArray(key)); HashMap<Long, Double> ldeletedIds = putDeletedItems(ids, localMod, summaryLocal.getJSONArray("del" + key)); Iterator<Long> idsIterator = ids.iterator(); while (idsIterator.hasNext()) { Long id = idsIterator.next(); Double localModTime = localMod.get(id); Double remoteModTime = remoteMod.get(id); Log.i(AnkiDroidApp.TAG, "\nid = " + id + ", localModTime = " + localModTime + ", remoteModTime = " + remoteModTime); // Changed/Existing on both sides if (localModTime != null && remoteModTime != null) { Log.i(AnkiDroidApp.TAG, "localModTime not null AND remoteModTime not null"); if (localModTime < remoteModTime) { Log.i(AnkiDroidApp.TAG, "Remotely edited"); remotelyEdited.put(id); } else if (localModTime > remoteModTime) { Log.i(AnkiDroidApp.TAG, "Locally edited"); locallyEdited.put(id); } } // If it's missing on server or newer here, sync else if (localModTime != null && remoteModTime == null) { Log.i(AnkiDroidApp.TAG, "localModTime not null AND remoteModTime null"); if (!rdeletedIds.containsKey(id) || rdeletedIds.get(id) < localModTime) { Log.i(AnkiDroidApp.TAG, "Locally edited"); locallyEdited.put(id); } else { Log.i(AnkiDroidApp.TAG, "Remotely deleted"); remotelyDeleted.put(id); } } // If it's missing locally or newer there, sync else if (remoteModTime != null && localModTime == null) { Log.i(AnkiDroidApp.TAG, "remoteModTime not null AND localModTime null"); if (!ldeletedIds.containsKey(id) || ldeletedIds.get(id) < remoteModTime) { Log.i(AnkiDroidApp.TAG, "Remotely edited"); remotelyEdited.put(id); } else { Log.i(AnkiDroidApp.TAG, "Locally deleted"); locallyDeleted.put(id); } } // Deleted or not modified in both sides else { Log.i(AnkiDroidApp.TAG, "localModTime null AND remoteModTime null"); if (ldeletedIds.containsKey(id) && !rdeletedIds.containsKey(id)) { Log.i(AnkiDroidApp.TAG, "Locally deleted"); locallyDeleted.put(id); } else if (rdeletedIds.containsKey(id) && !ldeletedIds.containsKey(id)) { Log.i(AnkiDroidApp.TAG, "Remotely deleted"); remotelyDeleted.put(id); } } } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } JSONArray diff = new JSONArray(); diff.put(locallyEdited); diff.put(locallyDeleted); diff.put(remotelyEdited); diff.put(remotelyDeleted); return diff; } private void putExistingItems(HashSet<Long> ids, HashMap<Long, Double> dictExistingItems, JSONArray existingItems) { int nbItems = existingItems.length(); for (int i = 0; i < nbItems; i++) { try { JSONArray itemModified = existingItems.getJSONArray(i); Long idItem = itemModified.getLong(0); Double modTimeItem = itemModified.getDouble(1); dictExistingItems.put(idItem, modTimeItem); ids.add(idItem); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } } private HashMap<Long, Double> putDeletedItems(HashSet<Long> ids, HashMap<Long, Double> dictDeletedItems, JSONArray deletedItems) { HashMap<Long, Double> deletedIds = new HashMap<Long, Double>(); int nbItems = deletedItems.length(); for (int i = 0; i < nbItems; i++) { try { JSONArray itemModified = deletedItems.getJSONArray(i); Long idItem = itemModified.getLong(0); Double modTimeItem = itemModified.getDouble(1); dictDeletedItems.put(idItem, null); deletedIds.put(idItem, modTimeItem); ids.add(idItem); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } return deletedIds; } private Object getObjsFromKey(JSONArray ids, String key) { if ("models".equalsIgnoreCase(key)) { return getModels(ids); } else if ("facts".equalsIgnoreCase(key)) { return getFacts(ids); } else if ("cards".equalsIgnoreCase(key)) { return getCards(ids); } else if ("media".equalsIgnoreCase(key)) { return getMedia(ids); } return null; } private void deleteObjsFromKey(JSONArray ids, String key) throws JSONException { if ("models".equalsIgnoreCase(key)) { deleteModels(ids); } else if ("facts".equalsIgnoreCase(key)) { mDeck.deleteFacts(Utils.jsonArrayToListString(ids)); } else if ("cards".equalsIgnoreCase(key)) { mDeck.deleteCards(Utils.jsonArrayToListString(ids)); } else if ("media".equalsIgnoreCase(key)) { deleteMedia(ids); } } private void updateObjsFromKey(JSONObject payloadReply, String key) { try { if ("models".equalsIgnoreCase(key)) { Log.i(AnkiDroidApp.TAG, "updateModels"); updateModels(payloadReply.getJSONArray("added-models")); } else if ("facts".equalsIgnoreCase(key)) { Log.i(AnkiDroidApp.TAG, "updateFacts"); updateFacts(payloadReply.getJSONObject("added-facts")); } else if ("cards".equalsIgnoreCase(key)) { Log.i(AnkiDroidApp.TAG, "updateCards"); updateCards(payloadReply.getJSONArray("added-cards")); } else if ("media".equalsIgnoreCase(key)) { Log.i(AnkiDroidApp.TAG, "updateMedia"); updateMedia(payloadReply.getJSONArray("added-media")); } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } /** * Models */ // TODO: Include the case with updateModified /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getModels * * @param ids * @return */ private JSONArray getModels(JSONArray ids)// , boolean updateModified) { JSONArray models = new JSONArray(); int nbIds = ids.length(); for (int i = 0; i < nbIds; i++) { try { models.put(bundleModel(ids.getLong(i))); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } return models; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - bundleModel * * @param id * @return */ private JSONObject bundleModel(Long id)// , boolean updateModified { JSONObject model = new JSONObject(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM models WHERE id = " + id, null); if (cursor.moveToFirst()) { try { model.put("id", cursor.getLong(0)); model.put("deckId", cursor.getInt(1)); model.put("created", cursor.getDouble(2)); model.put("modified", cursor.getDouble(3)); model.put("tags", cursor.getString(4)); model.put("name", cursor.getString(5)); model.put("description", cursor.getString(6)); model.put("features", cursor.getDouble(7)); model.put("spacing", cursor.getDouble(8)); model.put("initialSpacing", cursor.getDouble(9)); model.put("source", cursor.getInt(10)); model.put("fieldModels", bundleFieldModels(id)); model.put("cardModels", bundleCardModels(id)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); Log.i(AnkiDroidApp.TAG, "Model = "); Utils.printJSONObject(model, false); return model; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - bundleFieldModel * * @param id * @return */ private JSONArray bundleFieldModels(Long id) { JSONArray fieldModels = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM fieldModels WHERE modelId = " + id, null); while (cursor.moveToNext()) { JSONObject fieldModel = new JSONObject(); try { fieldModel.put("id", cursor.getLong(0)); fieldModel.put("ordinal", cursor.getInt(1)); fieldModel.put("modelId", cursor.getLong(2)); fieldModel.put("name", cursor.getString(3)); fieldModel.put("description", cursor.getString(4)); fieldModel.put("features", cursor.getString(5)); fieldModel.put("required", cursor.getString(6)); fieldModel.put("unique", cursor.getString(7)); fieldModel.put("numeric", cursor.getString(8)); fieldModel.put("quizFontFamily", cursor.getString(9)); fieldModel.put("quizFontSize", cursor.getInt(10)); fieldModel.put("quizFontColour", cursor.getString(11)); fieldModel.put("editFontFamily", cursor.getString(12)); fieldModel.put("editFontSize", cursor.getInt(13)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } fieldModels.put(fieldModel); } cursor.close(); return fieldModels; } private JSONArray bundleCardModels(Long id) { JSONArray cardModels = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM cardModels WHERE modelId = " + id, null); while (cursor.moveToNext()) { JSONObject cardModel = new JSONObject(); try { cardModel.put("id", cursor.getLong(0)); cardModel.put("ordinal", cursor.getInt(1)); cardModel.put("modelId", cursor.getLong(2)); cardModel.put("name", cursor.getString(3)); cardModel.put("description", cursor.getString(4)); cardModel.put("active", cursor.getString(5)); cardModel.put("qformat", cursor.getString(6)); cardModel.put("aformat", cursor.getString(7)); cardModel.put("lformat", cursor.getString(8)); cardModel.put("qedformat", cursor.getString(9)); cardModel.put("aedformat", cursor.getString(10)); cardModel.put("questionInAnswer", cursor.getString(11)); cardModel.put("questionFontFamily", cursor.getString(12)); cardModel.put("questionFontSize ", cursor.getInt(13)); cardModel.put("questionFontColour", cursor.getString(14)); cardModel.put("questionAlign", cursor.getInt(15)); cardModel.put("answerFontFamily", cursor.getString(16)); cardModel.put("answerFontSize", cursor.getInt(17)); cardModel.put("answerFontColour", cursor.getString(18)); cardModel.put("answerAlign", cursor.getInt(19)); cardModel.put("lastFontFamily", cursor.getString(20)); cardModel.put("lastFontSize", cursor.getInt(21)); cardModel.put("lastFontColour", cursor.getString(22)); cardModel.put("editQuestionFontFamily", cursor.getString(23)); cardModel.put("editQuestionFontSize", cursor.getInt(24)); cardModel.put("editAnswerFontFamily", cursor.getString(25)); cardModel.put("editAnswerFontSize", cursor.getInt(26)); cardModel.put("allowEmptyAnswer", cursor.getString(27)); cardModel.put("typeAnswer", cursor.getString(28)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } cardModels.put(cardModel); } cursor.close(); return cardModels; } private void deleteModels(JSONArray ids) { Log.i(AnkiDroidApp.TAG, "deleteModels"); int len = ids.length(); for (int i = 0; i < len; i++) { try { mDeck.deleteModel(ids.getString(i)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } } private void updateModels(JSONArray models) { ArrayList<String> insertedModelsIds = new ArrayList<String>(); AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); String sql = "INSERT OR REPLACE INTO models" + " (id, deckId, created, modified, tags, name, description, features, spacing, initialSpacing, source)" + " VALUES(?,?,?,?,?,?,?,?,?,?,?)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sql); int len = models.length(); for (int i = 0; i < len; i++) { try { JSONObject model = models.getJSONObject(i); // id String id = model.getString("id"); statement.bindString(1, id); // deckId statement.bindLong(2, model.getLong("deckId")); // created statement.bindDouble(3, model.getDouble("created")); // modified statement.bindDouble(4, model.getDouble("modified")); // tags statement.bindString(5, model.getString("tags")); // name statement.bindString(6, model.getString("name")); // description statement.bindString(7, model.getString("name")); // features statement.bindString(8, model.getString("features")); // spacing statement.bindDouble(9, model.getDouble("spacing")); // initialSpacing statement.bindDouble(10, model.getDouble("initialSpacing")); // source statement.bindLong(11, model.getLong("source")); statement.execute(); insertedModelsIds.add(id); mergeFieldModels(id, model.getJSONArray("fieldModels")); mergeCardModels(id, model.getJSONArray("cardModels")); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); // Delete inserted models from modelsDeleted ankiDB.getDatabase() .execSQL("DELETE FROM modelsDeleted WHERE modelId IN " + Utils.ids2str(insertedModelsIds)); } private void mergeFieldModels(String modelId, JSONArray fieldModels) { ArrayList<String> ids = new ArrayList<String>(); AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); String sql = "INSERT OR REPLACE INTO fieldModels VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sql); int len = fieldModels.length(); for (int i = 0; i < len; i++) { try { JSONObject fieldModel = fieldModels.getJSONObject(i); // id String id = fieldModel.getString("id"); statement.bindString(1, id); // ordinal statement.bindString(2, fieldModel.getString("ordinal")); // modelId statement.bindLong(3, fieldModel.getLong("modelId")); // name statement.bindString(4, fieldModel.getString("name")); // description statement.bindString(5, fieldModel.getString("description")); // features statement.bindString(6, fieldModel.getString("features")); // required statement.bindLong(7, Utils.booleanToInt(fieldModel.getBoolean("required"))); // unique statement.bindLong(8, Utils.booleanToInt(fieldModel.getBoolean("unique"))); // numeric statement.bindLong(9, Utils.booleanToInt(fieldModel.getBoolean("numeric"))); // quizFontFamily if (fieldModel.isNull("quizFontFamily")) { statement.bindNull(10); } else { statement.bindString(10, fieldModel.getString("quizFontFamily")); } // quizFontSize if (fieldModel.isNull("quizFontSize")) { statement.bindNull(11); } else { statement.bindString(11, fieldModel.getString("quizFontSize")); } // quizFontColour if (fieldModel.isNull("quizFontColour")) { statement.bindNull(12); } else { statement.bindString(12, fieldModel.getString("quizFontColour")); } // editFontFamily if (fieldModel.isNull("editFontFamily")) { statement.bindNull(13); } else { statement.bindString(13, fieldModel.getString("editFontFamily")); } // editFontSize statement.bindString(14, fieldModel.getString("editFontSize")); statement.execute(); ids.add(id); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException"); } } statement.close(); // Delete field models that were not returned by the server ArrayList<String> fieldModelsIds = ankiDB.queryColumn(String.class, "SELECT id FROM fieldModels WHERE modelId = " + modelId, 0); for (String fieldModelId : fieldModelsIds) { if (!ids.contains(fieldModelId)) { mDeck.deleteFieldModel(modelId, fieldModelId); } } } private void mergeCardModels(String modelId, JSONArray cardModels) { ArrayList<String> ids = new ArrayList<String>(); AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); String sql = "INSERT OR REPLACE INTO cardModels (id, ordinal, modelId, name, description, active, qformat, " + "aformat, lformat, qedformat, aedformat, questionInAnswer, questionFontFamily, questionFontSize, " + "questionFontColour, questionAlign, answerFontFamily, answerFontSize, answerFontColour, answerAlign, " + "lastFontFamily, lastFontSize, lastFontColour, editQuestionFontFamily, editQuestionFontSize, " + "editAnswerFontFamily, editAnswerFontSize, allowEmptyAnswer, typeAnswer) " + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sql); int len = cardModels.length(); for (int i = 0; i < len; i++) { try { JSONObject cardModel = cardModels.getJSONObject(i); // id String id = cardModel.getString("id"); statement.bindString(1, id); // ordinal statement.bindString(2, cardModel.getString("ordinal")); // modelId statement.bindLong(3, cardModel.getLong("modelId")); // name statement.bindString(4, cardModel.getString("name")); // description statement.bindString(5, cardModel.getString("description")); // active statement.bindLong(6, Utils.booleanToInt(cardModel.getBoolean("active"))); // qformat statement.bindString(7, cardModel.getString("qformat")); // aformat statement.bindString(8, cardModel.getString("aformat")); // lformat if (cardModel.isNull("lformat")) { statement.bindNull(9); } else { statement.bindString(9, cardModel.getString("lformat")); } // qedformat if (cardModel.isNull("qedformat")) { statement.bindNull(10); } else { statement.bindString(10, cardModel.getString("qedformat")); } // aedformat if (cardModel.isNull("aedformat")) { statement.bindNull(11); } else { statement.bindString(11, cardModel.getString("aedformat")); } // questionInAnswer statement.bindLong(12, Utils.booleanToInt(cardModel.getBoolean("questionInAnswer"))); // questionFontFamily statement.bindString(13, cardModel.getString("questionFontFamily")); // questionFontSize statement.bindString(14, cardModel.getString("questionFontSize")); // questionFontColour statement.bindString(15, cardModel.getString("questionFontColour")); // questionAlign statement.bindString(16, cardModel.getString("questionAlign")); // answerFontFamily statement.bindString(17, cardModel.getString("answerFontFamily")); // answerFontSize statement.bindString(18, cardModel.getString("answerFontSize")); // answerFontColour statement.bindString(19, cardModel.getString("answerFontColour")); // answerAlign statement.bindString(20, cardModel.getString("answerAlign")); // lastFontFamily statement.bindString(21, cardModel.getString("lastFontFamily")); // lastFontSize statement.bindString(22, cardModel.getString("lastFontSize")); // lastFontColour statement.bindString(23, cardModel.getString("lastFontColour")); // editQuestionFontFamily if (cardModel.isNull("editQuestionFontFamily")) { statement.bindNull(24); } else { statement.bindString(24, cardModel.getString("editQuestionFontFamily")); } // editQuestionFontSize if (cardModel.isNull("editQuestionFontSize")) { statement.bindNull(25); } else { statement.bindString(25, cardModel.getString("editQuestionFontSize")); } // editAnswerFontFamily if (cardModel.isNull("editAnswerFontFamily")) { statement.bindNull(26); } else { statement.bindString(26, cardModel.getString("editAnswerFontFamily")); } // editAnswerFontSize if (cardModel.isNull("editAnswerFontSize")) { statement.bindNull(27); } else { statement.bindString(27, cardModel.getString("editAnswerFontSize")); } // allowEmptyAnswer if (cardModel.isNull("allowEmptyAnswer")) { cardModel.put("allowEmptyAnswer", true); } statement.bindLong(28, Utils.booleanToInt(cardModel.getBoolean("allowEmptyAnswer"))); // typeAnswer statement.bindString(29, cardModel.getString("typeAnswer")); statement.execute(); ids.add(id); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); // Delete card models that were not returned by the server ArrayList<String> cardModelsIds = ankiDB.queryColumn(String.class, "SELECT id FROM cardModels WHERE modelId = " + modelId, 0); for (String cardModelId : cardModelsIds) { if (!ids.contains(cardModelId)) { mDeck.deleteCardModel(modelId, cardModelId); } } } /** * Facts */ // TODO: Take into account the updateModified boolean (modified = time.time() or modified = "modified"... what does // exactly do that?) /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getFacts */ private JSONObject getFacts(JSONArray ids)// , boolean updateModified) { Log.i(AnkiDroidApp.TAG, "getFacts"); JSONObject facts = new JSONObject(); JSONArray factsArray = new JSONArray(); JSONArray fieldsArray = new JSONArray(); int len = ids.length(); for (int i = 0; i < len; i++) { try { Long id = ids.getLong(i); factsArray.put(getFact(id)); putFields(fieldsArray, id); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } try { facts.put("facts", factsArray); facts.put("fields", fieldsArray); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } Log.i(AnkiDroidApp.TAG, "facts = "); Utils.printJSONObject(facts, false); return facts; } private JSONArray getFact(Long id) { JSONArray fact = new JSONArray(); // TODO: Take into account the updateModified boolean (modified = time.time() or modified = "modified"... what // does exactly do that?) Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase().rawQuery( "SELECT id, modelId, created, modified, tags, spaceUntil, lastCardId FROM facts WHERE id = " + id, null); if (cursor.moveToFirst()) { try { fact.put(cursor.getLong(0)); fact.put(cursor.getLong(1)); fact.put(cursor.getDouble(2)); fact.put(cursor.getDouble(3)); fact.put(cursor.getString(4)); fact.put(cursor.getDouble(5)); fact.put(cursor.getLong(6)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); return fact; } private void putFields(JSONArray fields, Long id) { Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM fields WHERE factId = " + id, null); while (cursor.moveToNext()) { JSONArray field = new JSONArray(); field.put(cursor.getLong(0)); field.put(cursor.getLong(1)); field.put(cursor.getLong(2)); field.put(cursor.getInt(3)); field.put(cursor.getString(4)); fields.put(field); } cursor.close(); } private void updateFacts(JSONObject factsDict) { try { AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); JSONArray facts = factsDict.getJSONArray("facts"); int lenFacts = facts.length(); if (lenFacts > 0) { JSONArray fields = factsDict.getJSONArray("fields"); int lenFields = fields.length(); // Grab fact ids // They will be used later to recalculate the count of facts and to delete them from DB ArrayList<String> factIds = new ArrayList<String>(); for (int i = 0; i < lenFacts; i++) { factIds.add(facts.getJSONArray(i).getString(0)); } String factIdsString = Utils.ids2str(factIds); // Recalculate fact count mDeck.setFactCount((int) (mDeck.getFactCount() + (lenFacts - ankiDB.queryScalar("SELECT COUNT(*) FROM facts WHERE id IN " + factIdsString)))); // Update facts String sqlFact = "INSERT OR REPLACE INTO facts (id, modelId, created, modified, tags, spaceUntil, lastCardId)" + " VALUES(?,?,?,?,?,?,?)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sqlFact); for (int i = 0; i < lenFacts; i++) { JSONArray fact = facts.getJSONArray(i); // id statement.bindLong(1, fact.getLong(0)); // modelId statement.bindLong(2, fact.getLong(1)); // created statement.bindDouble(3, fact.getDouble(2)); // modified statement.bindDouble(4, fact.getDouble(3)); // tags statement.bindString(5, fact.getString(4)); // spaceUntil statement.bindDouble(6, fact.getDouble(5)); // lastCardId if (!fact.isNull(6)) { statement.bindLong(7, fact.getLong(6)); } else { statement.bindNull(7); } statement.execute(); } statement.close(); // Update fields (and delete first the local ones, since ids may have changed) ankiDB.getDatabase().execSQL("DELETE FROM fields WHERE factId IN " + factIdsString); String sqlFields = "INSERT INTO fields (id, factId, fieldModelId, ordinal, value) VALUES(?,?,?,?,?)"; statement = ankiDB.getDatabase().compileStatement(sqlFields); for (int i = 0; i < lenFields; i++) { JSONArray field = fields.getJSONArray(i); // id statement.bindLong(1, field.getLong(0)); // factId statement.bindLong(2, field.getLong(1)); // fieldModelId statement.bindLong(3, field.getLong(2)); // ordinal statement.bindString(4, field.getString(3)); // value statement.bindString(5, field.getString(4)); statement.execute(); } statement.close(); // Delete inserted facts from deleted ankiDB.getDatabase().execSQL("DELETE FROM factsDeleted WHERE factId IN " + factIdsString); } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } /** * Cards */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getCards */ private JSONArray getCards(JSONArray ids) { JSONArray cards = new JSONArray(); // SELECT id, factId, cardModelId, created, modified, tags, ordinal, priority, interval, lastInterval, due, // lastDue, factor, // firstAnswered, reps, successive, averageTime, reviewTime, youngEase0, youngEase1, youngEase2, youngEase3, // youngEase4, // matureEase0, matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount, question, answer, // lastFactor, spaceUntil, // type, combinedDue FROM cards WHERE id IN " + ids2str(ids) Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM cards WHERE id IN " + Utils.ids2str(ids), null); while (cursor.moveToNext()) { try { JSONArray card = new JSONArray(); // id card.put(cursor.getLong(0)); // factId card.put(cursor.getLong(1)); // cardModelId card.put(cursor.getLong(2)); // created card.put(cursor.getDouble(3)); // modified card.put(cursor.getDouble(4)); // tags card.put(cursor.getString(5)); // ordinal card.put(cursor.getInt(6)); // priority card.put(cursor.getInt(9)); // interval card.put(cursor.getDouble(10)); // lastInterval card.put(cursor.getDouble(11)); // due card.put(cursor.getDouble(12)); // lastDue card.put(cursor.getDouble(13)); // factor card.put(cursor.getDouble(14)); // firstAnswered card.put(cursor.getDouble(16)); // reps card.put(cursor.getString(17)); // successive card.put(cursor.getInt(18)); // averageTime card.put(cursor.getDouble(19)); // reviewTime card.put(cursor.getDouble(20)); // youngEase0 card.put(cursor.getInt(21)); // youngEase1 card.put(cursor.getInt(22)); // youngEase2 card.put(cursor.getInt(23)); // youngEase3 card.put(cursor.getInt(24)); // youngEase4 card.put(cursor.getInt(25)); // matureEase0 card.put(cursor.getInt(26)); // matureEase1 card.put(cursor.getInt(27)); // matureEase2 card.put(cursor.getInt(28)); // matureEase3 card.put(cursor.getInt(29)); // matureEase4 card.put(cursor.getInt(30)); // yesCount card.put(cursor.getInt(31)); // noCount card.put(cursor.getInt(32)); // question card.put(cursor.getString(7)); // answer card.put(cursor.getString(8)); // lastFactor card.put(cursor.getDouble(15)); // spaceUntil card.put(cursor.getDouble(33)); // type card.put(cursor.getInt(36)); // combinedDue card.put(cursor.getInt(37)); cards.put(card); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); return cards; } private void updateCards(JSONArray cards) { int len = cards.length(); if (len > 0) { AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); ArrayList<String> ids = new ArrayList<String>(); for (int i = 0; i < len; i++) { try { ids.add(cards.getJSONArray(i).getString(0)); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } String idsString = Utils.ids2str(ids); mDeck.setCardCount((int) (mDeck.getCardCount() + (len - ankiDB.queryScalar("SELECT COUNT(*) FROM cards WHERE id IN " + idsString)))); String sql = "INSERT OR REPLACE INTO cards (id, factId, cardModelId, created, modified, tags, ordinal, " + "priority, interval, lastInterval, due, lastDue, factor, firstAnswered, reps, successive, " + "averageTime, reviewTime, youngEase0, youngEase1, youngEase2, youngEase3, youngEase4, " + "matureEase0, matureEase1, matureEase2, matureEase3, matureEase4, yesCount, noCount, question, " + "answer, lastFactor, spaceUntil, type, combinedDue, relativeDelay, isDue) " + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 0, 0)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sql); for (int i = 0; i < len; i++) { try { JSONArray card = cards.getJSONArray(i); // id statement.bindLong(1, card.getLong(0)); // factId statement.bindLong(2, card.getLong(1)); // cardModelId statement.bindLong(3, card.getLong(2)); // created statement.bindDouble(4, card.getDouble(3)); // modified statement.bindDouble(5, card.getDouble(4)); // tags statement.bindString(6, card.getString(5)); // ordinal statement.bindString(7, card.getString(6)); // priority statement.bindString(8, card.getString(7)); // interval statement.bindDouble(9, card.getDouble(8)); // lastInterval statement.bindDouble(10, card.getDouble(9)); // due statement.bindDouble(11, card.getDouble(10)); // lastDue statement.bindDouble(12, card.getDouble(11)); // factor statement.bindDouble(13, card.getDouble(12)); // firstAnswered statement.bindDouble(14, card.getDouble(13)); // reps statement.bindString(15, card.getString(14)); // successive statement.bindString(16, card.getString(15)); // averageTime statement.bindDouble(17, card.getDouble(16)); // reviewTime statement.bindDouble(18, card.getDouble(17)); // youngEase0 statement.bindString(19, card.getString(18)); // youngEase1 statement.bindString(20, card.getString(19)); // youngEase2 statement.bindString(21, card.getString(20)); // youngEase3 statement.bindString(22, card.getString(21)); // youngEase4 statement.bindString(23, card.getString(22)); // matureEase0 statement.bindString(24, card.getString(23)); // matureEase1 statement.bindString(25, card.getString(24)); // matureEase2 statement.bindString(26, card.getString(25)); // matureEase3 statement.bindString(27, card.getString(26)); // matureEase4 statement.bindString(28, card.getString(27)); // yesCount statement.bindString(29, card.getString(28)); // noCount statement.bindString(30, card.getString(29)); // question statement.bindString(31, card.getString(30)); // answer statement.bindString(32, card.getString(31)); // lastFactor statement.bindDouble(33, card.getDouble(32)); // spaceUntil statement.bindDouble(34, card.getDouble(33)); // type statement.bindString(35, card.getString(34)); // combinedDue statement.bindString(36, card.getString(35)); statement.execute(); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); ankiDB.getDatabase().execSQL("DELETE FROM cardsDeleted WHERE cardId IN " + idsString); } } /** * Media */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getMedia */ private JSONArray getMedia(JSONArray ids) { JSONArray media = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT id, filename, size, created, originalPath, description FROM media WHERE id IN " + Utils.ids2str(ids), null); while (cursor.moveToNext()) { try { JSONArray m = new JSONArray(); // id m.put(cursor.getLong(0)); // filename m.put(cursor.getString(1)); // size m.put(cursor.getInt(2)); // created m.put(cursor.getDouble(3)); // originalPath m.put(cursor.getString(4)); // description m.put(cursor.getString(5)); media.put(m); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); return media; } private void deleteMedia(JSONArray ids) { Log.i(AnkiDroidApp.TAG, "deleteMedia"); AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); String idsString = Utils.ids2str(ids); // Get filenames ArrayList<String> files = ankiDB.queryColumn(String.class, "SELECT filename FROM media WHERE id IN " + idsString, 0); // Note the media to delete (Insert the media to delete into mediaDeleted) double now = Utils.now(); String sqlInsert = "INSERT INTO mediaDeleted SELECT id, " + String.format(Utils.ENGLISH_LOCALE, "%f", now) + " FROM media WHERE media.id = ?"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sqlInsert); int len = ids.length(); for (int i = 0; i < len; i++) { try { Log.i(AnkiDroidApp.TAG, "Inserting media " + ids.getLong(i) + " into mediaDeleted"); statement.bindLong(1, ids.getLong(i)); statement.executeInsert(); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); // Delete media Log.i(AnkiDroidApp.TAG, "Deleting media in = " + idsString); ankiDB.getDatabase().execSQL("DELETE FROM media WHERE id IN " + idsString); } private void updateMedia(JSONArray media) { AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); ArrayList<String> mediaIds = new ArrayList<String>(); String sql = "INSERT OR REPLACE INTO media (id, filename, size, created, originalPath, description) " + "VALUES(?,?,?,?,?,?)"; SQLiteStatement statement = ankiDB.getDatabase().compileStatement(sql); int len = media.length(); for (int i = 0; i < len; i++) { try { JSONArray m = media.getJSONArray(i); // Grab media ids, to delete them later String id = m.getString(0); mediaIds.add(id); // id statement.bindString(1, id); // filename statement.bindString(2, m.getString(1)); // size statement.bindString(3, m.getString(2)); // created statement.bindDouble(4, m.getDouble(3)); // originalPath statement.bindString(5, m.getString(4)); // description statement.bindString(6, m.getString(5)); statement.execute(); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); ankiDB.getDatabase().execSQL("DELETE FROM mediaDeleted WHERE mediaId IN " + Utils.ids2str(mediaIds)); } /** * Deck/Stats/History/Sources */ private JSONObject bundleDeck() { JSONObject bundledDeck = new JSONObject(); try { bundledDeck = mDeck.bundleJson(bundledDeck); // AnkiDroid Deck.java does not have: // css, forceMediaDir, lastSessionStart, lastTags, needLock, newCardOrder, newCardSpacing, newCardsPerDay, // progressHandlerCalled, // progressHandlerEnabled, revCardOrder, sessionRepLimit, sessionStartReps, sessionStartTime, // sessionTimeLimit, tmpMediaDir // Add meta information of the deck (deckVars table) JSONArray meta = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM deckVars", null); while (cursor.moveToNext()) { JSONArray deckVar = new JSONArray(); deckVar.put(cursor.getString(0)); deckVar.put(cursor.getString(1)); meta.put(deckVar); } cursor.close(); bundledDeck.put("meta", meta); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } Log.i(AnkiDroidApp.TAG, "Deck ="); Utils.printJSONObject(bundledDeck, false); return bundledDeck; } private void updateDeck(JSONObject deckPayload) { try { JSONArray meta = deckPayload.getJSONArray("meta"); // Update meta information String sqlMeta = "INSERT OR REPLACE INTO deckVars (key, value) VALUES(?,?)"; SQLiteStatement statement = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .compileStatement(sqlMeta); int lenMeta = meta.length(); for (int i = 0; i < lenMeta; i++) { JSONArray deckVar = meta.getJSONArray(i); // key statement.bindString(1, deckVar.getString(0)); // value statement.bindString(2, deckVar.getString(1)); statement.execute(); } statement.close(); // Update deck mDeck.updateFromJson(deckPayload); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } private JSONObject bundleStats() { Log.i(AnkiDroidApp.TAG, "bundleStats"); JSONObject bundledStats = new JSONObject(); // Get daily stats since the last day the deck was synchronized Date lastDay = new Date(java.lang.Math.max(0, (long) (mDeck.getLastSync() - 60 * 60 * 24) * 1000)); Log.i(AnkiDroidApp.TAG, "lastDay = " + lastDay.toString()); ArrayList<Long> ids = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).queryColumn(Long.class, "SELECT id FROM stats WHERE type = 1 and day >= \"" + lastDay.toString() + "\"", 0); try { Stats stat = new Stats(mDeck); // Put global stats bundledStats.put("global", Stats.globalStats(mDeck).bundleJson()); // Put daily stats JSONArray dailyStats = new JSONArray(); for (Long id : ids) { // Update stat with the values of the stat with id ids.get(i) stat.fromDB(id); // Bundle this stat and add it to dailyStats dailyStats.put(stat.bundleJson()); } bundledStats.put("daily", dailyStats); } catch (SQLException e) { Log.i(AnkiDroidApp.TAG, "SQLException = " + e.getMessage()); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } Log.i(AnkiDroidApp.TAG, "Stats ="); Utils.printJSONObject(bundledStats, false); return bundledStats; } private void updateStats(JSONObject stats) { try { // Update global stats Stats globalStats = Stats.globalStats(mDeck); globalStats.updateFromJson(stats.getJSONObject("global")); // Update daily stats Stats stat = new Stats(mDeck); JSONArray remoteDailyStats = stats.getJSONArray("daily"); int len = remoteDailyStats.length(); for (int i = 0; i < len; i++) { // Get a specific daily stat JSONObject remoteStat = remoteDailyStats.getJSONObject(i); Date dailyStatDate = Utils.ordinalToDate(remoteStat.getInt("day")); // If exists a statistic for this day, get it try { Long id = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).queryScalar( "SELECT id FROM stats WHERE type = 1 AND day = \"" + dailyStatDate.toString() + "\""); stat.fromDB(id); } catch (SQLException e) { // If it does not exist, create a statistic for this day stat.create(Stats.STATS_DAY, dailyStatDate); } // Update daily stat stat.updateFromJson(remoteStat); } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } private JSONArray bundleHistory() { JSONArray bundledHistory = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase().rawQuery( "SELECT cardId, time, lastInterval, nextInterval, ease, delay, lastFactor, nextFactor, reps, " + "thinkingTime, yesCount, noCount FROM reviewHistory " + "WHERE time > " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync()), null); while (cursor.moveToNext()) { try { JSONArray review = new JSONArray(); // cardId review.put(cursor.getLong(0)); // time review.put(cursor.getDouble(1)); // lastInterval review.put(cursor.getDouble(2)); // nextInterval review.put(cursor.getDouble(3)); // ease review.put(cursor.getInt(4)); // delay review.put(cursor.getDouble(5)); // lastFactor review.put(cursor.getDouble(6)); // nextFactor review.put(cursor.getDouble(7)); // reps review.put(cursor.getDouble(8)); // thinkingTime review.put(cursor.getDouble(9)); // yesCount review.put(cursor.getDouble(10)); // noCount review.put(cursor.getDouble(11)); bundledHistory.put(review); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); Log.i(AnkiDroidApp.TAG, "Last sync = " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync())); Log.i(AnkiDroidApp.TAG, "Bundled history = " + bundledHistory.toString()); return bundledHistory; } private void updateHistory(JSONArray history) { String sql = "INSERT OR IGNORE INTO reviewHistory (cardId, time, lastInterval, nextInterval, ease, delay, " + "lastFactor, nextFactor, reps, thinkingTime, yesCount, noCount) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)"; SQLiteStatement statement = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .compileStatement(sql); int len = history.length(); for (int i = 0; i < len; i++) { try { JSONArray h = history.getJSONArray(i); // cardId statement.bindLong(1, h.getLong(0)); // time statement.bindDouble(2, h.getDouble(1)); // lastInterval statement.bindDouble(3, h.getDouble(2)); // nextInterval statement.bindDouble(4, h.getDouble(3)); // ease statement.bindString(5, h.getString(4)); // delay statement.bindDouble(6, h.getDouble(5)); // lastFactor statement.bindDouble(7, h.getDouble(6)); // nextFactor statement.bindDouble(8, h.getDouble(7)); // reps statement.bindDouble(9, h.getDouble(8)); // thinkingTime statement.bindDouble(10, h.getDouble(9)); // yesCount statement.bindDouble(11, h.getDouble(10)); // noCount statement.bindDouble(12, h.getDouble(11)); statement.execute(); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); } private JSONArray bundleSources() { JSONArray bundledSources = new JSONArray(); Cursor cursor = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .rawQuery("SELECT * FROM sources", null); while (cursor.moveToNext()) { try { JSONArray source = new JSONArray(); // id source.put(cursor.getLong(0)); // name source.put(cursor.getString(1)); // created source.put(cursor.getDouble(2)); // lastSync source.put(cursor.getDouble(3)); // syncPeriod source.put(cursor.getInt(4)); bundledSources.put(source); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } cursor.close(); Log.i(AnkiDroidApp.TAG, "Bundled sources = " + bundledSources); return bundledSources; } private void updateSources(JSONArray sources) { String sql = "INSERT OR REPLACE INTO sources VALUES(?,?,?,?,?)"; SQLiteStatement statement = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()).getDatabase() .compileStatement(sql); int len = sources.length(); for (int i = 0; i < len; i++) { try { JSONArray source = sources.getJSONArray(i); statement.bindLong(1, source.getLong(0)); statement.bindString(2, source.getString(1)); statement.bindDouble(3, source.getDouble(2)); statement.bindDouble(4, source.getDouble(3)); statement.bindString(5, source.getString(4)); statement.execute(); } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } statement.close(); } /** * Full sync */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - needFullSync * * @param sums * @return */ @SuppressWarnings("unchecked") public boolean needFullSync(JSONArray sums) { Log.i(AnkiDroidApp.TAG, "needFullSync - lastSync = " + mDeck.getLastSync()); if (mDeck.getLastSync() <= 0) { Log.i(AnkiDroidApp.TAG, "deck.lastSync <= 0"); return true; } int len = sums.length(); for (int i = 0; i < len; i++) { try { JSONObject summary = sums.getJSONObject(i); Iterator keys = summary.keys(); while (keys.hasNext()) { String key = (String) keys.next(); JSONArray l = (JSONArray) summary.get(key); Log.i(AnkiDroidApp.TAG, "Key " + key + ", length = " + l.length()); if (l.length() > 500) { Log.i(AnkiDroidApp.TAG, "Length of key > 500"); return true; } } } catch (JSONException e) { Log.i(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } AnkiDb ankiDB = AnkiDatabaseManager.getDatabase(mDeck.getDeckPath()); if (ankiDB.queryScalar("SELECT count() FROM reviewHistory WHERE time > " + mDeck.getLastSync()) > 500) { Log.i(AnkiDroidApp.TAG, "reviewHistory since lastSync > 500"); return true; } Date lastDay = new Date(java.lang.Math.max(0, (long) (mDeck.getLastSync() - 60 * 60 * 24) * 1000)); Log.i(AnkiDroidApp.TAG, "lastDay = " + lastDay.toString() + ", lastDayInMillis = " + lastDay.getTime()); Log.i(AnkiDroidApp.TAG, "Count stats = " + ankiDB.queryScalar("SELECT count() FROM stats WHERE day >= \"" + lastDay.toString() + "\"")); if (ankiDB.queryScalar("SELECT count() FROM stats WHERE day >= \"" + lastDay.toString() + "\"") > 100) { Log.i(AnkiDroidApp.TAG, "stats since lastDay > 100"); return true; } return false; } public String prepareFullSync() { mDeck.setLastSync(Utils.now()); mDeck.commitToDB(); // The deck is closed after the full sync it is completed // deck.closeDeck(); if (mLocalTime > mRemoteTime) { return "fromLocal"; } else { return "fromServer"; } } public static void fullSyncFromLocal(String password, String username, String deckName, String deckPath) { URL url; try { Log.i(AnkiDroidApp.TAG, "Fullup"); url = new URL(AnkiDroidProxy.SYNC_URL + "fullup"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); conn.setRequestMethod("POST"); conn.setRequestProperty("Connection", "close"); conn.setRequestProperty("Charset", "UTF-8"); // conn.setRequestProperty("Content-Length", "8494662"); conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + MIME_BOUNDARY); conn.setRequestProperty("Host", AnkiDroidProxy.SYNC_HOST); DataOutputStream ds = new DataOutputStream(conn.getOutputStream()); Log.i(AnkiDroidApp.TAG, "Pass"); ds.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); ds.writeBytes("Content-Disposition: form-data; name=\"p\"" + END + END + password + END); Log.i(AnkiDroidApp.TAG, "User"); ds.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); ds.writeBytes("Content-Disposition: form-data; name=\"u\"" + END + END + username + END); Log.i(AnkiDroidApp.TAG, "DeckName"); ds.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); ds.writeBytes("Content-Disposition: form-data; name=\"d\"" + END + END + deckName + END); Log.i(AnkiDroidApp.TAG, "Deck"); ds.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); ds.writeBytes("Content-Disposition: form-data; name=\"deck\";filename=\"deck\"" + END); ds.writeBytes("Content-Type: application/octet-stream" + END); ds.writeBytes(END); FileInputStream fStream = new FileInputStream(deckPath); byte[] buffer = new byte[Utils.CHUNK_SIZE]; int length = -1; Deflater deflater = new Deflater(Deflater.BEST_SPEED); DeflaterOutputStream dos = new DeflaterOutputStream(ds, deflater); Log.i(AnkiDroidApp.TAG, "Writing buffer..."); while ((length = fStream.read(buffer)) != -1) { dos.write(buffer, 0, length); Log.i(AnkiDroidApp.TAG, "Length = " + length); } dos.finish(); fStream.close(); ds.writeBytes(END); ds.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + TWO_HYPHENS + END); Log.i(AnkiDroidApp.TAG, "Closing streams..."); ds.flush(); ds.close(); // Ensure we got the HTTP 200 response code int responseCode = conn.getResponseCode(); if (responseCode != 200) { Log.i(AnkiDroidApp.TAG, "Response code = " + responseCode); // throw new Exception(String.format("Received the response code %d from the URL %s", responseCode, // url)); } else { Log.i(AnkiDroidApp.TAG, "Response code = 200"); } // Read the response InputStream is = conn.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] bytes = new byte[1024]; int bytesRead; while ((bytesRead = is.read(bytes)) != -1) { baos.write(bytes, 0, bytesRead); } byte[] bytesReceived = baos.toByteArray(); baos.close(); is.close(); String response = new String(bytesReceived); Log.i(AnkiDroidApp.TAG, "Finished!"); } catch (MalformedURLException e) { Log.i(AnkiDroidApp.TAG, "MalformedURLException = " + e.getMessage()); } catch (IOException e) { Log.i(AnkiDroidApp.TAG, "IOException = " + e.getMessage()); } } public static void fullSyncFromServer(String password, String username, String deckName, String deckPath) { // Log.i(AnkiDroidApp.TAG, "password = " + password + ", user = " + username + ", d = " + deckName); try { String data = "p=" + URLEncoder.encode(password, "UTF-8") + "&u=" + URLEncoder.encode(username, "UTF-8") + "&d=" + URLEncoder.encode(deckName, "UTF-8"); // Log.i(AnkiDroidApp.TAG, "Data json = " + data); HttpPost httpPost = new HttpPost(AnkiDroidProxy.SYNC_URL + "fulldown"); StringEntity entity = new StringEntity(data); httpPost.setEntity(entity); httpPost.setHeader("Content-type", "application/x-www-form-urlencoded"); DefaultHttpClient httpClient = new DefaultHttpClient(); HttpResponse response = httpClient.execute(httpPost); Log.i(AnkiDroidApp.TAG, "Response = " + response.toString()); HttpEntity entityResponse = response.getEntity(); Log.i(AnkiDroidApp.TAG, "Entity's response = " + entityResponse.toString()); InputStream content = entityResponse.getContent(); Log.i(AnkiDroidApp.TAG, "Content = " + content.toString()); Utils.writeToFile(new InflaterInputStream(content), deckPath); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (ClientProtocolException e) { Log.i(AnkiDroidApp.TAG, "ClientProtocolException = " + e.getMessage()); } catch (IOException e) { Log.i(AnkiDroidApp.TAG, "IOException = " + e.getMessage()); } } }