private void undoredo(Stack<UndoRow> src, Stack<UndoRow> dst) { UndoRow row; commitToDB(); while (true) { row = src.pop(); if (row != null) break; } Long start = row.start; Long end = row.end; if (end == null) end = latestUndoRow(); ArrayList<String> sql = AnkiDb.queryColumn( String.class, String.format( ENGLISH_LOCALE, "SELECT sql FROM undoLog " + "WHERE seq > %d and seq <= %d " + "ORDER BY seq DESC", start, end), 0); Long newstart = latestUndoRow(); Iterator<String> iter = sql.iterator(); while (iter.hasNext()) AnkiDb.database.execSQL(iter.next()); Long newend = latestUndoRow(); dst.push(new UndoRow(row.name, newstart, newend)); }
private long getRevCard() { long id; try { id = AnkiDb.queryScalar("SELECT id " + "FROM " + revCardTable() + "LIMIT 1"); } catch (Exception e) { return 0; } return id; }
public static double getLastModified(String deckPath) { double value; Cursor cursor = null; // Log.i(TAG, "Deck - getLastModified from deck = " + deckPath); AnkiDb.openDatabase(deckPath); try { cursor = AnkiDb.database.rawQuery("SELECT modified" + " FROM decks" + " LIMIT 1", null); if (!cursor.moveToFirst()) value = -1; else value = cursor.getDouble(0); } finally { if (cursor != null) cursor.close(); } AnkiDb.closeDatabase(); return value; }
private long latestUndoRow() { long result; try { result = AnkiDb.queryScalar("SELECT MAX(rowid) FROM undoLog"); } catch (SQLException e) { result = 0; } return result; }
private long getCardId() { long id; // Failed card due? if ((delay0 != 0) && (failedNowCount != 0)) return AnkiDb.queryScalar("SELECT id FROM failedCards LIMIT 1"); // Failed card queue too big? if ((failedCardMax != 0) && (failedSoonCount >= failedCardMax)) return AnkiDb.queryScalar("SELECT id FROM failedCards LIMIT 1"); // Distribute new cards? if (timeForNewCard()) { id = maybeGetNewCard(); if (id != 0) return id; } // Card due for review? if (revCount != 0) return getRevCard(); // New cards left? id = maybeGetNewCard(); if (id != 0) return id; // Review ahead? if (reviewEarly) { id = getCardIdAhead(); if (id != 0) return id; else { resetAfterReviewEarly(); checkDue(); } } // Display failed cards early/last if (showFailedLast()) { try { id = AnkiDb.queryScalar("SELECT id FROM failedCards LIMIT 1"); } catch (Exception e) { return 0; } return id; } return 0; }
private long getCardIdAhead() { long id = 0; try { id = AnkiDb.queryScalar( "SELECT id " + "FROM cards " + "WHERE type = 1 and " + "isDue = 0 and " + "priority in (1,2,3,4) " + "ORDER BY combinedDue " + "LIMIT 1"); } catch (SQLException e) { return 0; } return id; }
private boolean timeForNewCard() { if (newCardSpacing == NEW_CARDS_LAST) return false; if (newCardSpacing == NEW_CARDS_FIRST) return true; // Force old if there are very high priority cards try { AnkiDb.queryScalar( "SELECT 1 " + "FROM cards " + "WHERE type = 1 and " + "isDue = 1 and " + "priority = 4 " + "LIMIT 1"); } catch (Exception e) { // No result from query. if (newCardModulus == 0) return false; else return (dailyStats.reps % newCardModulus) == 0; } return false; }
private void rebuildCounts(boolean full) { Log.i(TAG, "rebuildCounts - Rebuilding global and due counts..."); // Need to check due first, so new due cards are not added later checkDue(); // Global counts if (full) { cardCount = (int) AnkiDb.queryScalar("SELECT count(id) FROM cards"); factCount = (int) AnkiDb.queryScalar("SELECT count(id) FROM facts"); } // Due counts failedSoonCount = (int) AnkiDb.queryScalar("SELECT count(id) FROM failedCards"); failedNowCount = (int) AnkiDb.queryScalar( "SELECT count(id) " + "FROM cards " + "WHERE type = 0 and " + "isDue = 1 and " + "combinedDue <= " + String.format( ENGLISH_LOCALE, "%f", (double) (System.currentTimeMillis() / 1000.0))); revCount = (int) AnkiDb.queryScalar( "SELECT count(id) " + "FROM cards " + "WHERE type = 1 and " + "priority in (1,2,3,4) and " + "isDue = 1"); newCount = (int) AnkiDb.queryScalar( "SELECT count(id) " + "FROM cards " + "WHERE type = 2 and " + "priority in (1,2,3,4) and " + "isDue = 1"); }
private Payload doInBackgroundUpgradeDecks(Payload data) { String path = (String) data.data[0]; File ankiDir = new File(path); if (!ankiDir.isDirectory()) { data.success = false; return data; } // step 1: gather all .anki files into a zip, without media. // we must store them as 1.anki, 2.anki and provide a map so we don't run into // encoding issues with the zip file. File[] fileList = ankiDir.listFiles(new OldAnkiDeckFilter()); JSONObject map = new JSONObject(); byte[] buf = new byte[1024]; String zipFilename = path + "/upload.zip"; String colFilename = path + "/collection.anki2"; try { ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilename)); int n = 1; for (File f : fileList) { String tmpName = n + ".anki"; FileInputStream in = new FileInputStream(f.getAbsolutePath()); ZipEntry ze = new ZipEntry(tmpName); zos.putNextEntry(ze); int len; while ((len = in.read(buf)) > 0) { zos.write(buf, 0, len); } zos.closeEntry(); map.put(tmpName, f.getName()); n++; } ZipEntry ze = new ZipEntry("map.json"); zos.putNextEntry(ze); InputStream in = new ByteArrayInputStream(map.toString().getBytes("UTF-8")); int len; while ((len = in.read(buf)) > 0) { zos.write(buf, 0, len); } zos.closeEntry(); zos.close(); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } catch (JSONException e) { throw new RuntimeException(e); } File zipFile = new File(zipFilename); // step 1.1: if it's over 50MB compressed, it must be upgraded by the user if (zipFile.length() > 50 * 1024 * 1024) { data.success = false; return data; } // step 2: upload zip file to upgrade service and get token BasicHttpSyncer h = new BasicHttpSyncer(null, null); // note: server doesn't expect it to be gzip compressed, because the zip file is compressed publishProgress(new Object[] {R.string.upgrade_decks_upload}); try { HttpResponse resp = h.req("upgrade/upload", new FileInputStream(zipFile), 0, false); String result = h.stream2String(resp.getEntity().getContent()); String key; if (result.startsWith("ok:")) { key = result.split(":")[1]; } else { data.success = false; return data; } while (true) { result = h.stream2String(h.req("upgrade/status?key=" + key).getEntity().getContent()); if (result.equals("error")) { data.success = false; return data; } else if (result.startsWith("waiting:")) { publishProgress(new Object[] {R.string.upgrade_decks_upload, result.split(":")[1]}); } else if (result.equals("upgrading")) { publishProgress(new Object[] {R.string.upgrade_decks_upgrade_started}); } else if (result.equals("ready")) { break; } else { data.success = false; return data; } Thread.sleep(1000); } // step 4: fetch upgraded file. this will return the .anki2 file directly, with // gzip compression if the client says it can handle it publishProgress(new Object[] {R.string.upgrade_decks_downloading}); resp = h.req("upgrade/download?key=" + key); if (resp == null) { data.success = false; return data; } // step 5: check the received file is valid InputStream cont = resp.getEntity().getContent(); if (!h.writeToFile(cont, colFilename)) { data.success = false; return data; } // check the received file is ok publishProgress(new Object[] {R.string.sync_check_download_file}); publishProgress(R.string.sync_check_download_file); try { AnkiDb d = AnkiDatabaseManager.getDatabase(colFilename); if (!d.queryString("PRAGMA integrity_check").equalsIgnoreCase("ok")) { data.success = false; return data; } } finally { AnkiDatabaseManager.closeDatabase(colFilename); } data.success = true; return data; } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (IllegalStateException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } finally { (new File(zipFilename)).delete(); } }
/** Mark expired cards due and update counts. */ private void checkDue() { Log.i(TAG, "Checking due cards..."); checkDailyStats(); // Failed cards ContentValues val = new ContentValues(1); val.put("isDue", 1); failedSoonCount += AnkiDb.database.update( "cards", val, "type = 0 and " + "isDue = 0 and " + "priority in (1,2,3,4) and " + String.format( ENGLISH_LOCALE, "combinedDue <= %f", (double) ((System.currentTimeMillis() / 1000.0) + delay0)), null); failedNowCount = (int) AnkiDb.queryScalar( "SELECT count(id) " + "FROM cards " + "WHERE type = 0 and " + "isDue = 1 and " + String.format( ENGLISH_LOCALE, "combinedDue <= %f", (double) (System.currentTimeMillis() / 1000.0))); // Review val.clear(); val.put("isDue", 1); revCount += AnkiDb.database.update( "cards", val, "type = 1 and " + "isDue = 0 and " + "priority in (1,2,3,4) and " + String.format( ENGLISH_LOCALE, "combinedDue <= %f", (double) (System.currentTimeMillis() / 1000.0)), null); // New val.clear(); val.put("isDue", 1); newCount += AnkiDb.database.update( "cards", val, "type = 2 and " + "isDue = 0 and " + "priority in (1,2,3,4) and " + String.format( ENGLISH_LOCALE, "combinedDue <= %f", (double) (System.currentTimeMillis() / 1000.0)), null); newCountToday = Math.max(Math.min(newCount, newCardsPerDay - newCardsToday()), 0); }
public void closeDeck() { DeckTask.waitToFinish(); // Wait for any thread working on the deck to finish. if (modifiedSinceSave()) commitToDB(); AnkiDb.closeDatabase(); }
public static Deck openDeck(String path) throws SQLException { Deck deck = null; Cursor cursor = null; Log.i(TAG, "openDeck - Opening database " + path); AnkiDb.openDatabase(path); try { // Read in deck table columns cursor = AnkiDb.database.rawQuery("SELECT *" + " FROM decks" + " LIMIT 1", null); if (!cursor.moveToFirst()) return null; deck = new Deck(); deck.id = cursor.getLong(0); deck.created = cursor.getDouble(1); deck.modified = cursor.getDouble(2); deck.description = cursor.getString(3); deck.version = cursor.getInt(4); deck.currentModelId = cursor.getLong(5); deck.syncName = cursor.getString(6); deck.lastSync = cursor.getDouble(7); deck.hardIntervalMin = cursor.getDouble(8); deck.hardIntervalMax = cursor.getDouble(9); deck.midIntervalMin = cursor.getDouble(10); deck.midIntervalMax = cursor.getDouble(11); deck.easyIntervalMin = cursor.getDouble(12); deck.easyIntervalMax = cursor.getDouble(13); deck.delay0 = cursor.getDouble(14); deck.delay1 = cursor.getDouble(15); deck.delay2 = cursor.getDouble(16); deck.collapseTime = cursor.getDouble(17); deck.highPriority = cursor.getString(18); deck.medPriority = cursor.getString(19); deck.lowPriority = cursor.getString(20); deck.suspended = cursor.getString(21); deck.newCardOrder = cursor.getInt(22); deck.newCardSpacing = cursor.getInt(23); deck.failedCardMax = cursor.getInt(24); deck.newCardsPerDay = cursor.getInt(25); deck.sessionRepLimit = cursor.getInt(26); deck.sessionTimeLimit = cursor.getInt(27); deck.utcOffset = cursor.getDouble(28); deck.cardCount = cursor.getInt(29); deck.factCount = cursor.getInt(30); deck.failedNowCount = cursor.getInt(31); deck.failedSoonCount = cursor.getInt(32); deck.revCount = cursor.getInt(33); deck.newCount = cursor.getInt(34); deck.revCardOrder = cursor.getInt(35); Log.i(TAG, "openDeck - Read " + cursor.getColumnCount() + " columns from decks table."); } finally { if (cursor != null) cursor.close(); } Log.i( TAG, String.format( ENGLISH_LOCALE, "openDeck - modified: %f currentTime: %f", deck.modified, System.currentTimeMillis() / 1000.0)); deck.initVars(); // Ensure necessary indices are available deck.updateDynamicIndices(); // Save counts to determine if we should save deck after check int oldCount = deck.failedSoonCount + deck.revCount + deck.newCount; // Update counts deck.rebuildQueue(); try { // Unsuspend reviewed early & buried cursor = AnkiDb.database.rawQuery( "SELECT id " + "FROM cards " + "WHERE type in (0,1,2) and " + "isDue = 0 and " + "priority in (-1,-2)", null); if (cursor.moveToFirst()) { int count = cursor.getCount(); long[] ids = new long[count]; for (int i = 0; i < count; i++) { ids[i] = cursor.getLong(0); cursor.moveToNext(); } deck.updatePriorities(ids); deck.checkDue(); } } finally { if (cursor != null) cursor.close(); } // Save deck to database if it has been modified if ((oldCount != (deck.failedSoonCount + deck.revCount + deck.newCount)) || deck.modifiedSinceSave()) deck.commitToDB(); // Create a temporary view for random new cards. Randomizing the cards by themselves // as is done in desktop Anki in Deck.randomizeNewCards() takes too long. AnkiDb.database.execSQL( "CREATE TEMPORARY VIEW acqCardsRandom AS " + "SELECT * FROM cards " + "WHERE type = 2 AND isDue = 1 " + "ORDER BY RANDOM()"); return deck; }
private void initUndo() { undoStack = new Stack<UndoRow>(); redoStack = new Stack<UndoRow>(); undoEnabled = true; AnkiDb.database.execSQL( "CREATE TEMPORARY TABLE undoLog (seq INTEGER PRIMARY KEY NOT NULL, sql TEXT)"); ArrayList<String> tables = AnkiDb.queryColumn(String.class, "SELECT name FROM sqlite_master WHERE type = 'table'", 0); Iterator<String> iter = tables.iterator(); while (iter.hasNext()) { String table = iter.next(); if (table.equals("undoLog") || table.equals("sqlite_stat1")) continue; ArrayList<String> columns = AnkiDb.queryColumn(String.class, "PRAGMA TABLE_INFO(" + table + ")", 1); // Insert trigger String sql = "CREATE TEMP TRIGGER _undo_%s_it " + "AFTER INSERT ON %s BEGIN " + "INSERT INTO undoLog VALUES " + "(null, 'DELETE FROM %s WHERE rowid = ' || new.rowid); END"; AnkiDb.database.execSQL(String.format(ENGLISH_LOCALE, sql, table, table, table)); // Update trigger sql = String.format( ENGLISH_LOCALE, "CREATE TEMP TRIGGER _undo_%s_ut " + "AFTER UPDATE ON %s BEGIN " + "INSERT INTO undoLog VALUES " + "(null, 'UPDATE %s ", table, table, table); String sep = "SET "; Iterator<String> columnIter = columns.iterator(); while (columnIter.hasNext()) { String column = columnIter.next(); if (column.equals("unique")) continue; sql += String.format(ENGLISH_LOCALE, "%s%s=' || quote(old.%s) || '", sep, column, column); sep = ","; } sql += "WHERE rowid = ' || old.rowid); END"; AnkiDb.database.execSQL(sql); // Delete trigger sql = String.format( ENGLISH_LOCALE, "CREATE TEMP TRIGGER _undo_%s_dt " + "BEFORE DELETE ON %s BEGIN " + "INSERT INTO undoLog VALUES " + "(null, 'INSERT INTO %s (rowid", table, table, table); columnIter = columns.iterator(); while (columnIter.hasNext()) { String column = columnIter.next(); sql += String.format(ENGLISH_LOCALE, ",\"%s\"", column); } sql += ") VALUES (' || old.rowid ||'"; columnIter = columns.iterator(); while (columnIter.hasNext()) { String column = columnIter.next(); if (column.equals("unique")) { sql += ",1"; continue; } sql += String.format(ENGLISH_LOCALE, ", ' || quote(old.%s) ||'", column); } sql += ")'); END"; AnkiDb.database.execSQL(sql); } }