/*************************************************************************************** * 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.service; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLEncoder; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; 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; 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.FileEntity; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ichi2.anki.AnkiDroidProxy; import com.ichi2.anki.Utils; import com.ichi2.anki.db.AnkiDatabaseManager; import com.ichi2.anki.db.AnkiDb; import com.ichi2.anki.model.Deck; import com.ichi2.anki.model.Stats; public class SyncClient { private static Logger log = LoggerFactory.getLogger(SyncClient.class); 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 double getRemoteTime() { return mRemoteTime; } public void setRemoteTime(double time) { mRemoteTime = time; } public double getLocalTime() { return mLocalTime; } public void setLocalTime(double time) { mLocalTime = time; } public void setDeck(Deck deck) { mDeck = deck; } /** * Anki Desktop -> libanki/anki/sync.py, prepareSync * * @return */ public boolean prepareSync(double timediff) { log.info("prepareSync = " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync())); mLocalTime = mDeck.getModified(); mRemoteTime = mServer.modified(); log.info("localTime = " + mLocalTime); log.info("remoteTime = " + mRemoteTime); if (mLocalTime == mRemoteTime) { return false; } double l = mDeck.getLastSync(); log.info("lastSync local = " + l); double r = mServer.lastSync(); log.info("lastSync remote = " + r); // Set lastSync to the lower of the two sides, and account for slow clocks & assume it took up to 10 seconds // for the reply to arrive mDeck.setLastSync(Math.min(l, r) - timediff - 10); return true; } public JSONArray summaries() throws JSONException { JSONArray summaries = new JSONArray(); JSONObject sum = summary(mDeck.getLastSync()); if (sum == null) { return null; } summaries.put(sum); sum = mServer.summary(mDeck.getLastSync()); summaries.put(sum); if (sum == null) { return null; } return summaries; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - summary * * @param lastSync * @throws JSONException */ public JSONObject summary(double lastSync) throws JSONException { log.info("Summary Local"); mDeck.setLastSync(lastSync); mDeck.commitToDB(); AnkiDb ankiDB = mDeck.getDB(); String lastSyncString = String.format(Utils.ENGLISH_LOCALE, "%f", lastSync); // Cards JSONArray cards = resultSetToJSONArray(ankiDB.rawQuery( "SELECT id, modified FROM cards WHERE modified > " + lastSyncString)); // Cards - delcards JSONArray delcards = resultSetToJSONArray(ankiDB.rawQuery( "SELECT cardId, deletedTime FROM cardsDeleted WHERE deletedTime > " + lastSyncString)); // Facts JSONArray facts = resultSetToJSONArray(ankiDB.rawQuery( "SELECT id, modified FROM facts WHERE modified > " + lastSyncString)); // Facts - delfacts JSONArray delfacts = resultSetToJSONArray(ankiDB.rawQuery( "SELECT factId, deletedTime FROM factsDeleted WHERE deletedTime > " + lastSyncString)); // Models JSONArray models = resultSetToJSONArray(ankiDB.rawQuery( "SELECT id, modified FROM models WHERE modified > " + lastSyncString)); // Models - delmodels JSONArray delmodels = resultSetToJSONArray(ankiDB.rawQuery( "SELECT modelId, deletedTime FROM modelsDeleted WHERE deletedTime > " + lastSyncString)); // Media JSONArray media = resultSetToJSONArray(ankiDB.rawQuery( "SELECT id, created FROM media WHERE created > " + lastSyncString)); // Media - delmedia JSONArray delmedia = resultSetToJSONArray(ankiDB.rawQuery( "SELECT mediaId, deletedTime FROM mediaDeleted WHERE deletedTime > " + lastSyncString)); 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.error("SyncClient.summary - JSONException = " + e.getMessage()); return null; } log.info("Summary Local = "); Utils.printJSONObject(summary, false); return summary; } private JSONArray resultSetToJSONArray(ResultSet result) throws JSONException { JSONArray jsonArray = new JSONArray(); try { while (result.next()) { JSONArray element = new JSONArray(); element.put(result.getLong(0)); element.put(result.getDouble(1)); jsonArray.put(element); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return jsonArray; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - genPayload * @throws JSONException */ public JSONObject genPayload(JSONArray summaries) throws JSONException { // log.info("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.info("Key " + keys[i].name()); String key = keys[i].name(); // 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); } // If the last modified deck was the local one, handle the remainder if (mLocalTime > mRemoteTime) { payload.put("stats", bundleStats()); payload.put("history", bundleHistory()); payload.put("sources", bundleSources()); // Finally, set new lastSync and bundle the deck info payload.put("deck", bundleDeck()); } log.info("Payload ="); Utils.printJSONObject(payload, true); //XXX: Why writeToFile = true? return payload; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - payloadChanges * @throws JSONException */ /* private Object[] payloadChanges(JSONObject payload) throws JSONException { Object[] h = new Object[8]; 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"; } return h; } */ /* Unsued public String payloadChangeReport(JSONObject payload) throws JSONException { return AnkiDroidApp.getAppResources().getString(R.string.change_report_format, payloadChanges(payload)); } */ public void applyPayloadReply(JSONObject payloadReply) throws JSONException { log.info("applyPayloadReply"); Keys[] keys = Keys.values(); for (int i = 0; i < keys.length; i++) { String key = keys[i].name(); updateObjsFromKey(payloadReply, key); } if (!payloadReply.isNull("deck")) { updateDeck(payloadReply.getJSONObject("deck")); updateStats(payloadReply.getJSONObject("stats")); updateHistory(payloadReply.getJSONArray("history")); if (!payloadReply.isNull("sources")) { updateSources(payloadReply.getJSONArray("sources")); } mDeck.commitToDB(); } mDeck.commitToDB(); // Rebuild priorities on client // Get card ids 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); } mDeck.updateCardTags(cardIds); rebuildPriorities(cardIds); long missingFacts = missingFacts(); if (missingFacts != 0l) { log.error("Facts missing after sync (" + missingFacts + " facts)!"); } assert missingFacts == 0l; } private long missingFacts() { try { return mDeck.getDB().queryScalar("SELECT count() FROM cards WHERE factId NOT IN (SELECT id FROM facts)"); } catch (Exception e) { return 0; } } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - preSyncRefresh */ private void preSyncRefresh() { Stats.globalStats(mDeck); } private void rebuildPriorities(long[] cardIds) { rebuildPriorities(cardIds, null); } private void rebuildPriorities(long[] cardIds, String[] suspend) { //try { mDeck.updateAllPriorities(true, false); mDeck.updatePriorities(cardIds, suspend, false); //} catch (SQLException e) { // log.error(TAG + " SQLException e = " + e.getMessage()); //} } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - diffSummary * @throws JSONException */ private JSONArray diffSummary(JSONObject summaryLocal, JSONObject summaryServer, String key) throws JSONException { JSONArray locallyEdited = new JSONArray(); JSONArray locallyDeleted = new JSONArray(); JSONArray remotelyEdited = new JSONArray(); JSONArray remotelyDeleted = new JSONArray(); log.info("\ndiffSummary - Key = " + key); log.info("\nSummary local = "); Utils.printJSONObject(summaryLocal, false); log.info("\nSummary server = "); Utils.printJSONObject(summaryServer, false); // Hash of all modified ids HashSet<Long> ids = new HashSet<Long>(); // 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.info("\nid = " + id + ", localModTime = " + localModTime + ", remoteModTime = " + remoteModTime); // Changed/Existing on both sides if (localModTime != null && remoteModTime != null) { log.info("localModTime not null AND remoteModTime not null"); if (localModTime < remoteModTime) { log.info("Remotely edited"); remotelyEdited.put(id); } else if (localModTime > remoteModTime) { log.info("Locally edited"); locallyEdited.put(id); } } // If it's missing on server or newer here, sync else if (localModTime != null && remoteModTime == null) { log.info("localModTime not null AND remoteModTime null"); if (!rdeletedIds.containsKey(id) || rdeletedIds.get(id) < localModTime) { log.info("Locally edited"); locallyEdited.put(id); } else { log.info("Remotely deleted"); remotelyDeleted.put(id); } } // If it's missing locally or newer there, sync else if (remoteModTime != null && localModTime == null) { log.info("remoteModTime not null AND localModTime null"); if (!ldeletedIds.containsKey(id) || ldeletedIds.get(id) < remoteModTime) { log.info("Remotely edited"); remotelyEdited.put(id); } else { log.info("Locally deleted"); locallyDeleted.put(id); } } // Deleted or not modified in both sides else { log.info("localModTime null AND remoteModTime null"); if (ldeletedIds.containsKey(id) && !rdeletedIds.containsKey(id)) { log.info("Locally deleted"); locallyDeleted.put(id); } else if (rdeletedIds.containsKey(id) && !ldeletedIds.containsKey(id)) { log.info("Remotely deleted"); remotelyDeleted.put(id); } } } 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) throws JSONException { int nbItems = existingItems.length(); for (int i = 0; i < nbItems; i++) { JSONArray itemModified = existingItems.getJSONArray(i); Long idItem = itemModified.getLong(0); Double modTimeItem = itemModified.getDouble(1); dictExistingItems.put(idItem, modTimeItem); ids.add(idItem); } } private HashMap<Long, Double> putDeletedItems(HashSet<Long> ids, HashMap<Long, Double> dictDeletedItems, JSONArray deletedItems) throws JSONException { HashMap<Long, Double> deletedIds = new HashMap<Long, Double>(); int nbItems = deletedItems.length(); for (int i = 0; i < nbItems; i++) { 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); } return deletedIds; } private Object getObjsFromKey(JSONArray ids, String key) throws JSONException { 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) throws JSONException { if ("models".equalsIgnoreCase(key)) { log.info("updateModels"); updateModels(payloadReply.getJSONArray("added-models")); } else if ("facts".equalsIgnoreCase(key)) { log.info("updateFacts"); updateFacts(payloadReply.getJSONObject("added-facts")); } else if ("cards".equalsIgnoreCase(key)) { log.info("updateCards"); updateCards(payloadReply.getJSONArray("added-cards")); } else if ("media".equalsIgnoreCase(key)) { log.info("updateMedia"); updateMedia(payloadReply.getJSONArray("added-media")); } } /** * Models */ // TODO: Include the case with updateModified /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getModels * * @param ids * @return * @throws JSONException */ private JSONArray getModels(JSONArray ids) throws JSONException// , boolean updateModified) { JSONArray models = new JSONArray(); int nbIds = ids.length(); for (int i = 0; i < nbIds; i++) { models.put(bundleModel(ids.getLong(i))); } return models; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - bundleModel * * @param id * @return * @throws JSONException */ private JSONObject bundleModel(Long id) throws JSONException// , boolean updateModified { JSONObject model = new JSONObject(); ResultSet result = null; try { result = mDeck.getDB().rawQuery( "SELECT * FROM models WHERE id = " + id); if (result.next()) { int i = 1; model.put("id", result.getLong(i++)); model.put("deckId", result.getInt(i++)); model.put("created", result.getDouble(i++)); model.put("modified", result.getDouble(i++)); model.put("tags", result.getString(i++)); model.put("name", result.getString(i++)); model.put("description", result.getString(i++)); model.put("features", result.getDouble(i++)); model.put("spacing", result.getDouble(i++)); model.put("initialSpacing", result.getDouble(i++)); model.put("source", result.getInt(i++)); model.put("fieldModels", bundleFieldModels(id)); model.put("cardModels", bundleCardModels(id)); } } catch (SQLException e) { } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } log.info("Model = "); Utils.printJSONObject(model, false); return model; } /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - bundleFieldModel * * @param id * @return * @throws JSONException */ private JSONArray bundleFieldModels(Long id) throws JSONException { JSONArray fieldModels = new JSONArray(); ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM fieldModels WHERE modelId = " + id); try { while (result.next()) { JSONObject fieldModel = new JSONObject(); int i = 1; fieldModel.put("id", result.getLong(i++)); fieldModel.put("ordinal", result.getInt(i++)); fieldModel.put("modelId", result.getLong(i++)); fieldModel.put("name", result.getString(i++)); fieldModel.put("description", result.getString(i++)); fieldModel.put("features", result.getString(i++)); fieldModel.put("required", result.getString(i++)); fieldModel.put("unique", result.getString(i++)); fieldModel.put("numeric", result.getString(i++)); fieldModel.put("quizFontFamily", result.getString(i++)); fieldModel.put("quizFontSize", result.getInt(i++)); fieldModel.put("quizFontColour", result.getString(i++)); fieldModel.put("editFontFamily", result.getString(i++)); fieldModel.put("editFontSize", result.getInt(i++)); fieldModels.put(fieldModel); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return fieldModels; } private JSONArray bundleCardModels(Long id) throws JSONException { JSONArray cardModels = new JSONArray(); ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM cardModels WHERE modelId = " + id); try { while (result.next()) { JSONObject cardModel = new JSONObject(); int i = 1; cardModel.put("id", result.getLong(i++)); cardModel.put("ordinal", result.getInt(i++)); cardModel.put("modelId", result.getLong(i++)); cardModel.put("name", result.getString(i++)); cardModel.put("description", result.getString(i++)); cardModel.put("active", result.getString(i++)); cardModel.put("qformat", result.getString(i++)); cardModel.put("aformat", result.getString(i++)); cardModel.put("lformat", result.getString(i++)); cardModel.put("qedformat", result.getString(i++)); cardModel.put("aedformat", result.getString(i++)); cardModel.put("questionInAnswer", result.getString(i++)); cardModel.put("questionFontFamily", result.getString(i++)); cardModel.put("questionFontSize ", result.getInt(i++)); cardModel.put("questionFontColour", result.getString(i++)); cardModel.put("questionAlign", result.getInt(i++)); cardModel.put("answerFontFamily", result.getString(i++)); cardModel.put("answerFontSize", result.getInt(i++)); cardModel.put("answerFontColour", result.getString(i++)); cardModel.put("answerAlign", result.getInt(i++)); cardModel.put("lastFontFamily", result.getString(i++)); cardModel.put("lastFontSize", result.getInt(i++)); cardModel.put("lastFontColour", result.getString(i++)); cardModel.put("editQuestionFontFamily", result.getString(i++)); cardModel.put("editQuestionFontSize", result.getInt(i++)); cardModel.put("editAnswerFontFamily", result.getString(i++)); cardModel.put("editAnswerFontSize", result.getInt(i++)); cardModel.put("allowEmptyAnswer", result.getString(i++)); cardModel.put("typeAnswer", result.getString(i++)); cardModels.put(cardModel); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return cardModels; } private void deleteModels(JSONArray ids) throws JSONException { log.info("deleteModels"); int len = ids.length(); for (int i = 0; i < len; i++) { mDeck.deleteModel(ids.getString(i)); } } private void updateModels(JSONArray models) throws JSONException { ArrayList<String> insertedModelsIds = new ArrayList<String>(); AnkiDb ankiDB = mDeck.getDB(); String sql = "INSERT OR REPLACE INTO models" + " (id, deckId, created, modified, tags, name, description, features, spacing, initialSpacing, source)" + " VALUES(?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement statement = ankiDB.compileStatement(sql); int len = models.length(); for (int i = 0; i < len; i++) { JSONObject model = models.getJSONObject(i); // id String id = model.getString("id"); try { statement.setString(1, id); // deckId statement.setLong(2, model.getLong("deckId")); // created statement.setDouble(3, model.getDouble("created")); // modified statement.setDouble(4, model.getDouble("modified")); // tags statement.setString(5, model.getString("tags")); // name statement.setString(6, model.getString("name")); // description statement.setString(7, model.getString("name")); // features statement.setString(8, model.getString("features")); // spacing statement.setDouble(9, model.getDouble("spacing")); // initialSpacing statement.setDouble(10, model.getDouble("initialSpacing")); // source statement.setLong(11, model.getLong("source")); statement.execute(); } catch (SQLException e) { e.printStackTrace(); } insertedModelsIds.add(id); mergeFieldModels(id, model.getJSONArray("fieldModels")); mergeCardModels(id, model.getJSONArray("cardModels")); } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Delete inserted models from modelsDeleted ankiDB.execSQL("DELETE FROM modelsDeleted WHERE modelId IN " + Utils.ids2str(insertedModelsIds)); } private void mergeFieldModels(String modelId, JSONArray fieldModels) throws JSONException { ArrayList<String> ids = new ArrayList<String>(); String sql = "INSERT OR REPLACE INTO fieldModels VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement statement = mDeck.getDB().compileStatement(sql); int len = fieldModels.length(); for (int i = 0; i < len; i++) { JSONObject fieldModel = fieldModels.getJSONObject(i); // id String id = fieldModel.getString("id"); try { statement.setString(1, id); // ordinal statement.setString(2, fieldModel.getString("ordinal")); // modelId statement.setLong(3, fieldModel.getLong("modelId")); // name statement.setString(4, fieldModel.getString("name")); // description statement.setString(5, fieldModel.getString("description")); // features statement.setString(6, fieldModel.getString("features")); // required statement.setLong(7, Utils.booleanToInt(fieldModel.getBoolean("required"))); // unique statement.setLong(8, Utils.booleanToInt(fieldModel.getBoolean("unique"))); // numeric statement.setLong(9, Utils.booleanToInt(fieldModel.getBoolean("numeric"))); // quizFontFamily if (fieldModel.isNull("quizFontFamily")) { statement.setNull(10, Types.CHAR); } else { statement.setString(10, fieldModel.getString("quizFontFamily")); } // quizFontSize if (fieldModel.isNull("quizFontSize")) { statement.setNull(11, Types.CHAR); } else { statement.setString(11, fieldModel.getString("quizFontSize")); } // quizFontColour if (fieldModel.isNull("quizFontColour")) { statement.setNull(12, Types.CHAR); } else { statement.setString(12, fieldModel.getString("quizFontColour")); } // editFontFamily if (fieldModel.isNull("editFontFamily")) { statement.setNull(13, Types.CHAR); } else { statement.setString(13, fieldModel.getString("editFontFamily")); } // editFontSize statement.setString(14, fieldModel.getString("editFontSize")); statement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } ids.add(id); } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Delete field models that were not returned by the server ArrayList<String> fieldModelsIds = mDeck.getDB().queryColumn(String.class, "SELECT id FROM fieldModels WHERE modelId = " + modelId, 1); if (fieldModelsIds != null) { for (String fieldModelId : fieldModelsIds) { if (!ids.contains(fieldModelId)) { mDeck.deleteFieldModel(modelId, fieldModelId); } } } } private void mergeCardModels(String modelId, JSONArray cardModels) throws JSONException { ArrayList<String> ids = new ArrayList<String>(); 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(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement statement = mDeck.getDB().compileStatement(sql); int len = cardModels.length(); for (int i = 0; i < len; i++) { JSONObject cardModel = cardModels.getJSONObject(i); // id String id = cardModel.getString("id"); try { statement.setString(1, id); // ordinal statement.setString(2, cardModel.getString("ordinal")); // modelId statement.setLong(3, cardModel.getLong("modelId")); // name statement.setString(4, cardModel.getString("name")); // description statement.setString(5, cardModel.getString("description")); // active statement.setLong(6, Utils.booleanToInt(cardModel.getBoolean("active"))); // qformat statement.setString(7, cardModel.getString("qformat")); // aformat statement.setString(8, cardModel.getString("aformat")); // lformat if (cardModel.isNull("lformat")) { statement.setNull(9, Types.CHAR); } else { statement.setString(9, cardModel.getString("lformat")); } // qedformat if (cardModel.isNull("qedformat")) { statement.setNull(10, Types.CHAR); } else { statement.setString(10, cardModel.getString("qedformat")); } // aedformat if (cardModel.isNull("aedformat")) { statement.setNull(11, Types.CHAR); } else { statement.setString(11, cardModel.getString("aedformat")); } // questionInAnswer statement.setLong(12, Utils.booleanToInt(cardModel.getBoolean("questionInAnswer"))); // questionFontFamily statement.setString(13, cardModel.getString("questionFontFamily")); // questionFontSize statement.setString(14, cardModel.getString("questionFontSize")); // questionFontColour statement.setString(15, cardModel.getString("questionFontColour")); // questionAlign statement.setString(16, cardModel.getString("questionAlign")); // answerFontFamily statement.setString(17, cardModel.getString("answerFontFamily")); // answerFontSize statement.setString(18, cardModel.getString("answerFontSize")); // answerFontColour statement.setString(19, cardModel.getString("answerFontColour")); // answerAlign statement.setString(20, cardModel.getString("answerAlign")); // lastFontFamily statement.setString(21, cardModel.getString("lastFontFamily")); // lastFontSize statement.setString(22, cardModel.getString("lastFontSize")); // lastFontColour statement.setString(23, cardModel.getString("lastFontColour")); // editQuestionFontFamily if (cardModel.isNull("editQuestionFontFamily")) { statement.setNull(24, Types.CHAR); } else { statement.setString(24, cardModel.getString("editQuestionFontFamily")); } // editQuestionFontSize if (cardModel.isNull("editQuestionFontSize")) { statement.setNull(25, Types.CHAR); } else { statement.setString(25, cardModel.getString("editQuestionFontSize")); } // editAnswerFontFamily if (cardModel.isNull("editAnswerFontFamily")) { statement.setNull(26, Types.CHAR); } else { statement.setString(26, cardModel.getString("editAnswerFontFamily")); } // editAnswerFontSize if (cardModel.isNull("editAnswerFontSize")) { statement.setNull(27, Types.CHAR); } else { statement.setString(27, cardModel.getString("editAnswerFontSize")); } // allowEmptyAnswer if (cardModel.isNull("allowEmptyAnswer")) { cardModel.put("allowEmptyAnswer", true); } statement.setLong(28, Utils.booleanToInt(cardModel.getBoolean("allowEmptyAnswer"))); // typeAnswer statement.setString(29, cardModel.getString("typeAnswer")); statement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } ids.add(id); } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Delete card models that were not returned by the server ArrayList<String> cardModelsIds = mDeck.getDB().queryColumn(String.class, "SELECT id FROM cardModels WHERE modelId = " + modelId, 1); if (cardModelsIds != null) { 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 * @throws JSONException */ private JSONObject getFacts(JSONArray ids) throws JSONException// , boolean updateModified) { log.info("getFacts"); JSONObject facts = new JSONObject(); JSONArray factsArray = new JSONArray(); JSONArray fieldsArray = new JSONArray(); int len = ids.length(); for (int i = 0; i < len; i++) { Long id = ids.getLong(i); factsArray.put(getFact(id)); putFields(fieldsArray, id); } facts.put("facts", factsArray); facts.put("fields", fieldsArray); log.info("facts = "); Utils.printJSONObject(facts, false); return facts; } private JSONArray getFact(Long id) throws JSONException { JSONArray fact = new JSONArray(); // TODO: Take into account the updateModified boolean (modified = time.time() or modified = "modified"... what // does exactly do that?) ResultSet result = mDeck.getDB() .rawQuery("SELECT id, modelId, created, modified, tags, spaceUntil, lastCardId FROM facts WHERE id = " + id); try { if (result.next()) { int i = 1; fact.put(result.getLong(i++)); fact.put(result.getLong(i++)); fact.put(result.getDouble(i++)); fact.put(result.getDouble(i++)); fact.put(result.getString(i++)); fact.put(result.getDouble(i++)); fact.put(result.getLong(i++)); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return fact; } private void putFields(JSONArray fields, Long id) { ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM fields WHERE factId = " + id); try { while (result.next()) { JSONArray field = new JSONArray(); int i = 1; field.put(result.getLong(i++)); field.put(result.getLong(i++)); field.put(result.getLong(i++)); field.put(result.getInt(i++)); field.put(result.getString(i++)); fields.put(field); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } } private void updateFacts(JSONObject factsDict) throws JSONException { AnkiDb ankiDB = mDeck.getDB(); 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); // Update facts String sqlFact = "INSERT OR REPLACE INTO facts (id, modelId, created, modified, tags, spaceUntil, lastCardId)" + " VALUES(?,?,?,?,?,?,?)"; PreparedStatement statement = ankiDB.compileStatement(sqlFact); for (int i = 0; i < lenFacts; i++) { JSONArray fact = facts.getJSONArray(i); // id try { statement.setLong(1, fact.getLong(0)); // modelId statement.setLong(2, fact.getLong(1)); // created statement.setDouble(3, fact.getDouble(2)); // modified statement.setDouble(4, fact.getDouble(3)); // tags statement.setString(5, fact.getString(4)); // spaceUntil if (fact.getString(5) == null) { statement.setString(6, ""); } else { statement.setString(6, fact.getString(5)); } // lastCardId if (!fact.isNull(6)) { statement.setLong(7, fact.getLong(6)); } else { statement.setNull(7, Types.BIGINT); } statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Update fields (and delete first the local ones, since ids may have changed) ankiDB.execSQL("DELETE FROM fields WHERE factId IN " + factIdsString); String sqlFields = "INSERT INTO fields (id, factId, fieldModelId, ordinal, value) VALUES(?,?,?,?,?)"; statement = ankiDB.compileStatement(sqlFields); for (int i = 0; i < lenFields; i++) { JSONArray field = fields.getJSONArray(i); // id try { statement.setLong(1, field.getLong(0)); // factId statement.setLong(2, field.getLong(1)); // fieldModelId statement.setLong(3, field.getLong(2)); // ordinal statement.setString(4, field.getString(3)); // value statement.setString(5, field.getString(4)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Delete inserted facts from deleted ankiDB.execSQL("DELETE FROM factsDeleted WHERE factId IN " + factIdsString); } } /** * Cards */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getCards * @throws JSONException */ private JSONArray getCards(JSONArray ids) throws JSONException { 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, relativeDelay, // type, combinedDue FROM cards WHERE id IN " + ids2str(ids) ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM cards WHERE id IN " + Utils.ids2str(ids)); try { while (result.next()) { JSONArray card = new JSONArray(); // id card.put(result.getLong(1)); // factId card.put(result.getLong(2)); // cardModelId card.put(result.getLong(3)); // created card.put(result.getDouble(4)); // modified card.put(result.getDouble(5)); // tags card.put(result.getString(6)); // ordinal card.put(result.getInt(7)); // priority card.put(result.getInt(10)); // interval card.put(result.getDouble(11)); // lastInterval card.put(result.getDouble(12)); // due card.put(result.getDouble(13)); // lastDue card.put(result.getDouble(14)); // factor card.put(result.getDouble(15)); // firstAnswered card.put(result.getDouble(17)); // reps card.put(result.getString(18)); // successive card.put(result.getInt(19)); // averageTime card.put(result.getDouble(20)); // reviewTime card.put(result.getDouble(21)); // youngEase0 card.put(result.getInt(22)); // youngEase1 card.put(result.getInt(23)); // youngEase2 card.put(result.getInt(24)); // youngEase3 card.put(result.getInt(25)); // youngEase4 card.put(result.getInt(26)); // matureEase0 card.put(result.getInt(27)); // matureEase1 card.put(result.getInt(28)); // matureEase2 card.put(result.getInt(29)); // matureEase3 card.put(result.getInt(30)); // matureEase4 card.put(result.getInt(31)); // yesCount card.put(result.getInt(32)); // noCount card.put(result.getInt(33)); // question card.put(result.getString(8)); // answer card.put(result.getString(9)); // lastFactor card.put(result.getDouble(16)); // spaceUntil card.put(result.getDouble(34)); // type card.put(result.getInt(37)); // combinedDue card.put(result.getDouble(38)); // relativeDelay card.put(result.getInt(35)); cards.put(card); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return cards; } private void updateCards(JSONArray cards) throws JSONException { int len = cards.length(); if (len > 0) { AnkiDb ankiDB = mDeck.getDB(); ArrayList<String> ids = new ArrayList<String>(); for (int i = 0; i < len; i++) { ids.add(cards.getJSONArray(i).getString(0)); } String idsString = Utils.ids2str(ids); 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)"; PreparedStatement statement = ankiDB.compileStatement(sql); for (int i = 0; i < len; i++) { JSONArray card = cards.getJSONArray(i); // id try { statement.setLong(1, card.getLong(0)); // factId statement.setLong(2, card.getLong(1)); // cardModelId statement.setLong(3, card.getLong(2)); // created statement.setDouble(4, card.getDouble(3)); // modified statement.setDouble(5, card.getDouble(4)); // tags statement.setString(6, card.getString(5)); // ordinal statement.setLong(7, card.getInt(6)); // priority statement.setLong(8, card.getInt(7)); // interval statement.setDouble(9, card.getDouble(8)); // lastInterval statement.setDouble(10, card.getDouble(9)); // due statement.setDouble(11, card.getDouble(10)); // lastDue statement.setDouble(12, card.getDouble(11)); // factor statement.setDouble(13, card.getDouble(12)); // firstAnswered statement.setDouble(14, card.getDouble(13)); // reps statement.setLong(15, card.getInt(14)); // successive statement.setLong(16, card.getInt(15)); // averageTime statement.setDouble(17, card.getDouble(16)); // reviewTime statement.setDouble(18, card.getDouble(17)); // youngEase0 statement.setLong(19, card.getInt(18)); // youngEase1 statement.setLong(20, card.getInt(19)); // youngEase2 statement.setLong(21, card.getInt(20)); // youngEase3 statement.setLong(22, card.getInt(21)); // youngEase4 statement.setLong(23, card.getInt(22)); // matureEase0 statement.setLong(24, card.getInt(23)); // matureEase1 statement.setLong(25, card.getInt(24)); // matureEase2 statement.setLong(26, card.getInt(25)); // matureEase3 statement.setLong(27, card.getInt(26)); // matureEase4 statement.setLong(28, card.getInt(27)); // yesCount statement.setLong(29, card.getInt(28)); // noCount statement.setLong(30, card.getInt(29)); // question statement.setString(31, card.getString(30)); // answer statement.setString(32, card.getString(31)); // lastFactor statement.setDouble(33, card.getDouble(32)); // spaceUntil statement.setDouble(34, card.getDouble(33)); // type statement.setLong(35, card.getInt(34)); // combinedDue statement.setDouble(36, card.getDouble(35)); // relativeDelay statement.setString(37, genType(card)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } ankiDB.execSQL("DELETE FROM cardsDeleted WHERE cardId IN " + idsString); } } private String genType(JSONArray row) throws JSONException { if (row.length() >= 37) { return row.getString(36); } if (row.getInt(15) != 0) { return "1"; } else if (row.getInt(14) != 0) { return "0"; } return "2"; } /** * Media */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - getMedia * @throws JSONException */ private JSONArray getMedia(JSONArray ids) throws JSONException { JSONArray media = new JSONArray(); ResultSet result = mDeck.getDB().rawQuery( "SELECT id, filename, size, created, originalPath, description FROM media WHERE id IN " + Utils.ids2str(ids)); try { while (result.next()) { JSONArray m = new JSONArray(); // id m.put(result.getLong(1)); // filename m.put(result.getString(2)); // size m.put(result.getInt(3)); // created m.put(result.getDouble(4)); // originalPath m.put(result.getString(5)); // description m.put(result.getString(6)); media.put(m); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } return media; } private void deleteMedia(JSONArray ids) throws JSONException { log.info("deleteMedia"); String idsString = Utils.ids2str(ids); // Get filenames // files below is never used, so it's commented out // ArrayList<String> files = mDeck.getDB().queryColumn(String.class, "SELECT filename FROM media WHERE id IN " // + idsString, 1); // 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 = ?"; PreparedStatement statement = mDeck.getDB().compileStatement(sqlInsert); int len = ids.length(); for (int i = 0; i < len; i++) { log.info("Inserting media " + ids.getLong(i) + " into mediaDeleted"); try { statement.setLong(1, ids.getLong(i)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Delete media log.info("Deleting media in = " + idsString); mDeck.getDB().execSQL("DELETE FROM media WHERE id IN " + idsString); } void updateMedia(JSONArray media) throws JSONException { AnkiDb ankiDB = mDeck.getDB(); ArrayList<String> mediaIds = new ArrayList<String>(); String sql = "INSERT OR REPLACE INTO media (id, filename, size, created, originalPath, description) " + "VALUES(?,?,?,?,?,?)"; PreparedStatement statement = ankiDB.compileStatement(sql); int len = media.length(); String filename = null; String sum = null; for (int i = 0; i < len; i++) { JSONArray m = media.getJSONArray(i); // Grab media ids, to delete them later String id = m.getString(0); mediaIds.add(id); // id try { statement.setString(1, id); // filename filename = m.getString(1); statement.setString(2, filename); // size statement.setString(3, m.getString(2)); // created statement.setDouble(4, m.getDouble(3)); // originalPath sum = m.getString(4); statement.setString(5, sum); // description statement.setString(6, m.getString(5)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } ankiDB.execSQL("DELETE FROM mediaDeleted WHERE mediaId IN " + Utils.ids2str(mediaIds)); } /** * Deck/Stats/History/Sources * @throws JSONException */ private JSONObject bundleDeck() throws JSONException { JSONObject bundledDeck = new JSONObject(); // Ensure modified is not greater than server time if ((mServer != null) && (mServer.getTimestamp() != 0.0)) { mDeck.setModified(Math.min(mDeck.getModified(), mServer.getTimestamp())); log.info(String.format(Utils.ENGLISH_LOCALE, "Modified: %f", mDeck.getModified())); } // And ensure lastSync is greater than modified mDeck.setLastSync(Math.max(Utils.now(), mDeck.getModified() + 1)); log.info(String.format(Utils.ENGLISH_LOCALE, "LastSync: %f", mDeck.getLastSync())); bundledDeck = mDeck.bundleJson(bundledDeck); // AnkiDroid Deck.java does not have: // css, forceMediaDir, lastSessionStart, lastTags, needLock, // progressHandlerCalled, // progressHandlerEnabled, revCardOrder, sessionStartReps, sessionStartTime, // tmpMediaDir // XXX: this implies that they are not synched toward the server, I guess (tested on 0.7). // However, the ones left are not persisted by libanki on the DB, so it's a libanki bug that they are sync'ed at all. // Our bundleDeck also doesn't need all those fields that store the scheduler Methods // Add meta information of the deck (deckVars table) JSONArray meta = new JSONArray(); ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM deckVars"); try { while (result.next()) { JSONArray deckVar = new JSONArray(); deckVar.put(result.getString(1)); deckVar.put(result.getString(2)); meta.put(deckVar); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } bundledDeck.put("meta", meta); log.info("Deck ="); Utils.printJSONObject(bundledDeck, false); return bundledDeck; } private void updateDeck(JSONObject deckPayload) throws JSONException { JSONArray meta = deckPayload.getJSONArray("meta"); // Update meta information String sqlMeta = "INSERT OR REPLACE INTO deckVars (key, value) VALUES(?,?)"; PreparedStatement statement = mDeck.getDB() .compileStatement(sqlMeta); int lenMeta = meta.length(); for (int i = 0; i < lenMeta; i++) { JSONArray deckVar = meta.getJSONArray(i); // key try { statement.setString(1, deckVar.getString(0)); // value statement.setString(2, deckVar.getString(1)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } // Update deck mDeck.updateFromJson(deckPayload); } private JSONObject bundleStats() throws JSONException { log.info("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.info("lastDay = " + lastDay.toString()); ArrayList<Long> ids = mDeck.getDB().queryColumn(Long.class, "SELECT id FROM stats WHERE type = 1 and day >= \"" + lastDay.toString() + "\"", 1); Stats stat = new Stats(mDeck); // Put global stats bundledStats.put("global", Stats.globalStats(mDeck).bundleJson()); // Put daily stats JSONArray dailyStats = new JSONArray(); if (ids != null) { 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); log.info("Stats ="); Utils.printJSONObject(bundledStats, false); return bundledStats; } private void updateStats(JSONObject stats) throws JSONException { // 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 Long id = mDeck.getDB().queryScalar( "SELECT id FROM stats WHERE type = 1 AND day = \"" + dailyStatDate.toString() + "\""); if (id == -1) { stat.create(Stats.STATS_DAY, dailyStatDate); } else { stat.fromDB(id); } // Update daily stat stat.updateFromJson(remoteStat); } } private JSONArray bundleHistory() throws JSONException { double delay = 0.0; JSONArray bundledHistory = new JSONArray(); ResultSet result = mDeck.getDB().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())); try { while (result.next()) { JSONArray review = new JSONArray(); int i = 1; // cardId review.put(0, (long)result.getLong(i++)); // time review.put(1, (double)result.getDouble(i++)); // lastInterval review.put(2, (double)result.getDouble(i++)); // nextInterval review.put(3, (double)result.getDouble(i++)); // ease review.put(4, (int)result.getInt(i++)); // delay delay = result.getDouble(i++); Number num = Double.valueOf(delay); log.debug(String.format(Utils.ENGLISH_LOCALE, "issue 372 2: %.18f %s %s", delay, num.toString(), review.toString())); review.put(5, delay); log.debug(String.format(Utils.ENGLISH_LOCALE, "issue 372 3: %.18f %s %s", review.getDouble(5), num.toString(), review.toString())); // lastFactor review.put(6, (double)result.getDouble(i++)); log.debug(String.format(Utils.ENGLISH_LOCALE, "issue 372 4: %.18f %s %s", review.getDouble(5), num.toString(), review.toString())); // nextFactor review.put(7, (double)result.getDouble(i++)); // reps review.put(8, (double)result.getDouble(i++)); // thinkingTime review.put(9, (double)result.getDouble(i++)); // yesCount review.put(10, (double)result.getDouble(i++)); // noCount review.put(11, (double)result.getDouble(i++)); log.debug(String.format(Utils.ENGLISH_LOCALE, "issue 372 complete row: %.18f %.18f %s", delay, review.getDouble(5), review.toString())); bundledHistory.put(review); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } log.info("Last sync = " + String.format(Utils.ENGLISH_LOCALE, "%f", mDeck.getLastSync())); log.info("Bundled history = " + bundledHistory.toString()); return bundledHistory; } private void updateHistory(JSONArray history) throws JSONException { String sql = "INSERT OR IGNORE INTO reviewHistory (cardId, time, lastInterval, nextInterval, ease, delay, " + "lastFactor, nextFactor, reps, thinkingTime, yesCount, noCount) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement statement = mDeck.getDB().compileStatement(sql); int len = history.length(); for (int i = 0; i < len; i++) { JSONArray h = history.getJSONArray(i); // cardId try { statement.setLong(1, h.getLong(0)); // time statement.setDouble(2, h.getDouble(1)); // lastInterval statement.setDouble(3, h.getDouble(2)); // nextInterval statement.setDouble(4, h.getDouble(3)); // ease statement.setString(5, h.getString(4)); // delay statement.setDouble(6, h.getDouble(5)); // lastFactor statement.setDouble(7, h.getDouble(6)); // nextFactor statement.setDouble(8, h.getDouble(7)); // reps statement.setDouble(9, h.getDouble(8)); // thinkingTime statement.setDouble(10, h.getDouble(9)); // yesCount statement.setDouble(11, h.getDouble(10)); // noCount statement.setDouble(12, h.getDouble(11)); statement.executeUpdate(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } } private JSONArray bundleSources() throws JSONException { JSONArray bundledSources = new JSONArray(); ResultSet result = mDeck.getDB().rawQuery( "SELECT * FROM sources"); try { while (result.next()) { JSONArray source = new JSONArray(); // id source.put(result.getLong(1)); // name source.put(result.getString(2)); // created source.put(result.getDouble(3)); // lastSync source.put(result.getDouble(4)); // syncPeriod source.put(result.getInt(5)); bundledSources.put(source); } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } log.info("Bundled sources = " + bundledSources); return bundledSources; } private void updateSources(JSONArray sources) throws JSONException { String sql = "INSERT OR REPLACE INTO sources VALUES(?,?,?,?,?)"; PreparedStatement statement = mDeck.getDB().compileStatement(sql); int len = sources.length(); for (int i = 0; i < len; i++) { JSONArray source = sources.getJSONArray(i); try { statement.setLong(1, source.getLong(0)); statement.setString(2, source.getString(1)); statement.setDouble(3, source.getDouble(2)); statement.setDouble(4, source.getDouble(3)); statement.setString(5, source.getString(4)); statement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * Full sync */ /** * Anki Desktop -> libanki/anki/sync.py, SyncTools - needFullSync * * @param sums * @return * @throws JSONException */ public boolean needFullSync(JSONArray sums) throws JSONException { log.info("needFullSync - lastSync = " + mDeck.getLastSync()); if (mDeck.getLastSync() <= 0) { log.info("deck.lastSync <= 0"); return true; } int len = sums.length(); for (int i = 0; i < len; i++) { JSONObject summary = sums.getJSONObject(i); @SuppressWarnings("unchecked") Iterator<String> keys = (Iterator<String>) summary.keys(); while (keys.hasNext()) { String key = keys.next(); JSONArray l = (JSONArray) summary.get(key); log.info("Key " + key + ", length = " + l.length()); if (l.length() > 500) { log.info("Length of key > 500"); return true; } } } AnkiDb ankiDB = mDeck.getDB(); if (ankiDB.queryScalar("SELECT count() FROM reviewHistory WHERE time > " + mDeck.getLastSync()) > 500) { log.info("reviewHistory since lastSync > 500"); return true; } Date lastDay = new Date(java.lang.Math.max(0, (long) (mDeck.getLastSync() - 60 * 60 * 24) * 1000)); log.info("lastDay = " + lastDay.toString() + ", lastDayInMillis = " + lastDay.getTime()); log.info("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.info("stats since lastDay > 100"); return true; } return false; } public String prepareFullSync() { // Ensure modified is not greater than server time mDeck.setModified(Math.min(mDeck.getModified(), mServer.getTimestamp())); mDeck.commitToDB(); // The deck is closed after the full sync is completed if (mLocalTime > mRemoteTime) { return "fromLocal"; } else { return "fromServer"; } } public static HashMap<String, String> fullSyncFromLocal(String password, String username, Deck deck, String deckName) { HashMap<String, String> result = new HashMap<String, String>(); Throwable exc = null; try { log.info("Fullup"); // We need to write the output to a temporary file, so that FileEntity knows the length String tmpPath = (new File(deck.getDeckPath())).getParent(); File tmpFile = new File(tmpPath + "/fulluploadPayload.tmp"); if (tmpFile.exists()) { tmpFile.delete(); } log.info("Writing temporary payload file..."); tmpFile.createNewFile(); DataOutputStream tmp = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(tmpFile))); tmp.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); tmp.writeBytes("Content-Disposition: form-data; name=\"p\"" + END + END + password + END); tmp.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); tmp.writeBytes("Content-Disposition: form-data; name=\"u\"" + END + END + username + END); tmp.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); tmp.writeBytes("Content-Disposition: form-data; name=\"d\"" + END + END); tmp.write(deckName.getBytes("UTF-8")); tmp.writeBytes(END); tmp.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + END); tmp.writeBytes("Content-Disposition: form-data; name=\"deck\"; filename=\"deck\"" + END); tmp.writeBytes("Content-Type: application/octet-stream" + END); tmp.writeBytes(END); String deckPath = deck.getDeckPath(); 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(tmp, deflater); while ((length = fStream.read(buffer)) != -1) { dos.write(buffer, 0, length); log.info("Length = " + length); } dos.finish(); fStream.close(); tmp.writeBytes(END); tmp.writeBytes(TWO_HYPHENS + MIME_BOUNDARY + TWO_HYPHENS + END + END); tmp.flush(); tmp.close(); log.info("Payload file ready, size: " + tmpFile.length()); HttpPost httpPost = new HttpPost(AnkiDroidProxy.SYNC_URL + "fullup?v=" + URLEncoder.encode(AnkiDroidProxy.SYNC_VERSION, "UTF-8")); httpPost.setHeader("Content-type", "multipart/form-data; boundary=" + MIME_BOUNDARY); httpPost.addHeader("Host", AnkiDroidProxy.SYNC_HOST); httpPost.setEntity(new FileEntity(tmpFile, "application/octet-stream")); DefaultHttpClient httpClient = new DefaultHttpClient(); HttpResponse resp = httpClient.execute(httpPost); // Ensure we got the HTTP 200 response code String response = Utils.convertStreamToString(resp.getEntity().getContent()); int responseCode = resp.getStatusLine().getStatusCode(); log.info("Response code = " + responseCode); // Read the response if (response.substring(0,2).equals("OK")) { // Update lastSync deck.setLastSync(Double.parseDouble(response.substring(3, response.length()-3))); deck.commitToDB(); // Make sure we don't set modified later than lastSync when we do closeDeck later: deck.setLastLoaded(deck.getModified()); // Remove temp file tmpFile.delete(); } log.info("Finished!"); result.put("code", String.valueOf(responseCode)); result.put("message", response); } catch (ClientProtocolException e) { log.error("ClientProtocolException", e); result.put("code", "ClientProtocolException"); exc = e; } catch (UnsupportedEncodingException e) { log.error("UnsupportedEncodingException", e); result.put("code", "UnsupportedEncodingException"); exc = e; } catch (MalformedURLException e) { log.error("MalformedURLException", e); result.put("code", "MalformedURLException"); exc = e; } catch (IOException e) { log.error("IOException", e); result.put("code", "IOException"); exc = e; } if (exc != null) { // Sometimes the exception has null message and we have to get it from its cause while (exc.getMessage() == null && exc.getCause() != null) { exc = exc.getCause(); } result.put("message", exc.getMessage()); } return result; } public static HashMap<String, String> fullSyncFromServer(String password, String username, String deckName, String deckPath) { HashMap<String, String> result = new HashMap<String, String>(); Throwable exc = null; try { String data = "p=" + URLEncoder.encode(password, "UTF-8") + "&u=" + URLEncoder.encode(username, "UTF-8") + "&d=" + URLEncoder.encode(deckName, "UTF-8"); // log.info("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); HttpEntity entityResponse = response.getEntity(); InputStream content = entityResponse.getContent(); int responseCode = response.getStatusLine().getStatusCode(); String tempDeckPath = deckPath + ".tmp"; if (responseCode == 200) { Utils.writeToFile(new InflaterInputStream(content), tempDeckPath); File newFile = new File(tempDeckPath); //File oldFile = new File(deckPath); if (newFile.renameTo(new File(deckPath))) { result.put("code", "200"); } else { result.put("code", "PermissionError"); result.put("message", "Can't overwrite old deck with downloaded from server"); } } else { result.put("code", String.valueOf(responseCode)); result.put("message", Utils.convertStreamToString(content)); } } catch (UnsupportedEncodingException e) { log.error("UnsupportedEncodingException", e); result.put("code", "UnsupportedEncodingException"); exc = e; } catch (ClientProtocolException e) { log.error("ClientProtocolException", e); result.put("code", "ClientProtocolException"); exc = e; } catch (IOException e) { log.error("IOException", e); result.put("code", "IOException"); exc = e; } if (exc != null) { // Sometimes the exception has null message and we have to get it from its cause while (exc.getMessage() == null && exc.getCause() != null) { exc = exc.getCause(); } result.put("message", exc.getMessage()); } return result; } }