/*************************************************************************************** * Copyright (c) 2011 Norbert Nagold <norbert.nagold@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.libanki; import android.content.ContentValues; import android.content.Context; import com.ichi2.anki.exception.ConfirmModSchemaException; import com.ichi2.libanki.hooks.Hooks; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import timber.log.Timber; public class Storage { String mPath; /* Open a new or existing collection. Path must be unicode */ public static Collection Collection(Context context, String path) { return Collection(context, path, false, false); } public static Collection Collection(Context context, String path, boolean server, boolean log) { assert path.endsWith(".anki2"); // Since this is the entry point into libanki, initialize the hooks here. Hooks.getInstance(context); File dbFile = new File(path); boolean create = !dbFile.exists(); // connect DB db = new DB(path); try { // initialize int ver; if (create) { ver = _createDB(db); } else { ver = _upgradeSchema(db); } db.execute("PRAGMA temp_store = memory"); // add db to col and do any remaining upgrades Collection col = new Collection(context, db, path, server, log); if (ver < Consts.SCHEMA_VERSION) { _upgrade(col, ver); } else if (create) { try { // add in reverse order so basic is default Models.addClozeModel(col); Models.addForwardOptionalReverse(col); Models.addForwardReverse(col); Models.addBasicModel(col); } catch (ConfirmModSchemaException e) { // This should never reached as we've just created a new database throw new RuntimeException(e); } col.save(); } return col; } catch (Exception e) { Timber.e(e, "Error opening collection; closing database"); db.close(); throw e; } } private static int _upgradeSchema(DB db) { int ver = db.queryScalar("SELECT ver FROM col"); if (ver == Consts.SCHEMA_VERSION) { return ver; } // add odid to cards, edue->odue if (db.queryScalar("SELECT ver FROM col") == 1) { db.execute("ALTER TABLE cards RENAME TO cards2"); _addSchema(db, false); db.execute("insert into cards select id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, reps, lapses, left, edue, 0, flags, data from cards2"); db.execute("DROP TABLE cards2"); db.execute("UPDATE col SET ver = 2"); _updateIndices(db); } // remove did from notes if (db.queryScalar("SELECT ver FROM col") == 2) { db.execute("ALTER TABLE notes RENAME TO notes2"); _addSchema(db, false); db.execute("insert into notes select id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data from notes2"); db.execute("DROP TABLE notes2"); db.execute("UPDATE col SET ver = 3"); _updateIndices(db); } return ver; } private static void _upgrade(Collection col, int ver) { try { if (ver < 3) { // new deck properties for (JSONObject d : col.getDecks().all()) { d.put("dyn", 0); d.put("collapsed", false); col.getDecks().save(d); } } if (ver < 4) { col.modSchemaNoCheck(); ArrayList<JSONObject> clozes = new ArrayList<>(); for (JSONObject m : col.getModels().all()) { if (!m.getJSONArray("tmpls").getJSONObject(0).getString("qfmt").contains("{{cloze:")) { m.put("type", Consts.MODEL_STD); } else { clozes.add(m); } } for (JSONObject m : clozes) { try { _upgradeClozeModel(col, m); } catch (ConfirmModSchemaException e) { // Will never be reached as we already set modSchemaNoCheck() throw new RuntimeException(e); } } col.getDb().execute("UPDATE col SET ver = 4"); } if (ver < 5) { col.getDb().execute("UPDATE cards SET odue = 0 WHERE queue = 2"); col.getDb().execute("UPDATE col SET ver = 5"); } if (ver < 6) { col.modSchemaNoCheck(); for (JSONObject m : col.getModels().all()) { m.put("css", new JSONObject(Models.defaultModel).getString("css")); JSONArray ar = m.getJSONArray("tmpls"); for (int i = 0; i < ar.length(); i++) { JSONObject t = ar.getJSONObject(i); if (!t.has("css")) { continue; } m.put("css", m.getString("css") + "\n" + t.getString("css").replace(".card ", ".card" + t.getInt("ord") + 1)); t.remove("css"); } col.getModels().save(m); } col.getDb().execute("UPDATE col SET ver = 6"); } if (ver < 7) { col.modSchemaNoCheck(); col.getDb().execute("UPDATE cards SET odue = 0 WHERE (type = 1 OR queue = 2) AND NOT odid"); col.getDb().execute("UPDATE col SET ver = 7"); } if (ver < 8) { col.modSchemaNoCheck(); col.getDb().execute("UPDATE cards SET due = due / 1000 WHERE due > 4294967296"); col.getDb().execute("UPDATE col SET ver = 8"); } if (ver < 9) { col.getDb().execute("UPDATE col SET ver = 9"); } if (ver < 10) { col.getDb().execute("UPDATE cards SET left = left + left * 1000 WHERE queue = 1"); col.getDb().execute("UPDATE col SET ver = 10"); } if (ver < 11) { col.modSchemaNoCheck(); for (JSONObject d : col.getDecks().all()) { if (d.getInt("dyn") != 0) { int order = d.getInt("order"); // failed order was removed if (order >= 5) { order -= 1; } JSONArray ja = new JSONArray(Arrays.asList(new Object[] { d.getString("search"), d.getInt("limit"), order })); d.put("terms", new JSONArray()); d.getJSONArray("terms").put(0, ja); d.remove("search"); d.remove("limit"); d.remove("order"); d.put("resched", true); d.put("return", true); } else { if (!d.has("extendNew")) { d.put("extendNew", 10); d.put("extendRev", 50); } } col.getDecks().save(d); } for (JSONObject c : col.getDecks().allConf()) { JSONObject r = c.getJSONObject("rev"); r.put("ivlFct", r.optDouble("ivlFct", 1)); if (r.has("ivlfct")) { r.remove("ivlfct"); } r.put("maxIvl", 36500); col.getDecks().save(c); } for (JSONObject m : col.getModels().all()) { JSONArray tmpls = m.getJSONArray("tmpls"); for (int ti = 0; ti < tmpls.length(); ++ti) { JSONObject t = tmpls.getJSONObject(ti); t.put("bqfmt", ""); t.put("bafmt", ""); } col.getModels().save(m); } col.getDb().execute("update col set ver = 11"); } } catch (JSONException e) { throw new RuntimeException(e); } } private static void _upgradeClozeModel(Collection col, JSONObject m) throws ConfirmModSchemaException { try { m.put("type", Consts.MODEL_CLOZE); // convert first template JSONObject t = m.getJSONArray("tmpls").getJSONObject(0); for (String type : new String[] { "qfmt", "afmt" }) { t.put(type, t.getString(type).replaceAll("\\{\\{cloze:1:(.+?)\\}\\}", "{{cloze:$1}}")); } t.put("name", "Cloze"); // delete non-cloze cards for the model JSONArray ja = m.getJSONArray("tmpls"); ArrayList<JSONObject> rem = new ArrayList<>(); for (int i = 1; i < ja.length(); i++) { JSONObject ta = ja.getJSONObject(i); if (!ta.getString("afmt").contains("{{cloze:")) { rem.add(ta); } } for (JSONObject r : rem) { col.getModels().remTemplate(m, r); } JSONArray newArray = new JSONArray(); newArray.put(ja.get(0)); m.put("tmpls", newArray); col.getModels()._updateTemplOrds(m); col.getModels().save(m); } catch (JSONException e) { throw new RuntimeException(e); } } private static int _createDB(DB db) { db.execute("PRAGMA page_size = 4096"); db.execute("PRAGMA legacy_file_format = 0"); db.execute("VACUUM"); _addSchema(db); _updateIndices(db); db.execute("ANALYZE"); return Consts.SCHEMA_VERSION; } private static void _addSchema(DB db) { _addSchema(db, true); } private static void _addSchema(DB db, boolean setColConf) { db.execute("create table if not exists col ( " + "id integer primary key, " + "crt integer not null," + "mod integer not null," + "scm integer not null," + "ver integer not null," + "dty integer not null," + "usn integer not null," + "ls integer not null," + "conf text not null," + "models text not null," + "decks text not null," + "dconf text not null," + "tags text not null" + ");"); db.execute("create table if not exists notes (" + " id integer primary key, /* 0 */" + " guid text not null, /* 1 */" + " mid integer not null, /* 2 */" + " mod integer not null, /* 3 */" + " usn integer not null, /* 4 */" + " tags text not null, /* 5 */" + " flds text not null, /* 6 */" + " sfld integer not null, /* 7 */" + " csum integer not null, /* 8 */" + " flags integer not null, /* 9 */" + " data text not null /* 10 */" + ");"); db.execute("create table if not exists cards (" + " id integer primary key, /* 0 */" + " nid integer not null, /* 1 */" + " did integer not null, /* 2 */" + " ord integer not null, /* 3 */" + " mod integer not null, /* 4 */" + " usn integer not null, /* 5 */" + " type integer not null, /* 6 */" + " queue integer not null, /* 7 */" + " due integer not null, /* 8 */" + " ivl integer not null, /* 9 */" + " factor integer not null, /* 10 */" + " reps integer not null, /* 11 */" + " lapses integer not null, /* 12 */" + " left integer not null, /* 13 */" + " odue integer not null, /* 14 */" + " odid integer not null, /* 15 */" + " flags integer not null, /* 16 */" + " data text not null /* 17 */" + ");"); db.execute("create table if not exists revlog (" + " id integer primary key," + " cid integer not null," + " usn integer not null," + " ease integer not null," + " ivl integer not null," + " lastIvl integer not null," + " factor integer not null," + " time integer not null," + " type integer not null" + ");"); db.execute("create table if not exists graves (" + " usn integer not null," + " oid integer not null," + " type integer not null" + ")"); db.execute("INSERT OR IGNORE INTO col VALUES(1,0,0," + Utils.intNow(1000) + "," + Consts.SCHEMA_VERSION + ",0,0,0,'','{}','','','{}')"); if (setColConf) { _setColVars(db); } } private static void _setColVars(DB db) { try { JSONObject g = new JSONObject(Decks.defaultDeck); g.put("id", 1); g.put("name", "Default"); g.put("conf", 1); g.put("mod", Utils.intNow()); JSONObject gc = new JSONObject(Decks.defaultConf); gc.put("id", 1); JSONObject ag = new JSONObject(); ag.put("1", g); JSONObject agc = new JSONObject(); agc.put("1", gc); ContentValues values = new ContentValues(); values.put("conf", Collection.defaultConf); values.put("decks", Utils.jsonToString(ag)); values.put("dconf", Utils.jsonToString(agc)); db.update("col", values); } catch (JSONException e) { throw new RuntimeException(e); } } private static void _updateIndices(DB db) { db.execute("create index if not exists ix_notes_usn on notes (usn);"); db.execute("create index if not exists ix_cards_usn on cards (usn);"); db.execute("create index if not exists ix_revlog_usn on revlog (usn);"); db.execute("create index if not exists ix_cards_nid on cards (nid);"); db.execute("create index if not exists ix_cards_sched on cards (did, queue, due);"); db.execute("create index if not exists ix_revlog_cid on revlog (cid);"); db.execute("create index if not exists ix_notes_csum on notes (csum);)"); } public static void addIndices(DB db) { _updateIndices(db); } }