1 package org.coolreader.db; 2 3 import android.database.Cursor; 4 import android.database.DatabaseUtils; 5 import android.database.SQLException; 6 import android.database.sqlite.SQLiteException; 7 import android.database.sqlite.SQLiteStatement; 8 import android.util.Log; 9 10 import org.coolreader.crengine.BookInfo; 11 import org.coolreader.crengine.Bookmark; 12 import org.coolreader.crengine.DocumentFormat; 13 import org.coolreader.crengine.FileInfo; 14 import org.coolreader.crengine.L; 15 import org.coolreader.crengine.Logger; 16 import org.coolreader.crengine.MountPathCorrector; 17 import org.coolreader.crengine.OPDSConst; 18 import org.coolreader.crengine.Services; 19 import org.coolreader.crengine.Utils; 20 import org.coolreader.genrescollection.GenresCollection; 21 22 import java.util.ArrayList; 23 import java.util.Collection; 24 import java.util.Collections; 25 import java.util.HashMap; 26 import java.util.Iterator; 27 import java.util.Map; 28 29 public class MainDB extends BaseDB { 30 public static final Logger log = L.create("mdb"); 31 public static final Logger vlog = L.create("mdb", Log.VERBOSE); 32 33 private boolean pathCorrectionRequired = false; 34 public final int DB_VERSION = 34; 35 @Override upgradeSchema()36 protected boolean upgradeSchema() { 37 // When the database is just created, its version is 0. 38 int currentVersion = mDB.getVersion(); 39 //int currentVersion = 32; 40 // TODO: check database structure consistency regardless of its version. 41 if (currentVersion > DB_VERSION) { 42 // trying to update the structure of a database that has been modified by some kind of inconsistent fork of the program. 43 log.v("MainDB: incompatible database version found (" + currentVersion + "), forced setting to 26."); 44 currentVersion = 26; 45 } 46 if (mDB.needUpgrade(DB_VERSION) || currentVersion < DB_VERSION) { 47 execSQL("CREATE TABLE IF NOT EXISTS author (" + 48 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 49 "name VARCHAR NOT NULL COLLATE NOCASE" + 50 ")"); 51 execSQL("CREATE INDEX IF NOT EXISTS " + 52 "author_name_index ON author (name) "); 53 execSQL("CREATE TABLE IF NOT EXISTS series (" + 54 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 55 "name VARCHAR NOT NULL COLLATE NOCASE" + 56 ")"); 57 execSQL("CREATE INDEX IF NOT EXISTS " + 58 "series_name_index ON series (name) "); 59 execSQL("CREATE TABLE IF NOT EXISTS folder (" + 60 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 61 "name VARCHAR NOT NULL" + 62 ")"); 63 execSQL("CREATE INDEX IF NOT EXISTS " + 64 "folder_name_index ON folder (name) "); 65 execSQL("CREATE TABLE IF NOT EXISTS book (" + 66 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 67 "pathname VARCHAR NOT NULL," + 68 "folder_fk INTEGER REFERENCES folder (id)," + 69 "filename VARCHAR NOT NULL," + 70 "arcname VARCHAR," + 71 "title VARCHAR COLLATE NOCASE," + 72 "series_fk INTEGER REFERENCES series (id)," + 73 "series_number INTEGER," + 74 "format INTEGER," + 75 "filesize INTEGER," + 76 "arcsize INTEGER," + 77 "create_time INTEGER," + 78 "last_access_time INTEGER, " + 79 "flags INTEGER DEFAULT 0, " + 80 "language VARCHAR DEFAULT NULL, " + 81 "description TEXT DEFAULT NULL, " + 82 "crc32 INTEGER DEFAULT NULL, " + 83 "domVersion INTEGER DEFAULT 0, " + 84 "rendFlags INTEGER DEFAULT 0" + 85 ")"); 86 execSQL("CREATE INDEX IF NOT EXISTS " + 87 "book_folder_index ON book (folder_fk) "); 88 execSQL("CREATE UNIQUE INDEX IF NOT EXISTS " + 89 "book_pathname_index ON book (pathname) "); 90 execSQL("CREATE INDEX IF NOT EXISTS " + 91 "book_filename_index ON book (filename) "); 92 execSQL("CREATE INDEX IF NOT EXISTS " + 93 "book_title_index ON book (title) "); 94 execSQL("CREATE INDEX IF NOT EXISTS " + 95 "book_last_access_time_index ON book (last_access_time) "); 96 execSQL("CREATE INDEX IF NOT EXISTS " + 97 "book_title_index ON book (title) "); 98 execSQL("CREATE TABLE IF NOT EXISTS book_author (" + 99 "book_fk INTEGER NOT NULL REFERENCES book (id)," + 100 "author_fk INTEGER NOT NULL REFERENCES author (id)," + 101 "PRIMARY KEY (book_fk, author_fk)" + 102 ")"); 103 execSQL("CREATE UNIQUE INDEX IF NOT EXISTS " + 104 "author_book_index ON book_author (author_fk, book_fk) "); 105 execSQL("CREATE TABLE IF NOT EXISTS bookmark (" + 106 "id INTEGER PRIMARY KEY AUTOINCREMENT," + 107 "book_fk INTEGER NOT NULL REFERENCES book (id)," + 108 "type INTEGER NOT NULL DEFAULT 0," + 109 "percent INTEGER DEFAULT 0," + 110 "shortcut INTEGER DEFAULT 0," + 111 "time_stamp INTEGER DEFAULT 0," + 112 "start_pos VARCHAR NOT NULL," + 113 "end_pos VARCHAR," + 114 "title_text VARCHAR," + 115 "pos_text VARCHAR," + 116 "comment_text VARCHAR, " + 117 "time_elapsed INTEGER DEFAULT 0" + 118 ")"); 119 execSQL("CREATE INDEX IF NOT EXISTS " + 120 "bookmark_book_index ON bookmark (book_fk) "); 121 execSQL("CREATE TABLE IF NOT EXISTS metadata (" + 122 "param VARCHAR NOT NULL PRIMARY KEY, " + 123 "value VARCHAR NOT NULL)"); 124 execSQL("CREATE TABLE IF NOT EXISTS genre_group (" + 125 "id INTEGER NOT NULL PRIMARY KEY, " + 126 "code VARCHAR NOT NULL)"); 127 execSQL("CREATE INDEX IF NOT EXISTS " + 128 "genre_group_code_index ON genre_group (code) "); 129 execSQL("CREATE TABLE IF NOT EXISTS genre (" + 130 "id INTEGER NOT NULL PRIMARY KEY, " + 131 "code VARCHAR NOT NULL)"); 132 execSQL("CREATE INDEX IF NOT EXISTS " + 133 "genre_code_index ON genre (code) "); 134 execSQL("CREATE TABLE IF NOT EXISTS genre_hier (" + 135 "group_fk INTEGER NOT NULL REFERENCES genre_group(id), " + 136 "genre_fk INTEGER NOT NULL REFERENCES genre(id), " + 137 "UNIQUE (group_fk, genre_fk))"); 138 execSQL("CREATE TABLE IF NOT EXISTS book_genre (" + 139 "book_fk INTEGER NOT NULL REFERENCES book(id), " + 140 "genre_fk INTEGER NOT NULL REFERENCES genre(id), " + 141 "UNIQUE (book_fk, genre_fk))"); 142 execSQL("CREATE UNIQUE INDEX IF NOT EXISTS " + 143 "book_genre_index ON book_genre (book_fk, genre_fk) "); 144 // ==================================================================== 145 if ( currentVersion<1 ) 146 execSQLIgnoreErrors("ALTER TABLE bookmark ADD COLUMN shortcut INTEGER DEFAULT 0"); 147 if ( currentVersion<4 ) 148 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN flags INTEGER DEFAULT 0"); 149 if ( currentVersion<6 ) 150 execSQL("CREATE TABLE IF NOT EXISTS opds_catalog (" + 151 "id INTEGER PRIMARY KEY AUTOINCREMENT, " + 152 "name VARCHAR NOT NULL COLLATE NOCASE, " + 153 "url VARCHAR NOT NULL COLLATE NOCASE, " + 154 "last_usage INTEGER DEFAULT 0," + 155 "username VARCHAR DEFAULT NULL, " + 156 "password VARCHAR DEFAULT NULL" + 157 ")"); 158 if (currentVersion < 7) { 159 addOPDSCatalogs(DEF_OPDS_URLS1); 160 } 161 if (currentVersion < 13) 162 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN language VARCHAR DEFAULT NULL"); 163 if (currentVersion < 14) 164 pathCorrectionRequired = true; 165 if (currentVersion < 15) 166 execSQLIgnoreErrors("ALTER TABLE opds_catalog ADD COLUMN last_usage INTEGER DEFAULT 0"); 167 if (currentVersion < 16) 168 execSQLIgnoreErrors("ALTER TABLE bookmark ADD COLUMN time_elapsed INTEGER DEFAULT 0"); 169 if (currentVersion < 17) 170 pathCorrectionRequired = true; // chance to correct paths under Android 4.2 171 if (currentVersion < 20) 172 removeOPDSCatalogsFromBlackList(); // BLACK LIST enforcement, by LitRes request 173 if (currentVersion < 21) 174 execSQL("CREATE TABLE IF NOT EXISTS favorite_folders (" + 175 "id INTEGER PRIMARY KEY AUTOINCREMENT, " + 176 "path VARCHAR NOT NULL, " + 177 "position INTEGER NOT NULL default 0" + 178 ")"); 179 if (currentVersion < 23) { 180 execSQLIgnoreErrors("ALTER TABLE opds_catalog ADD COLUMN username VARCHAR DEFAULT NULL"); 181 execSQLIgnoreErrors("ALTER TABLE opds_catalog ADD COLUMN password VARCHAR DEFAULT NULL"); 182 } 183 if (currentVersion < 26) { 184 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS search_history (" + 185 "id INTEGER PRIMARY KEY AUTOINCREMENT, " + 186 "book_fk INTEGER NOT NULL REFERENCES book (id), " + 187 "search_text VARCHAR " + 188 ")"); 189 execSQLIgnoreErrors("CREATE INDEX IF NOT EXISTS " + 190 "search_history_index ON search_history (book_fk) "); 191 } 192 if (currentVersion < 27) { 193 removeOPDSCatalogsByURLs(OBSOLETE_OPDS_URLS); 194 addOPDSCatalogs(DEF_OPDS_URLS3); 195 } 196 if (currentVersion < 28) { 197 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN crc32 INTEGER DEFAULT NULL"); 198 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN domVersion INTEGER DEFAULT 0"); 199 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN rendFlags INTEGER DEFAULT 0"); 200 } 201 if (currentVersion < 29) { 202 // After adding support for the 'fb3' and 'docx' formats in version 3.2.33, 203 // the 'format' field in the 'book' table becomes invalid because the enum DocumentFormat has been changed. 204 // So, after reading this field from the database, we must recheck the format by pathname. 205 // TODO: check format by mime-type or file contents... 206 log.i("Update 'format' field in table 'book'..."); 207 String sql = "SELECT id, pathname, format FROM book"; 208 HashMap<Long, Long> formatsMap = new HashMap<>(); 209 try (Cursor rs = mDB.rawQuery(sql, null)) { 210 if (rs.moveToFirst()) { 211 do { 212 Long id = rs.getLong(0); 213 String pathname = rs.getString(1); 214 long old_format = rs.getLong(2); 215 if (old_format > 1) { // skip 'none', 'fb2' - ordinal is not changed 216 DocumentFormat new_format = DocumentFormat.byExtension(pathname); 217 if (null != new_format && old_format != new_format.ordinal()) 218 formatsMap.put(id, (long) new_format.ordinal()); 219 } 220 } while (rs.moveToNext()); 221 } 222 } catch (Exception e) { 223 Log.e("cr3", "exception while reading format", e); 224 } 225 // Save new format in table 'book'... 226 if (!formatsMap.isEmpty()) { 227 int updatedCount = 0; 228 mDB.beginTransaction(); 229 try (SQLiteStatement stmt = mDB.compileStatement("UPDATE book SET format = ? WHERE id = ?")) { 230 for (Map.Entry<Long, Long> record : formatsMap.entrySet()) { 231 stmt.clearBindings(); 232 stmt.bindLong(1, record.getValue()); 233 stmt.bindLong(2, record.getKey()); 234 stmt.execute(); 235 updatedCount++; 236 } 237 mDB.setTransactionSuccessful(); 238 vlog.i("Updated " + updatedCount + " records with invalid format."); 239 } catch (Exception e) { 240 Log.e("cr3", "exception while reading format", e); 241 } finally { 242 mDB.endTransaction(); 243 } 244 } 245 } 246 if (currentVersion < 30) { 247 // Forced update DOM version from previous latest (20200223) to current (20200824). 248 execSQLIgnoreErrors("UPDATE book SET domVersion=20200824 WHERE domVersion=20200223"); 249 } 250 if (currentVersion < 31) { 251 execSQLIgnoreErrors("ALTER TABLE book ADD COLUMN description TEXT DEFAULT NULL"); 252 } 253 if (currentVersion < 33) { 254 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS metadata (" + 255 "param VARCHAR NOT NULL PRIMARY KEY, " + 256 "value VARCHAR NOT NULL)"); 257 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS genre_group (" + 258 "id INTEGER NOT NULL PRIMARY KEY, " + 259 "code VARCHAR NOT NULL"); 260 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS genre (" + 261 "id INTEGER NOT NULL, " + 262 "parent INTEGER NOT NULL REFERENCES genre_group(id), " + 263 "code VARCHAR NOT NULL, " + 264 "PRIMARY KEY (id, parent))"); 265 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS book_genre (" + 266 "book_fk INTEGER NOT NULL REFERENCES book(id), " + 267 "genre_fk INTEGER NOT NULL REFERENCES genre(id), " + 268 "UNIQUE (book_fk, genre_fk))"); 269 execSQLIgnoreErrors("CREATE INDEX IF NOT EXISTS " + 270 "genre_group_code_index ON genre_group (code) "); 271 execSQLIgnoreErrors("CREATE INDEX IF NOT EXISTS " + 272 "genre_code_index ON genre (code) "); 273 execSQLIgnoreErrors("CREATE UNIQUE INDEX IF NOT EXISTS " + 274 "book_genre_index ON book_genre (book_fk, genre_fk) "); 275 } 276 if (currentVersion < 34) { 277 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS genre_hier (" + 278 "group_fk INTEGER NOT NULL REFERENCES genre_group(id), " + 279 "genre_fk INTEGER NOT NULL REFERENCES genre(id) )"); 280 execSQLIgnoreErrors("INSERT INTO genre_hier (group_fk, genre_fk) SELECT parent as group_fk, id as genre_fk FROM genre ORDER BY parent, id"); 281 execSQLIgnoreErrors("CREATE TABLE IF NOT EXISTS genre_new (" + 282 "id INTEGER NOT NULL PRIMARY KEY," + 283 "code VARCHAR NOT NULL UNIQUE)"); 284 execSQLIgnoreErrors("INSERT INTO genre_new (id, code) SELECT id, code FROM genre GROUP BY id"); 285 Long pragma_foreign_keys = longQuery("PRAGMA foreign_keys"); 286 if (null == pragma_foreign_keys) 287 pragma_foreign_keys = 0L; 288 if (pragma_foreign_keys != 0L) 289 execSQLIgnoreErrors("PRAGMA foreign_keys=OFF"); 290 execSQLIgnoreErrors("DROP TABLE genre"); 291 execSQLIgnoreErrors("ALTER TABLE genre_new RENAME TO genre"); 292 if (pragma_foreign_keys != 0L) 293 execSQLIgnoreErrors("PRAGMA foreign_keys=ON"); 294 } 295 296 //============================================================== 297 // add more updates above this line 298 299 // set current version 300 mDB.setVersion(DB_VERSION); 301 } 302 303 checkOrUpgradeGenresHandbook(); 304 305 dumpStatistics(); 306 307 return true; 308 } 309 dumpStatistics()310 private void dumpStatistics() { 311 log.i("mainDB: " + longQuery("SELECT count(*) FROM author") + " authors, " 312 + longQuery("SELECT count(*) FROM series") + " series, " 313 + longQuery("SELECT count(*) FROM book") + " books, " 314 + longQuery("SELECT count(*) FROM bookmark") + " bookmarks, " 315 + longQuery("SELECT count(*) FROM folder") + " folders" 316 ); 317 } 318 checkOrUpgradeGenresHandbook()319 private boolean checkOrUpgradeGenresHandbook() { 320 boolean res = true; 321 boolean needUpgrade = false; 322 Long version = longQuery("SELECT value FROM metadata WHERE param='genre_version'"); 323 if (null == version || version != Services.getGenresCollection().getVersion()) 324 needUpgrade = true; 325 if (needUpgrade) { 326 mDB.beginTransaction(); 327 try { 328 // fill/append table "genre_group" 329 SQLiteStatement stmt = mDB.compileStatement("INSERT OR IGNORE INTO genre_group (id, code) VALUES (?,?)"); 330 Map<String, GenresCollection.GenreRecord> collection = Services.getGenresCollection().getCollection(); 331 for (Map.Entry<String, GenresCollection.GenreRecord> entry : collection.entrySet()) { 332 GenresCollection.GenreRecord group = entry.getValue(); 333 if (group.getLevel() == 0) { 334 stmt.bindLong(1, group.getId()); 335 stmt.bindString(2, group.getCode()); 336 stmt.executeInsert(); 337 } 338 } 339 // fill/append table "genre" 340 stmt = mDB.compileStatement("INSERT OR IGNORE INTO genre (id, code) VALUES (?,?)"); 341 for (Map.Entry<String, GenresCollection.GenreRecord> entry : collection.entrySet()) { 342 GenresCollection.GenreRecord group = entry.getValue(); 343 if (group.hasChilds()) { 344 for (GenresCollection.GenreRecord genre : group.getChilds()) { 345 stmt.bindLong(1, genre.getId()); 346 stmt.bindString(2, genre.getCode()); 347 stmt.executeInsert(); 348 } 349 } 350 } 351 // fill/append table "genre_hier" 352 stmt = mDB.compileStatement("INSERT OR IGNORE INTO genre_hier (group_fk, genre_fk) VALUES (?,?)"); 353 for (Map.Entry<String, GenresCollection.GenreRecord> entry : collection.entrySet()) { 354 GenresCollection.GenreRecord group = entry.getValue(); 355 if (group.hasChilds()) { 356 for (GenresCollection.GenreRecord genre : group.getChilds()) { 357 stmt.bindLong(1, group.getId()); 358 stmt.bindLong(2, genre.getId()); 359 stmt.executeInsert(); 360 } 361 } 362 } 363 // Update genres data version in metadata 364 stmt = mDB.compileStatement("INSERT OR REPLACE INTO metadata (param, value) VALUES ('genre_version', ?)"); 365 stmt.bindLong(1, Services.getGenresCollection().getVersion()); 366 stmt.executeInsert(); 367 mDB.setTransactionSuccessful(); 368 } catch (SQLException e) { 369 res = false; 370 e.printStackTrace(); 371 } 372 mDB.endTransaction(); 373 } 374 return res; 375 } 376 377 @Override dbFileName()378 protected String dbFileName() { 379 return "cr3db.sqlite"; 380 } 381 clearCaches()382 public void clearCaches() { 383 seriesCache.clear(); 384 authorCache.clear(); 385 folderCache.clear(); 386 fileInfoCache.clear(); 387 } 388 flush()389 public void flush() { 390 super.flush(); 391 if (seriesStmt != null) { 392 seriesStmt.close(); 393 seriesStmt = null; 394 } 395 if (folderStmt != null) { 396 folderStmt.close(); 397 folderStmt = null; 398 } 399 if (authorStmt != null) { 400 authorStmt.close(); 401 authorStmt = null; 402 } 403 if (seriesSelectStmt != null) { 404 seriesSelectStmt.close(); 405 seriesSelectStmt = null; 406 } 407 if (folderSelectStmt != null) { 408 folderSelectStmt.close(); 409 folderSelectStmt = null; 410 } 411 if (authorSelectStmt != null) { 412 authorSelectStmt.close(); 413 authorSelectStmt = null; 414 } 415 } 416 417 //======================================================================================= 418 // OPDS access code 419 //======================================================================================= 420 private final static String[] DEF_OPDS_URLS1 = { 421 // feedbooks.com tested 2020.01 422 // offers preview or requires registration 423 //"http://www.feedbooks.com/catalog.atom", "Feedbooks", 424 // tested 2020.01 - error 500 425 "http://bookserver.archive.org/catalog/", "Internet Archive", 426 // obsolete link 427 // "http://m.gutenberg.org/", "Project Gutenberg", 428 // "http://ebooksearch.webfactional.com/catalog.atom", "eBookSearch", 429 //"http://bookserver.revues.org/", "Revues.org", 430 //"http://www.legimi.com/opds/root.atom", "Legimi", 431 //https://www.ebooksgratuits.com/opds/index.php 432 // tested 2020.01 433 "http://www.ebooksgratuits.com/opds/", "Ebooks libres et gratuits (fr)", 434 }; 435 436 private final static String[] OBSOLETE_OPDS_URLS = { 437 "http://m.gutenberg.org/", // "Project Gutenberg" old URL 438 "http://www.shucang.org/s/index.php", //"ShuCang.org" 439 "http://www.legimi.com/opds/root.atom", //"Legimi", 440 "http://bookserver.revues.org/", //"Revues.org", 441 "http://ebooksearch.webfactional.com/catalog.atom", // 442 }; 443 444 private final static String[] DEF_OPDS_URLS3 = { 445 // o'reilly 446 //"http://opds.oreilly.com/opds/", "O'Reilly", 447 "http://m.gutenberg.org/ebooks.opds/", "Project Gutenberg", 448 //"https://api.gitbook.com/opds/catalog.atom", "GitBook", 449 "http://srv.manybooks.net/opds/index.php", "ManyBooks", 450 //"http://opds.openedition.org/", "OpenEdition (fr)", 451 "https://gallica.bnf.fr/opds", "Gallica (fr)", 452 "https://www.textos.info/catalogo.atom", "textos.info (es)", 453 "https://wolnelektury.pl/opds/", "Wolne Lektury (pl)", 454 "http://www.bokselskap.no/wp-content/themes/bokselskap/tekster/opds/root.xml", "Bokselskap (no)", 455 }; 456 addOPDSCatalogs(String[] catalogs)457 private void addOPDSCatalogs(String[] catalogs) { 458 for (int i=0; i<catalogs.length-1; i+=2) { 459 String url = catalogs[i]; 460 String name = catalogs[i+1]; 461 saveOPDSCatalog(null, url, name, null, null); 462 } 463 } 464 removeOPDSCatalogsByURLs(String .... urls)465 public void removeOPDSCatalogsByURLs(String ... urls) { 466 for (String url : urls) { 467 execSQLIgnoreErrors("DELETE FROM opds_catalog WHERE url=" + quoteSqlString(url)); 468 } 469 } 470 removeOPDSCatalogsFromBlackList()471 public void removeOPDSCatalogsFromBlackList() { 472 if (OPDSConst.BLACK_LIST_MODE != OPDSConst.BLACK_LIST_MODE_FORCE) { 473 removeOPDSCatalogsByURLs("http://flibusta.net/opds/"); 474 } else { 475 removeOPDSCatalogsByURLs(OPDSConst.BLACK_LIST); 476 } 477 } 478 updateOPDSCatalogLastUsage(String url)479 public void updateOPDSCatalogLastUsage(String url) { 480 try { 481 Long existingIdByUrl = longQuery("SELECT id FROM opds_catalog WHERE url=" + quoteSqlString(url)); 482 if (existingIdByUrl == null) 483 return; 484 // update existing 485 Long lastUsage = longQuery("SELECT max(last_usage) FROM opds_catalog"); 486 if (lastUsage == null) 487 lastUsage = 1L; 488 else 489 lastUsage = lastUsage + 1; 490 execSQL("UPDATE opds_catalog SET last_usage="+ lastUsage +" WHERE id=" + existingIdByUrl); 491 } catch (Exception e) { 492 log.e("exception while updating OPDS catalog item", e); 493 } 494 } 495 496 saveOPDSCatalog(Long id, String url, String name, String username, String password)497 public boolean saveOPDSCatalog(Long id, String url, String name, String username, String password) { 498 if (!isOpened()) 499 return false; 500 if (url==null || name==null) 501 return false; 502 url = url.trim(); 503 name = name.trim(); 504 if (url.length()==0 || name.length()==0) 505 return false; 506 try { 507 Long existingIdByUrl = longQuery("SELECT id FROM opds_catalog WHERE url=" + quoteSqlString(url)); 508 Long existingIdByName = longQuery("SELECT id FROM opds_catalog WHERE name=" + quoteSqlString(name)); 509 if (existingIdByUrl!=null && existingIdByName!=null && !existingIdByName.equals(existingIdByUrl)) 510 return false; // duplicates detected 511 if (id==null) { 512 id = existingIdByUrl; 513 if (id==null) 514 id = existingIdByName; 515 } 516 if (id==null) { 517 // insert new 518 execSQL("INSERT INTO opds_catalog (name, url, username, password) VALUES ("+quoteSqlString(name)+", "+quoteSqlString(url)+", "+quoteSqlString(username)+", "+quoteSqlString(password)+")"); 519 } else { 520 // update existing 521 execSQL("UPDATE opds_catalog SET name="+quoteSqlString(name)+", url="+quoteSqlString(url)+", username="+quoteSqlString(username)+", password="+quoteSqlString(password)+" WHERE id=" + id); 522 } 523 updateOPDSCatalogLastUsage(url); 524 525 } catch (Exception e) { 526 log.e("exception while saving OPDS catalog item", e); 527 return false; 528 } 529 return true; 530 } 531 saveSearchHistory(BookInfo book, String sHist)532 public boolean saveSearchHistory(BookInfo book, String sHist) { 533 if (!isOpened()) 534 return false; 535 if (sHist==null) 536 return false; 537 if (book.getFileInfo().id == null) 538 return false; // unknown book id 539 sHist = sHist.trim(); 540 if (sHist.length()==0) 541 return false; 542 try { 543 execSQL("DELETE FROM search_history where book_fk = " + book.getFileInfo().id+ 544 " and search_text = "+quoteSqlString(sHist)); 545 execSQL("INSERT INTO search_history (book_fk, search_text) values (" + book.getFileInfo().id+ 546 ", "+quoteSqlString(sHist)+")"); 547 } catch (Exception e) { 548 log.e("exception while saving search history item", e); 549 return false; 550 } 551 return true; 552 } 553 loadSearchHistory(BookInfo book)554 public ArrayList<String> loadSearchHistory(BookInfo book) { 555 log.i("loadSearchHistory()"); 556 String sql = "SELECT search_text FROM search_history where book_fk=" + book.getFileInfo().id + " ORDER BY id desc"; 557 ArrayList<String> list = new ArrayList<>(); 558 try (Cursor rs = mDB.rawQuery(sql, null)) { 559 if (rs.moveToFirst()) { 560 do { 561 String sHist = rs.getString(0); 562 list.add(sHist); 563 } while (rs.moveToNext()); 564 } 565 } catch (Exception e) { 566 Log.e("cr3", "exception while loading search history", e); 567 } 568 return list; 569 } 570 loadOPDSCatalogs(ArrayList<FileInfo> list)571 public boolean loadOPDSCatalogs(ArrayList<FileInfo> list) { 572 log.i("loadOPDSCatalogs()"); 573 boolean found = false; 574 String sql = "SELECT id, name, url, username, password FROM opds_catalog ORDER BY last_usage DESC, name"; 575 try (Cursor rs = mDB.rawQuery(sql, null)) { 576 if (rs.moveToFirst()) { 577 // remove existing entries 578 list.clear(); 579 // read DB 580 do { 581 long id = rs.getLong(0); 582 String name = rs.getString(1); 583 String url = rs.getString(2); 584 String username = rs.getString(3); 585 String password = rs.getString(4); 586 FileInfo opds = new FileInfo(); 587 opds.isDirectory = true; 588 opds.pathname = FileInfo.OPDS_DIR_PREFIX + url; 589 opds.filename = name; 590 opds.username = username; 591 opds.password = password; 592 opds.isListed = true; 593 opds.isScanned = true; 594 opds.id = id; 595 list.add(opds); 596 found = true; 597 } while (rs.moveToNext()); 598 } 599 } catch (Exception e) { 600 Log.e("cr3", "exception while loading list of OPDS catalogs", e); 601 } 602 return found; 603 } 604 removeOPDSCatalog(Long id)605 public void removeOPDSCatalog(Long id) { 606 log.i("removeOPDSCatalog(" + id + ")"); 607 execSQLIgnoreErrors("DELETE FROM opds_catalog WHERE id = " + id); 608 } 609 loadFavoriteFolders()610 public ArrayList<FileInfo> loadFavoriteFolders() { 611 log.i("loadFavoriteFolders()"); 612 ArrayList<FileInfo> list = new ArrayList<>(); 613 String sql = "SELECT id, path, position FROM favorite_folders ORDER BY position, path"; 614 try (Cursor rs = mDB.rawQuery(sql, null)) { 615 if ( rs.moveToFirst() ) { 616 do { 617 long id = rs.getLong(0); 618 String path = rs.getString(1); 619 int pos = rs.getInt(2); 620 FileInfo favorite = new FileInfo(path); 621 favorite.id = id; 622 favorite.seriesNumber = pos; 623 favorite.setType(FileInfo.TYPE_NOT_SET); 624 list.add(favorite); 625 } while (rs.moveToNext()); 626 } 627 } catch (Exception e) { 628 Log.e("cr3", "exception while loading list of favorite folders", e); 629 } 630 return list; 631 } 632 deleteFavoriteFolder(FileInfo folder)633 public void deleteFavoriteFolder(FileInfo folder){ 634 execSQLIgnoreErrors("DELETE FROM favorite_folders WHERE id = "+ folder.id); 635 } 636 updateFavoriteFolder(FileInfo folder)637 public void updateFavoriteFolder(FileInfo folder){ 638 try (SQLiteStatement stmt = mDB.compileStatement("UPDATE favorite_folders SET position = ?, path = ? WHERE id = ?")) { 639 stmt.bindLong(1, folder.seriesNumber); 640 stmt.bindString(2, folder.pathname); 641 stmt.bindLong(3, folder.id); 642 stmt.execute(); 643 } 644 } 645 createFavoritesFolder(FileInfo folder)646 public void createFavoritesFolder(FileInfo folder){ 647 try (SQLiteStatement stmt = mDB.compileStatement("INSERT INTO favorite_folders (id, path, position) VALUES (NULL, ?, ?)")) { 648 stmt.bindString(1, folder.pathname); 649 stmt.bindLong(2, folder.seriesNumber); 650 folder.id = stmt.executeInsert(); 651 } 652 } 653 654 //======================================================================================= 655 // Bookmarks access code 656 //======================================================================================= 657 private static final String READ_BOOKMARK_SQL = 658 "SELECT " + 659 "id, type, percent, shortcut, time_stamp, " + 660 "start_pos, end_pos, title_text, pos_text, comment_text, time_elapsed " + 661 "FROM bookmark b "; readBookmarkFromCursor(Bookmark v, Cursor rs )662 private void readBookmarkFromCursor(Bookmark v, Cursor rs ) 663 { 664 int i=0; 665 v.setId( rs.getLong(i++) ); 666 v.setType( (int)rs.getLong(i++) ); 667 v.setPercent( (int)rs.getLong(i++) ); 668 v.setShortcut( (int)rs.getLong(i++) ); 669 v.setTimeStamp( rs.getLong(i++) ); 670 v.setStartPos( rs.getString(i++) ); 671 v.setEndPos( rs.getString(i++) ); 672 v.setTitleText( rs.getString(i++) ); 673 v.setPosText( rs.getString(i++) ); 674 v.setCommentText( rs.getString(i++) ); 675 v.setTimeElapsed( rs.getLong(i++) ); 676 } 677 findBy( Bookmark v, String condition )678 public boolean findBy( Bookmark v, String condition ) { 679 boolean found = false; 680 try (Cursor rs = mDB.rawQuery(READ_BOOKMARK_SQL + " WHERE " + condition, null)) { 681 if (rs.moveToFirst()) { 682 readBookmarkFromCursor( v, rs ); 683 found = true; 684 } 685 } 686 return found; 687 } 688 load( ArrayList<Bookmark> list, String condition )689 public boolean load( ArrayList<Bookmark> list, String condition ) 690 { 691 boolean found = false; 692 try (Cursor rs = mDB.rawQuery(READ_BOOKMARK_SQL + " WHERE " + condition, null)) { 693 if ( rs.moveToFirst() ) { 694 do { 695 Bookmark v = new Bookmark(); 696 readBookmarkFromCursor( v, rs ); 697 list.add(v); 698 found = true; 699 } while ( rs.moveToNext() ); 700 } 701 } 702 return found; 703 } 704 loadBookmarks(BookInfo book)705 public void loadBookmarks(BookInfo book) { 706 if (book.getFileInfo().id == null) 707 return; // unknown book id 708 ArrayList<Bookmark> bookmarks = new ArrayList<>(); 709 if (load( bookmarks, "book_fk=" + book.getFileInfo().id + " ORDER BY type" ) ) { 710 book.setBookmarks(bookmarks); 711 } 712 } 713 714 //======================================================================================= 715 // Item groups access code 716 //======================================================================================= 717 718 /// add items range to parent dir addItems(FileInfo parent, ArrayList<FileInfo> items, int start, int end)719 private static void addItems(FileInfo parent, ArrayList<FileInfo> items, int start, int end) { 720 for (int i=start; i<end; i++) { 721 items.get(i).parent = parent; 722 parent.addDir(items.get(i)); 723 } 724 } 725 726 private static abstract class ItemGroupExtractor { getComparisionField(FileInfo item)727 public abstract String getComparisionField(FileInfo item); getItemFirstLetters(FileInfo item, int level)728 public String getItemFirstLetters(FileInfo item, int level) { 729 String name = getComparisionField(item); //.filename; 730 int l = name == null ? 0 : Math.min(name.length(), level); 731 return l > 0 ? name.substring(0, l).toUpperCase() : "_"; 732 } 733 } 734 735 private static class ItemGroupFilenameExtractor extends ItemGroupExtractor { 736 @Override getComparisionField(FileInfo item)737 public String getComparisionField(FileInfo item) { 738 return item.filename; 739 } 740 } 741 742 private static class ItemGroupTitleExtractor extends ItemGroupExtractor { 743 @Override getComparisionField(FileInfo item)744 public String getComparisionField(FileInfo item) { 745 return item.title; 746 } 747 } 748 createItemGroup(String groupPrefix, String groupPrefixTag)749 private FileInfo createItemGroup(String groupPrefix, String groupPrefixTag) { 750 FileInfo groupDir = new FileInfo(); 751 groupDir.isDirectory = true; 752 groupDir.pathname = groupPrefixTag + groupPrefix; 753 groupDir.filename = groupPrefix + "..."; 754 groupDir.isListed = true; 755 groupDir.isScanned = true; 756 groupDir.id = 0l; 757 return groupDir; 758 } 759 sortItems(ArrayList<FileInfo> items, final ItemGroupExtractor extractor)760 private void sortItems(ArrayList<FileInfo> items, final ItemGroupExtractor extractor) { 761 Collections.sort(items, (lhs, rhs) -> { 762 String l = extractor.getComparisionField(lhs) != null ? extractor.getComparisionField(lhs).toUpperCase() : ""; 763 String r = extractor.getComparisionField(rhs) != null ? extractor.getComparisionField(rhs).toUpperCase() : ""; 764 return l.compareTo(r); 765 }); 766 } 767 addGroupedItems(FileInfo parent, ArrayList<FileInfo> items, int start, int end, String groupPrefixTag, int level, final ItemGroupExtractor extractor)768 private void addGroupedItems(FileInfo parent, ArrayList<FileInfo> items, int start, int end, String groupPrefixTag, int level, final ItemGroupExtractor extractor) { 769 int itemCount = end - start; 770 if (itemCount < 1) 771 return; 772 // for nested level (>1), create base subgroup, otherwise use parent 773 if (level > 1 && itemCount > 1) { 774 String baseFirstLetter = extractor.getItemFirstLetters(items.get(start), level - 1); 775 FileInfo newGroup = createItemGroup(baseFirstLetter, groupPrefixTag); 776 newGroup.parent = parent; 777 parent.addDir(newGroup); 778 parent = newGroup; 779 } 780 781 // check group count 782 int topLevelGroupsCount = 0; 783 String lastFirstLetter = ""; 784 for (int i=start; i<end; i++) { 785 String firstLetter = extractor.getItemFirstLetters(items.get(i), level); 786 if (!firstLetter.equals(lastFirstLetter)) { 787 topLevelGroupsCount++; 788 lastFirstLetter = firstLetter; 789 } 790 } 791 if (itemCount <= topLevelGroupsCount * 11 / 10 || itemCount < 8) { 792 // small number of items: add as is 793 addItems(parent, items, start, end); 794 return; 795 } 796 797 // divide items into groups 798 for (int i=start; i<end; ) { 799 String firstLetter = extractor.getItemFirstLetters(items.get(i), level); 800 int groupEnd = i + 1; 801 for (; groupEnd < end; groupEnd++) { 802 String firstLetter2 = groupEnd < end ? extractor.getItemFirstLetters(items.get(groupEnd), level) : ""; 803 if (!firstLetter.equals(firstLetter2)) 804 break; 805 } 806 // group is i..groupEnd 807 addGroupedItems(parent, items, i, groupEnd, groupPrefixTag, level + 1, extractor); 808 i = groupEnd; 809 } 810 } 811 812 private boolean loadItemList(ArrayList<FileInfo> list, String sql, String groupPrefixTag) { 813 boolean found = false; 814 try (Cursor rs = mDB.rawQuery(sql, null)) { 815 if (rs.moveToFirst()) { 816 // read DB 817 do { 818 long id = rs.getLong(0); 819 String name = rs.getString(1); 820 if (FileInfo.AUTHOR_PREFIX.equals(groupPrefixTag)) 821 name = Utils.authorNameFileAs(name); 822 int bookCount = rs.getInt(2); 823 824 FileInfo item = new FileInfo(); 825 item.isDirectory = true; 826 item.pathname = groupPrefixTag + id; 827 item.filename = name; 828 item.isListed = true; 829 item.isScanned = true; 830 item.id = id; 831 item.tag = bookCount; 832 833 list.add(item); 834 found = true; 835 } while (rs.moveToNext()); 836 } 837 } catch (Exception e) { 838 Log.e("cr3", "exception while loading list of authors", e); 839 } 840 sortItems(list, new ItemGroupFilenameExtractor()); 841 return found; 842 } 843 844 public boolean loadGenresList(FileInfo parent, boolean showEmptyGenres) { 845 Log.i("cr3", "loadGenresList()"); 846 beginReading(); 847 parent.clear(); 848 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 849 String sql = "SELECT code, (SELECT COUNT(DISTINCT book_fk) FROM book_genre bg JOIN genre g ON g.id=bg.genre_fk JOIN genre_hier gh ON gh.genre_fk = g.id WHERE gh.group_fk=gg.id) as book_count FROM genre_group gg"; 850 try (Cursor rs = mDB.rawQuery(sql, null)) { 851 if (rs.moveToFirst()) { 852 // read DB 853 do { 854 String code = rs.getString(0); 855 int bookCount = rs.getInt(1); 856 if (bookCount > 0 || showEmptyGenres) { 857 FileInfo item = new FileInfo(); 858 item.isDirectory = true; 859 item.pathname = FileInfo.GENRES_PREFIX + code; 860 item.filename = Services.getGenresCollection().translate(code); 861 item.isListed = true; 862 item.isScanned = true; 863 item.id = (long) -1; // fake id 864 item.tag = bookCount; 865 list.add(item); 866 } 867 } while (rs.moveToNext()); 868 } 869 } catch (Exception e) { 870 Log.e("cr3", "exception while loading list of authors", e); 871 } 872 endReading(); 873 addItems(parent, list, 0, list.size()); 874 return true; 875 } 876 877 loadAuthorsList(FileInfo parent)878 public boolean loadAuthorsList(FileInfo parent) { 879 Log.i("cr3", "loadAuthorsList()"); 880 beginReading(); 881 parent.clear(); 882 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 883 String sql = "SELECT author.id, author.name, count(*) as book_count FROM author INNER JOIN book_author ON book_author.author_fk = author.id GROUP BY author.name, author.id ORDER BY author.name"; 884 boolean found = loadItemList(list, sql, FileInfo.AUTHOR_PREFIX); 885 addGroupedItems(parent, list, 0, list.size(), FileInfo.AUTHOR_GROUP_PREFIX, 1, new ItemGroupFilenameExtractor()); 886 endReading(); 887 return found; 888 } 889 loadSeriesList(FileInfo parent)890 public boolean loadSeriesList(FileInfo parent) { 891 Log.i("cr3", "loadSeriesList()"); 892 beginReading(); 893 parent.clear(); 894 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 895 String sql = "SELECT series.id, series.name, count(*) as book_count FROM series INNER JOIN book ON book.series_fk = series.id GROUP BY series.name, series.id ORDER BY series.name"; 896 boolean found = loadItemList(list, sql, FileInfo.SERIES_PREFIX); 897 addGroupedItems(parent, list, 0, list.size(), FileInfo.SERIES_GROUP_PREFIX, 1, new ItemGroupFilenameExtractor()); 898 endReading(); 899 return found; 900 } 901 loadTitleList(FileInfo parent)902 public boolean loadTitleList(FileInfo parent) { 903 Log.i("cr3", "loadTitleList()"); 904 beginReading(); 905 parent.clear(); 906 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 907 String sql = READ_FILEINFO_SQL + " WHERE b.title IS NOT NULL AND b.title != '' ORDER BY b.title"; 908 boolean found = findBooks(sql, list); 909 sortItems(list, new ItemGroupTitleExtractor()); 910 // remove duplicate titles 911 for (int i=list.size() - 1; i>0; i--) { 912 String title = list.get(i).title; 913 if (title == null) { 914 list.remove(i); 915 continue; 916 } 917 String prevTitle = list.get(i - 1).title; 918 if (title.equals(prevTitle)) 919 list.remove(i); 920 } 921 addGroupedItems(parent, list, 0, list.size(), FileInfo.TITLE_GROUP_PREFIX, 1, new ItemGroupTitleExtractor()); 922 endReading(); 923 return found; 924 } 925 findAuthorBooks(ArrayList<FileInfo> list, long authorId)926 public boolean findAuthorBooks(ArrayList<FileInfo> list, long authorId) 927 { 928 if (!isOpened()) 929 return false; 930 String sql = READ_FILEINFO_SQL + " INNER JOIN book_author ON book_author.book_fk = b.id WHERE book_author.author_fk = " + authorId + " ORDER BY b.title"; 931 return findBooks(sql, list); 932 } 933 findSeriesBooks(ArrayList<FileInfo> list, long seriesId)934 public boolean findSeriesBooks(ArrayList<FileInfo> list, long seriesId) 935 { 936 if (!isOpened()) 937 return false; 938 String sql = READ_FILEINFO_SQL + " INNER JOIN series ON series.id = b.series_fk WHERE series.id = " + seriesId + " ORDER BY b.series_number, b.title"; 939 return findBooks(sql, list); 940 } 941 findBooksByRating(ArrayList<FileInfo> list, int minRate, int maxRate)942 public boolean findBooksByRating(ArrayList<FileInfo> list, int minRate, int maxRate) 943 { 944 if (!isOpened()) 945 return false; 946 String sql = READ_FILEINFO_SQL + " WHERE ((flags>>20)&15) BETWEEN " + minRate + " AND " + maxRate + " ORDER BY ((flags>>20)&15) DESC, b.title LIMIT 1000"; 947 return findBooks(sql, list); 948 } 949 findBooksByState(ArrayList<FileInfo> list, int state)950 public boolean findBooksByState(ArrayList<FileInfo> list, int state) 951 { 952 if (!isOpened()) 953 return false; 954 String sql = READ_FILEINFO_SQL + " WHERE ((flags>>16)&15) = " + state + " ORDER BY b.title LIMIT 1000"; 955 return findBooks(sql, list); 956 } 957 findAuthors(int maxCount, String authorPattern)958 private String findAuthors(int maxCount, String authorPattern) { 959 StringBuilder buf = new StringBuilder(); 960 String sql = "SELECT id, name FROM author"; 961 int count = 0; 962 try (Cursor rs = mDB.rawQuery(sql, null)) { 963 if (rs.moveToFirst()) { 964 do { 965 long id = rs.getLong(0); 966 String name = rs.getString(1); 967 if (Utils.matchPattern(name, authorPattern)) { 968 if (buf.length() != 0) 969 buf.append(","); 970 buf.append(id); 971 count++; 972 if (count >= maxCount) 973 break; 974 } 975 } while (rs.moveToNext()); 976 } 977 } 978 return buf.toString(); 979 } 980 981 982 //======================================================================================= 983 // Series access code 984 //======================================================================================= 985 986 private SQLiteStatement seriesStmt; 987 private SQLiteStatement seriesSelectStmt; 988 private HashMap<String,Long> seriesCache = new HashMap<String,Long>(); getSeriesId( String seriesName )989 public Long getSeriesId( String seriesName ) 990 { 991 if ( seriesName==null || seriesName.trim().length()==0 ) 992 return null; 993 Long id = seriesCache.get(seriesName); 994 if ( id!=null ) 995 return id; 996 if (seriesSelectStmt == null) 997 seriesSelectStmt = mDB.compileStatement("SELECT id FROM series WHERE name=?"); 998 try { 999 seriesSelectStmt.bindString(1, seriesName); 1000 return seriesSelectStmt.simpleQueryForLong(); 1001 } catch ( Exception e ) { 1002 // not found 1003 } 1004 if (seriesStmt == null) 1005 seriesStmt = mDB.compileStatement("INSERT INTO series (id, name) VALUES (NULL,?)"); 1006 seriesStmt.bindString(1, seriesName); 1007 id = seriesStmt.executeInsert(); 1008 seriesCache.put( seriesName, id ); 1009 return id; 1010 } 1011 1012 //======================================================================================= 1013 // Folder access code 1014 //======================================================================================= 1015 1016 private SQLiteStatement folderStmt; 1017 private SQLiteStatement folderSelectStmt; 1018 private HashMap<String,Long> folderCache = new HashMap<String,Long>(); getFolderId( String folderName )1019 public Long getFolderId( String folderName ) 1020 { 1021 if ( folderName==null || folderName.trim().length()==0 ) 1022 return null; 1023 Long id = folderCache.get(folderName); 1024 if ( id!=null ) 1025 return id; 1026 if ( folderSelectStmt==null ) 1027 folderSelectStmt = mDB.compileStatement("SELECT id FROM folder WHERE name=?"); 1028 try { 1029 folderSelectStmt.bindString(1, folderName); 1030 return folderSelectStmt.simpleQueryForLong(); 1031 } catch ( Exception e ) { 1032 // not found 1033 } 1034 if ( folderStmt==null ) 1035 folderStmt = mDB.compileStatement("INSERT INTO folder (id, name) VALUES (NULL,?)"); 1036 folderStmt.bindString(1, folderName); 1037 id = folderStmt.executeInsert(); 1038 folderCache.put( folderName, id ); 1039 return id; 1040 } 1041 1042 //======================================================================================= 1043 // Author access code 1044 //======================================================================================= 1045 1046 private SQLiteStatement authorStmt; 1047 private SQLiteStatement authorSelectStmt; 1048 private HashMap<String,Long> authorCache = new HashMap<String,Long>(); getAuthorId( String authorName )1049 private Long getAuthorId( String authorName ) { 1050 if ( authorName==null || authorName.trim().length()==0 ) 1051 return null; 1052 Long id = authorCache.get(authorName); 1053 if ( id!=null ) 1054 return id; 1055 if ( authorSelectStmt==null ) 1056 authorSelectStmt = mDB.compileStatement("SELECT id FROM author WHERE name=?"); 1057 try { 1058 authorSelectStmt.bindString(1, authorName); 1059 return authorSelectStmt.simpleQueryForLong(); 1060 } catch ( Exception e ) { 1061 // not found 1062 } 1063 if ( authorStmt==null ) 1064 authorStmt = mDB.compileStatement("INSERT INTO author (id, name) VALUES (NULL,?)"); 1065 authorStmt.bindString(1, authorName); 1066 id = authorStmt.executeInsert(); 1067 authorCache.put( authorName, id ); 1068 return id; 1069 } 1070 getAuthorIds( String authorNames )1071 private Long[] getAuthorIds( String authorNames ) { 1072 if ( authorNames==null || authorNames.trim().length()==0 ) 1073 return null; 1074 String[] names = authorNames.split("\\|"); 1075 if ( names==null || names.length==0 ) 1076 return null; 1077 ArrayList<Long> ids = new ArrayList<Long>(names.length); 1078 for ( String name : names ) { 1079 Long id = getAuthorId(name); 1080 if ( id!=null ) 1081 ids.add(id); 1082 } 1083 if ( ids.size()>0 ) 1084 return ids.toArray(new Long[ids.size()]); 1085 return null; 1086 } 1087 saveBookAuthors( Long bookId, Long[] authors)1088 public void saveBookAuthors( Long bookId, Long[] authors) { 1089 if ( authors==null || authors.length==0 ) 1090 return; 1091 String insertQuery = "INSERT OR IGNORE INTO book_author (book_fk,author_fk) VALUES "; 1092 for ( Long id : authors ) { 1093 String sql = insertQuery + "(" + bookId + "," + id + ")"; 1094 //Log.v("cr3", "executing: " + sql); 1095 mDB.execSQL(sql); 1096 } 1097 } 1098 getGenresIds( String keywords )1099 private Integer[] getGenresIds( String keywords ) { 1100 if ( keywords==null || keywords.trim().length()==0 ) 1101 return null; 1102 String[] codes = keywords.split("\\|"); 1103 if ( codes==null || codes.length==0 ) 1104 return null; 1105 GenresCollection genresCollection = Services.getGenresCollection(); 1106 ArrayList<Integer> ids = new ArrayList<Integer>(codes.length); 1107 for ( String code : codes ) { 1108 GenresCollection.GenreRecord genre = genresCollection.byCode(code); 1109 if (null != genre) { 1110 int id = genre.getId(); 1111 ids.add(id); 1112 } 1113 } 1114 if ( ids.size()>0 ) 1115 return ids.toArray(new Integer[0]); 1116 return null; 1117 } 1118 saveBookGenres( Long bookId, Integer[] genres)1119 public void saveBookGenres( Long bookId, Integer[] genres) { 1120 if ( genres==null || genres.length==0 ) 1121 return; 1122 String insertQuery = "INSERT OR IGNORE INTO book_genre (book_fk,genre_fk) VALUES "; 1123 for ( Integer id : genres ) { 1124 String sql = insertQuery + "(" + bookId + "," + id + ")"; 1125 //Log.v("cr3", "executing: " + sql); 1126 mDB.execSQL(sql); 1127 } 1128 } 1129 eq(String s1, String s2)1130 private static boolean eq(String s1, String s2) 1131 { 1132 if ( s1!=null ) 1133 return s1.equals(s2); 1134 return s2==null; 1135 } 1136 1137 private final static int FILE_INFO_CACHE_SIZE = 3000; 1138 private FileInfoCache fileInfoCache = new FileInfoCache(FILE_INFO_CACHE_SIZE); 1139 findMovedFileInfo(String path)1140 private FileInfo findMovedFileInfo(String path) { 1141 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 1142 FileInfo fi = new FileInfo(path); 1143 if (fi.exists()) { 1144 if (findAllBy(list, "filename", fi.filename)) { 1145 for (FileInfo item : list) { 1146 if (item.exists()) 1147 continue; 1148 if (item.size == fi.size) { 1149 log.i("Found record for file of the same name and size: treat as moved " + item.filename + " " + item.size); 1150 // fix and save 1151 item.pathname = fi.pathname; 1152 item.arcname = fi.arcname; 1153 item.arcsize = fi.arcsize; 1154 item.path = fi.path; 1155 item.createTime = fi.createTime; 1156 save(item); 1157 fileInfoCache.put(item); 1158 return item; 1159 } 1160 } 1161 } 1162 } 1163 return null; 1164 } 1165 findFileInfoByPathname(String path, boolean detectMoved)1166 private FileInfo findFileInfoByPathname(String path, boolean detectMoved) 1167 { 1168 FileInfo existing = fileInfoCache.get(path); 1169 if (existing != null) 1170 return existing; 1171 FileInfo fileInfo = new FileInfo(); 1172 if (findBy(fileInfo, "pathname", path)) { 1173 fileInfoCache.put(fileInfo); 1174 return fileInfo; 1175 } 1176 if (!detectMoved) 1177 return null; 1178 return findMovedFileInfo(path); 1179 } 1180 findFileInfoById(Long id)1181 private FileInfo findFileInfoById(Long id) 1182 { 1183 if (id == null) 1184 return null; 1185 FileInfo existing = fileInfoCache.get(id); 1186 if (existing != null) 1187 return existing; 1188 FileInfo fileInfo = new FileInfo(); 1189 if (findBy( fileInfo, "b.id", id)) { 1190 return fileInfo; 1191 } 1192 return null; 1193 } 1194 findBy( FileInfo fileInfo, String fieldName, Object fieldValue )1195 private boolean findBy( FileInfo fileInfo, String fieldName, Object fieldValue ) 1196 { 1197 String condition; 1198 StringBuilder buf = new StringBuilder(" WHERE "); 1199 buf.append(fieldName); 1200 if ( fieldValue==null ) { 1201 buf.append(" IS NULL "); 1202 } else { 1203 buf.append("="); 1204 DatabaseUtils.appendValueToSql(buf, fieldValue); 1205 buf.append(" "); 1206 } 1207 condition = buf.toString(); 1208 boolean found = false; 1209 try (Cursor rs = mDB.rawQuery(READ_FILEINFO_SQL + 1210 condition, null)) { 1211 if (rs.moveToFirst()) { 1212 readFileInfoFromCursor(fileInfo, rs); 1213 found = true; 1214 } 1215 } 1216 return found; 1217 } 1218 findAllBy( ArrayList<FileInfo> result, String fieldName, Object fieldValue )1219 private boolean findAllBy( ArrayList<FileInfo> result, String fieldName, Object fieldValue ) 1220 { 1221 String condition; 1222 StringBuilder buf = new StringBuilder(" WHERE "); 1223 buf.append(fieldName); 1224 if ( fieldValue==null ) { 1225 buf.append(" IS NULL "); 1226 } else { 1227 buf.append("="); 1228 DatabaseUtils.appendValueToSql(buf, fieldValue); 1229 buf.append(" "); 1230 } 1231 condition = buf.toString(); 1232 boolean found = false; 1233 try (Cursor rs = mDB.rawQuery(READ_FILEINFO_SQL + 1234 condition, null)) { 1235 if (rs.moveToFirst()) { 1236 do { 1237 FileInfo fileInfo = new FileInfo(); 1238 readFileInfoFromCursor(fileInfo, rs); 1239 result.add(fileInfo); 1240 found = true; 1241 } while (rs.moveToNext()); 1242 } 1243 } 1244 return found; 1245 } 1246 loadBookmarks(FileInfo fileInfo)1247 private HashMap<String, Bookmark> loadBookmarks(FileInfo fileInfo) { 1248 HashMap<String, Bookmark> map = new HashMap<>(); 1249 if (fileInfo.id != null) { 1250 ArrayList<Bookmark> bookmarks = new ArrayList<>(); 1251 if (load(bookmarks, "book_fk=" + fileInfo.id + " ORDER BY type")) { 1252 for (Bookmark b : bookmarks) { 1253 // delete non-unique bookmarks 1254 String key = b.getUniqueKey(); 1255 if (!map.containsKey(key)) 1256 map.put(key, b); 1257 else { 1258 log.w("Removing non-unique bookmark " + b + " for " + fileInfo.getPathName()); 1259 deleteBookmark(b); 1260 } 1261 } 1262 } 1263 } 1264 return map; 1265 } 1266 save( Bookmark v, long bookId )1267 private boolean save( Bookmark v, long bookId ) 1268 { 1269 Log.d("cr3db", "saving bookmark id=" + v.getId() + ", bookId=" + bookId + ", pos=" + v.getStartPos()); 1270 Bookmark oldValue = new Bookmark(); 1271 if ( v.getId()!=null ) { 1272 // update 1273 oldValue.setId(v.getId()); 1274 if ( findBy(oldValue, "book_fk=" + bookId + " AND id=" + v.getId()) ) { 1275 // found, updating 1276 QueryHelper h = new QueryHelper(v, oldValue, bookId); 1277 h.update(v.getId()); 1278 } else { 1279 oldValue = new Bookmark(); 1280 QueryHelper h = new QueryHelper(v, oldValue, bookId); 1281 v.setId( h.insert() ); 1282 } 1283 } else { 1284 QueryHelper h = new QueryHelper(v, oldValue, bookId); 1285 v.setId( h.insert() ); 1286 } 1287 return true; 1288 } 1289 saveBookInfo(BookInfo bookInfo)1290 public void saveBookInfo(BookInfo bookInfo) { 1291 if (!isOpened()) { 1292 Log.e("cr3db", "cannot save book info : DB is closed"); 1293 return; 1294 } 1295 if (bookInfo == null || bookInfo.getFileInfo() == null) 1296 return; 1297 1298 // save main data 1299 save(bookInfo.getFileInfo()); 1300 fileInfoCache.put(bookInfo.getFileInfo()); 1301 1302 // save bookmarks 1303 HashMap<String, Bookmark> existingBookmarks = loadBookmarks(bookInfo.getFileInfo()); 1304 int changed = 0; 1305 int removed = 0; 1306 int added = 0; 1307 for (Bookmark bmk : bookInfo.getAllBookmarks()) { 1308 Bookmark existing = existingBookmarks.get(bmk.getUniqueKey()); 1309 if (existing != null) { 1310 bmk.setId(existing.getId()); 1311 if (!bmk.equals(existing)) { 1312 save(bmk, bookInfo.getFileInfo().id); 1313 changed++; 1314 } 1315 existingBookmarks.remove(bmk.getUniqueKey()); // saved 1316 } else { 1317 // create new 1318 save(bmk, bookInfo.getFileInfo().id); 1319 added++; 1320 } 1321 } 1322 if (existingBookmarks.size() > 0) { 1323 // remove bookmarks not found in new object 1324 for (Bookmark bmk : existingBookmarks.values()) { 1325 deleteBookmark(bmk); 1326 removed++; 1327 } 1328 } 1329 if (added + changed + removed > 0) 1330 vlog.i("bookmarks added:" + added + ", updated: " + changed + ", removed:" + removed); 1331 } 1332 save(FileInfo fileInfo)1333 private boolean save(FileInfo fileInfo) { 1334 boolean authorsChanged = true; 1335 boolean genresChanged = true; 1336 try { 1337 FileInfo oldValue = findFileInfoByPathname(fileInfo.getPathName(), false); 1338 if (oldValue == null && fileInfo.id != null) 1339 oldValue = findFileInfoById(fileInfo.id); 1340 if (oldValue != null && fileInfo.id == null && oldValue.id != null) 1341 fileInfo.id = oldValue.id; 1342 if (oldValue != null) { 1343 // found, updating 1344 if (!fileInfo.equals(oldValue)) { 1345 vlog.d("updating file " + fileInfo.getPathName()); 1346 beginChanges(); 1347 QueryHelper h = new QueryHelper(fileInfo, oldValue); 1348 h.update(fileInfo.id); 1349 } 1350 authorsChanged = !eq(fileInfo.authors, oldValue.authors); 1351 genresChanged = !eq(fileInfo.genres, oldValue.genres); 1352 } else { 1353 // inserting 1354 vlog.d("inserting new file " + fileInfo.getPathName()); 1355 beginChanges(); 1356 QueryHelper h = new QueryHelper(fileInfo, new FileInfo()); 1357 fileInfo.id = h.insert(); 1358 authorsChanged = true; 1359 genresChanged = true; 1360 } 1361 1362 fileInfoCache.put(fileInfo); 1363 if (fileInfo.id != null) { 1364 if ( authorsChanged ) { 1365 vlog.d("updating authors for file " + fileInfo.getPathName()); 1366 beginChanges(); 1367 Long[] authorIds = getAuthorIds(fileInfo.authors); 1368 saveBookAuthors(fileInfo.id, authorIds); 1369 } 1370 if (genresChanged) { 1371 vlog.d("updating genres for file " + fileInfo.getPathName()); 1372 beginChanges(); 1373 Integer[] genresIds = getGenresIds(fileInfo.genres); 1374 saveBookGenres(fileInfo.id, genresIds); 1375 } 1376 return true; 1377 } 1378 return false; 1379 } catch (SQLiteException e) { 1380 log.e("error while writing to DB", e); 1381 return false; 1382 } 1383 } 1384 saveFileInfos(Collection<FileInfo> list)1385 public void saveFileInfos(Collection<FileInfo> list) 1386 { 1387 Log.v("cr3db", "save BookInfo collection: " + list.size() + " items"); 1388 if (!isOpened()) { 1389 Log.e("cr3db", "cannot save book info : DB is closed"); 1390 return; 1391 } 1392 for (FileInfo fileInfo : list) { 1393 save(fileInfo); 1394 } 1395 } 1396 1397 /** 1398 * Load recent books list, with bookmarks 1399 * @param maxCount is max number of recent books to get 1400 * @return list of loaded books 1401 */ loadRecentBooks(int maxCount)1402 public ArrayList<BookInfo> loadRecentBooks(int maxCount) 1403 { 1404 ArrayList<FileInfo> list = new ArrayList<FileInfo>(); 1405 if (!isOpened()) 1406 return null; 1407 beginReading(); 1408 findRecentBooks(list, maxCount, maxCount*10); 1409 ArrayList<BookInfo> res = new ArrayList<BookInfo>(list.size()); 1410 for (FileInfo f : list) { 1411 FileInfo file = fileInfoCache.get(f.getPathName()); // try using cached value instead 1412 if (file == null) { 1413 file = f; 1414 fileInfoCache.put(file); 1415 } 1416 BookInfo item = new BookInfo(new FileInfo(file)); 1417 loadBookmarks(item); 1418 res.add(item); 1419 } 1420 endReading(); 1421 return res; 1422 } 1423 findRecentBooks( ArrayList<FileInfo> list, int maxCount, int limit )1424 private boolean findRecentBooks( ArrayList<FileInfo> list, int maxCount, int limit ) 1425 { 1426 String sql = READ_FILEINFO_SQL + " WHERE last_access_time>0 ORDER BY last_access_time DESC LIMIT " + limit; 1427 boolean found = false; 1428 try (Cursor rs = mDB.rawQuery(sql, null)) { 1429 if (rs.moveToFirst()) { 1430 do { 1431 FileInfo fileInfo = new FileInfo(); 1432 readFileInfoFromCursor(fileInfo, rs); 1433 if (!fileInfo.fileExists()) 1434 continue; 1435 list.add(fileInfo); 1436 fileInfoCache.put(fileInfo); 1437 found = true; 1438 if (list.size() > maxCount) 1439 break; 1440 } while (rs.moveToNext()); 1441 } 1442 } 1443 return found; 1444 } 1445 1446 1447 //======================================================================================= 1448 // File info access code 1449 //======================================================================================= 1450 1451 public class QueryHelper { 1452 String tableName; QueryHelper(String tableName)1453 QueryHelper(String tableName) 1454 { 1455 this.tableName = tableName; 1456 } 1457 ArrayList<String> fields = new ArrayList<String>(); 1458 ArrayList<Object> values = new ArrayList<Object>(); add(String fieldName, int value, int oldValue )1459 QueryHelper add(String fieldName, int value, int oldValue ) 1460 { 1461 if ( value!=oldValue ) { 1462 fields.add(fieldName); 1463 values.add(Long.valueOf(value)); 1464 } 1465 return this; 1466 } add(String fieldName, Long value, Long oldValue )1467 QueryHelper add(String fieldName, Long value, Long oldValue ) 1468 { 1469 if ( value!=null && (oldValue==null || !oldValue.equals(value))) { 1470 fields.add(fieldName); 1471 values.add(value); 1472 } 1473 return this; 1474 } add(String fieldName, String value, String oldValue)1475 QueryHelper add(String fieldName, String value, String oldValue) 1476 { 1477 if ( value!=null && (oldValue==null || !oldValue.equals(value))) { 1478 fields.add(fieldName); 1479 values.add(value); 1480 } 1481 return this; 1482 } add(String fieldName, Double value, Double oldValue)1483 QueryHelper add(String fieldName, Double value, Double oldValue) 1484 { 1485 if ( value!=null && (oldValue==null || !oldValue.equals(value))) { 1486 fields.add(fieldName); 1487 values.add(value); 1488 } 1489 return this; 1490 } insert()1491 Long insert() 1492 { 1493 if ( fields.size()==0 ) 1494 return null; 1495 beginChanges(); 1496 StringBuilder valueBuf = new StringBuilder(); 1497 try { 1498 String ignoreOption = ""; //"OR IGNORE "; 1499 StringBuilder buf = new StringBuilder("INSERT " + ignoreOption + " INTO "); 1500 buf.append(tableName); 1501 buf.append(" (id"); 1502 for ( String field : fields ) { 1503 buf.append(","); 1504 buf.append(field); 1505 } 1506 buf.append(") VALUES (NULL"); 1507 for ( @SuppressWarnings("unused") String field : fields ) { 1508 buf.append(","); 1509 buf.append("?"); 1510 } 1511 buf.append(")"); 1512 String sql = buf.toString(); 1513 Log.d("cr3db", "going to execute " + sql); 1514 long id; 1515 try (SQLiteStatement stmt = mDB.compileStatement(sql)) { 1516 for (int i = 1; i <= values.size(); i++) { 1517 Object v = values.get(i - 1); 1518 valueBuf.append(v != null ? v.toString() : "null"); 1519 valueBuf.append(","); 1520 if (v == null) 1521 stmt.bindNull(i); 1522 else if (v instanceof String) 1523 stmt.bindString(i, (String) v); 1524 else if (v instanceof Long) 1525 stmt.bindLong(i, (Long) v); 1526 else if (v instanceof Double) 1527 stmt.bindDouble(i, (Double) v); 1528 } 1529 id = stmt.executeInsert(); 1530 Log.d("cr3db", "added book, id=" + id + ", query=" + sql); 1531 } 1532 return id; 1533 } catch ( Exception e ) { 1534 Log.e("cr3db", "insert failed: " + e.getMessage()); 1535 Log.e("cr3db", "values: " + valueBuf.toString()); 1536 return null; 1537 } 1538 } update( Long id )1539 boolean update( Long id ) 1540 { 1541 if ( fields.size()==0 ) 1542 return false; 1543 beginChanges(); 1544 StringBuilder buf = new StringBuilder("UPDATE "); 1545 buf.append(tableName); 1546 buf.append(" SET "); 1547 boolean first = true; 1548 for ( String field : fields ) { 1549 if ( !first ) 1550 buf.append(","); 1551 buf.append(field); 1552 buf.append("=?"); 1553 first = false; 1554 } 1555 buf.append(" WHERE id=" + id ); 1556 vlog.v("executing " + buf); 1557 mDB.execSQL(buf.toString(), values.toArray()); 1558 return true; 1559 } fromFormat( DocumentFormat f )1560 Long fromFormat( DocumentFormat f ) 1561 { 1562 if ( f==null ) 1563 return null; 1564 return (long)f.ordinal(); 1565 } QueryHelper( FileInfo newValue, FileInfo oldValue )1566 QueryHelper( FileInfo newValue, FileInfo oldValue ) 1567 { 1568 this("book"); 1569 add("pathname", newValue.getPathName(), oldValue.getPathName()); 1570 add("folder_fk", getFolderId(newValue.path), getFolderId(oldValue.path)); 1571 add("filename", newValue.filename, oldValue.filename); 1572 add("arcname", newValue.arcname, oldValue.arcname); 1573 add("title", newValue.title, oldValue.title); 1574 add("series_fk", getSeriesId(newValue.series), getSeriesId(oldValue.series)); 1575 add("series_number", (long)newValue.seriesNumber, (long)oldValue.seriesNumber); 1576 add("format", fromFormat(newValue.format), fromFormat(oldValue.format)); 1577 add("filesize", (long)newValue.size, (long)oldValue.size); 1578 add("arcsize", (long)newValue.arcsize, (long)oldValue.arcsize); 1579 add("last_access_time", (long)newValue.lastAccessTime, (long)oldValue.lastAccessTime); 1580 add("create_time", (long)newValue.createTime, (long)oldValue.createTime); 1581 add("flags", (long)newValue.flags, (long)oldValue.flags); 1582 add("language", newValue.language, oldValue.language); 1583 add("description", newValue.description, oldValue.description); 1584 add("crc32", newValue.crc32, oldValue.crc32); 1585 add("domVersion", newValue.domVersion, oldValue.domVersion); 1586 add("rendFlags", newValue.blockRenderingFlags, oldValue.blockRenderingFlags); 1587 if (fields.size() == 0) 1588 vlog.v("QueryHelper: no fields to update"); 1589 } QueryHelper( Bookmark newValue, Bookmark oldValue, long bookId )1590 QueryHelper( Bookmark newValue, Bookmark oldValue, long bookId ) 1591 { 1592 this("bookmark"); 1593 add("book_fk", bookId, oldValue.getId()!=null ? bookId : null); 1594 add("type", newValue.getType(), oldValue.getType()); 1595 add("percent", newValue.getPercent(), oldValue.getPercent()); 1596 add("shortcut", newValue.getShortcut(), oldValue.getShortcut()); 1597 add("start_pos", newValue.getStartPos(), oldValue.getStartPos()); 1598 add("end_pos", newValue.getEndPos(), oldValue.getEndPos()); 1599 add("title_text", newValue.getTitleText(), oldValue.getTitleText()); 1600 add("pos_text", newValue.getPosText(), oldValue.getPosText()); 1601 add("comment_text", newValue.getCommentText(), oldValue.getCommentText()); 1602 add("time_stamp", newValue.getTimeStamp(), oldValue.getTimeStamp()); 1603 add("time_elapsed", newValue.getTimeElapsed(), oldValue.getTimeElapsed()); 1604 } 1605 } 1606 1607 private static final String READ_FILEINFO_FIELDS = 1608 "b.id AS id, pathname," + 1609 "f.name as path, " + 1610 "filename, arcname, title, " + 1611 "(SELECT GROUP_CONCAT(a.name,'|') FROM author a JOIN book_author ba ON a.id=ba.author_fk WHERE ba.book_fk=b.id) as authors, " + 1612 "(SELECT GROUP_CONCAT(g.code,'|') FROM genre g JOIN book_genre bg ON g.id=bg.genre_fk WHERE bg.book_fk=b.id) as genres, " + 1613 "s.name as series_name, " + 1614 "series_number, " + 1615 "format, filesize, arcsize, " + 1616 "create_time, last_access_time, flags, language, description, crc32, domVersion, rendFlags "; 1617 1618 private static final String READ_FILEINFO_SQL = 1619 "SELECT " + 1620 READ_FILEINFO_FIELDS + 1621 "FROM book b " + 1622 "LEFT JOIN series s ON s.id=b.series_fk " + 1623 "LEFT JOIN folder f ON f.id=b.folder_fk "; 1624 readFileInfoFromCursor(FileInfo fileInfo, Cursor rs)1625 private void readFileInfoFromCursor(FileInfo fileInfo, Cursor rs) { 1626 int i = 0; 1627 fileInfo.id = rs.getLong(i++); 1628 String pathName = rs.getString(i++); 1629 String[] parts = FileInfo.splitArcName(pathName); 1630 fileInfo.pathname = parts[0]; 1631 fileInfo.path = rs.getString(i++); 1632 fileInfo.filename = rs.getString(i++); 1633 fileInfo.arcname = rs.getString(i++); 1634 fileInfo.title = rs.getString(i++); 1635 fileInfo.authors = rs.getString(i++); 1636 fileInfo.genres = rs.getString(i++); 1637 fileInfo.series = rs.getString(i++); 1638 fileInfo.seriesNumber = rs.getInt(i++); 1639 fileInfo.format = DocumentFormat.byId(rs.getInt(i++)); 1640 fileInfo.size = rs.getInt(i++); 1641 fileInfo.arcsize = rs.getInt(i++); 1642 fileInfo.createTime = rs.getInt(i++); 1643 fileInfo.lastAccessTime = rs.getInt(i++); 1644 fileInfo.flags = rs.getInt(i++); 1645 fileInfo.language = rs.getString(i++); 1646 fileInfo.description = rs.getString(i++); 1647 fileInfo.crc32 = rs.getLong(i++); 1648 fileInfo.domVersion = rs.getInt(i++); 1649 fileInfo.blockRenderingFlags = rs.getInt(i++); 1650 fileInfo.isArchive = fileInfo.arcname != null; 1651 } 1652 findBooks(String sql, ArrayList<FileInfo> list)1653 private boolean findBooks(String sql, ArrayList<FileInfo> list) { 1654 boolean found = false; 1655 try (Cursor rs = mDB.rawQuery(sql, null)) { 1656 if (rs.moveToFirst()) { 1657 do { 1658 FileInfo fileInfo = new FileInfo(); 1659 readFileInfoFromCursor(fileInfo, rs); 1660 if (!fileInfo.fileExists()) 1661 continue; 1662 fileInfoCache.put(fileInfo); 1663 list.add(new FileInfo(fileInfo)); 1664 found = true; 1665 } while (rs.moveToNext()); 1666 } 1667 } 1668 return found; 1669 } 1670 findSeries(int maxCount, String seriesPattern)1671 private String findSeries(int maxCount, String seriesPattern) { 1672 StringBuilder buf = new StringBuilder(); 1673 String sql = "SELECT id, name FROM series"; 1674 int count = 0; 1675 try (Cursor rs = mDB.rawQuery(sql, null)) { 1676 if (rs.moveToFirst()) { 1677 do { 1678 long id = rs.getLong(0); 1679 String name = rs.getString(1); 1680 if (Utils.matchPattern(name, seriesPattern)) { 1681 if (buf.length() != 0) 1682 buf.append(","); 1683 buf.append(id); 1684 count++; 1685 if (count >= maxCount) 1686 break; 1687 } 1688 } while (rs.moveToNext()); 1689 } 1690 } 1691 return buf.toString(); 1692 } 1693 findByPatterns(int maxCount, String authors, String title, String series, String filename)1694 public ArrayList<FileInfo> findByPatterns(int maxCount, String authors, String title, String series, String filename) 1695 { 1696 beginReading(); 1697 ArrayList<FileInfo> list = new ArrayList<>(); 1698 StringBuilder buf = new StringBuilder(); 1699 boolean hasCondition = false; 1700 if ( authors!=null && authors.length()>0 ) { 1701 // When synchronizing from the cloud, the 'authors' variable can contain multiple authors separated by '|'. 1702 // See MainDB.READ_FILEINFO_FIELDS 1703 String[] authorsArray = authors.split("\\|"); 1704 StringBuilder authorIds = new StringBuilder(); 1705 for (String author : authorsArray) { 1706 String ids = findAuthors(maxCount, author); 1707 if (ids.length() > 0) { 1708 if (authorIds.length() > 0) 1709 authorIds.append(","); 1710 authorIds.append(ids); 1711 } 1712 } 1713 if (authorIds.length() == 0) 1714 return list; 1715 if (buf.length() > 0) 1716 buf.append(" AND "); 1717 buf.append(" b.id IN (SELECT ba.book_fk FROM book_author ba WHERE ba.author_fk IN (").append(authorIds).append(")) "); 1718 hasCondition = true; 1719 } 1720 if ( series!=null && series.length()>0 ) { 1721 String seriesIds = findSeries(maxCount, series); 1722 if (seriesIds.length() == 0) 1723 return list; 1724 if ( buf.length()>0 ) 1725 buf.append(" AND "); 1726 buf.append(" b.series_fk IN (").append(seriesIds).append(") "); 1727 hasCondition = true; 1728 } 1729 if ( title!=null && title.length()>0 ) { 1730 hasCondition = true; 1731 } 1732 if ( filename!=null && filename.length()>0 ) { 1733 hasCondition = true; 1734 } 1735 if (!hasCondition) 1736 return list; 1737 1738 String condition = buf.length()==0 ? "" : " WHERE " + buf.toString(); 1739 String sql = READ_FILEINFO_SQL + condition; 1740 Log.d("cr3", "sql: " + sql ); 1741 try (Cursor rs = mDB.rawQuery(sql, null)) { 1742 if (rs.moveToFirst()) { 1743 int count = 0; 1744 do { 1745 if (title != null && title.length() > 0) 1746 if (!Utils.matchPattern(rs.getString(5), title)) 1747 continue; 1748 if (filename != null && filename.length() > 0) 1749 if (!Utils.matchPattern(rs.getString(3), filename)) 1750 continue; 1751 FileInfo fi = new FileInfo(); 1752 readFileInfoFromCursor(fi, rs); 1753 list.add(fi); 1754 fileInfoCache.put(fi); 1755 count++; 1756 } while (count < maxCount && rs.moveToNext()); 1757 } 1758 } 1759 endReading(); 1760 return list; 1761 } 1762 findByGenre(String genreCode, boolean showEmptyGenres)1763 public ArrayList<FileInfo> findByGenre(String genreCode, boolean showEmptyGenres) { 1764 ArrayList<FileInfo> list = new ArrayList<>(); 1765 boolean OutpuSubGenres = true; 1766 if (genreCode.endsWith(":all")) { 1767 OutpuSubGenres = false; 1768 genreCode = genreCode.substring(0, genreCode.length() - 4); 1769 } 1770 GenresCollection.GenreRecord genreRecord = Services.getGenresCollection().byCode(genreCode); 1771 if (null == genreRecord) 1772 return list; 1773 int book_count = 0; 1774 String sql; 1775 beginReading(); 1776 if (genreRecord.getLevel() == 0 && OutpuSubGenres) { 1777 // special item to include all child genres 1778 FileInfo item = new FileInfo(); 1779 item.isDirectory = true; 1780 item.pathname = FileInfo.GENRES_PREFIX + genreRecord.getCode() + ":all"; 1781 item.filename = genreRecord.getName(); 1782 item.isListed = true; 1783 item.isScanned = true; 1784 item.id = (long)-1; // fake id 1785 // get books count 1786 StringBuilder where_clause = new StringBuilder(" WHERE "); 1787 Iterator<GenresCollection.GenreRecord> it = genreRecord.getChilds().iterator(); 1788 while (it.hasNext()) { 1789 where_clause.append("bg.genre_fk=").append(it.next().getId()); 1790 if (it.hasNext()) 1791 where_clause.append(" OR "); 1792 } 1793 sql = "SELECT count(DISTINCT book_fk) as book_count FROM book_genre bg " + where_clause.toString(); 1794 Log.d("cr3", "sql: " + sql ); 1795 try (Cursor rs = mDB.rawQuery(sql, null)) { 1796 if (rs.moveToFirst()) { 1797 do { 1798 book_count = rs.getInt(0); 1799 } while (rs.moveToNext()); 1800 } 1801 } 1802 item.tag = FileInfo.GENRE_DATA_INCCHILD_MASK | book_count; 1803 list.add(item); 1804 1805 // child genres 1806 sql = "SELECT code, " + 1807 "(SELECT COUNT(DISTINCT book_fk) FROM book_genre bg " + 1808 " WHERE bg.genre_fk=g.id " + 1809 ") as book_count " + 1810 "FROM genre g " + 1811 "INNER JOIN genre_hier gh ON gh.genre_fk = g.id " + 1812 "WHERE gh.group_fk=" + genreRecord.getId(); 1813 Log.d("cr3", "sql: " + sql ); 1814 try (Cursor rs = mDB.rawQuery(sql, null)) { 1815 if (rs.moveToFirst()) { 1816 do { 1817 String code = rs.getString(0); 1818 book_count = rs.getInt(1); 1819 if (book_count > 0 || showEmptyGenres) { 1820 item = new FileInfo(); 1821 item.isDirectory = true; 1822 item.pathname = FileInfo.GENRES_PREFIX + code; 1823 item.filename = Services.getGenresCollection().translate(code); 1824 item.isListed = true; 1825 item.isScanned = true; 1826 item.id = (long) -1; // fake id 1827 item.tag = book_count; 1828 list.add(item); 1829 } 1830 } while (rs.moveToNext()); 1831 } 1832 } 1833 } else { 1834 // Find all books for this genre (or genre group) 1835 StringBuilder where_clause = new StringBuilder(" WHERE "); 1836 if (genreRecord.hasChilds()) { 1837 Iterator<GenresCollection.GenreRecord> it = genreRecord.getChilds().iterator(); 1838 while (it.hasNext()) { 1839 where_clause.append("bg.genre_fk=").append(it.next().getId()); 1840 if (it.hasNext()) 1841 where_clause.append(" OR "); 1842 } 1843 } else { 1844 where_clause.append("bg.genre_fk=").append(genreRecord.getId()); 1845 } 1846 sql = READ_FILEINFO_SQL + " JOIN book_genre bg ON (bg.book_fk=b.id)" + where_clause.toString(); 1847 Log.d("cr3", "sql: " + sql ); 1848 try (Cursor rs = mDB.rawQuery(sql, null)) { 1849 if (rs.moveToFirst()) { 1850 do { 1851 FileInfo fi = new FileInfo(); 1852 readFileInfoFromCursor(fi, rs); 1853 list.add(fi); 1854 fileInfoCache.put(fi); 1855 } while (rs.moveToNext()); 1856 } 1857 } 1858 } 1859 endReading(); 1860 return list; 1861 } 1862 findByFingerprints(int maxCount, Collection<String> fingerprints)1863 public ArrayList<FileInfo> findByFingerprints(int maxCount, Collection<String> fingerprints) 1864 { 1865 // TODO: replace crc32 with sha512 1866 1867 ArrayList<FileInfo> list = new ArrayList<>(); 1868 if (fingerprints.size() < 1) 1869 return list; 1870 1871 beginReading(); 1872 StringBuilder condition = new StringBuilder(" WHERE "); 1873 Iterator<String> it = fingerprints.iterator(); 1874 while (it.hasNext()) { 1875 condition.append("b.crc32=").append(it.next()); 1876 if (it.hasNext()) 1877 condition.append(" OR "); 1878 } 1879 String sql = READ_FILEINFO_SQL + condition.toString(); 1880 Log.d("cr3", "sql: " + sql ); 1881 try (Cursor rs = mDB.rawQuery(sql, null)) { 1882 if (rs.moveToFirst()) { 1883 int count = 0; 1884 do { 1885 FileInfo fi = new FileInfo(); 1886 readFileInfoFromCursor(fi, rs); 1887 list.add(fi); 1888 fileInfoCache.put(fi); 1889 count++; 1890 } while (count < maxCount && rs.moveToNext()); 1891 } 1892 } 1893 endReading(); 1894 return list; 1895 } 1896 loadFileInfos(ArrayList<String> pathNames)1897 public ArrayList<FileInfo> loadFileInfos(ArrayList<String> pathNames) { 1898 ArrayList<FileInfo> list = new ArrayList<>(); 1899 if (!isOpened()) 1900 return list; 1901 try { 1902 beginReading(); 1903 for (String path : pathNames) { 1904 FileInfo file = findFileInfoByPathname(path, true); 1905 if (file != null) 1906 list.add(new FileInfo(file)); 1907 } 1908 endReading(); 1909 } catch (Exception e) { 1910 log.e("Exception while loading books from DB", e); 1911 } 1912 return list; 1913 } 1914 deleteRecentPosition( FileInfo fileInfo )1915 public void deleteRecentPosition( FileInfo fileInfo ) { 1916 Long bookId = getBookId(fileInfo); 1917 if (bookId == null) 1918 return; 1919 execSQLIgnoreErrors("DELETE FROM bookmark WHERE book_fk=" + bookId + " AND type=0"); 1920 execSQLIgnoreErrors("UPDATE book SET last_access_time=0 WHERE id=" + bookId); 1921 } 1922 deleteBookmark(Bookmark bm)1923 public void deleteBookmark(Bookmark bm) { 1924 if (bm.getId() == null) 1925 return; 1926 execSQLIgnoreErrors("DELETE FROM bookmark WHERE id=" + bm.getId()); 1927 } 1928 loadBookInfo(FileInfo fileInfo)1929 public BookInfo loadBookInfo(FileInfo fileInfo) { 1930 if (!isOpened()) 1931 return null; 1932 try { 1933 FileInfo cached = fileInfoCache.get(fileInfo.getPathName()); 1934 if (cached != null) { 1935 BookInfo book = new BookInfo(new FileInfo(cached)); 1936 loadBookmarks(book); 1937 return book; 1938 } 1939 if (loadByPathname(fileInfo)) { 1940 BookInfo book = new BookInfo(new FileInfo(fileInfo)); 1941 loadBookmarks(book); 1942 return book; 1943 } 1944 } catch (Exception e) { 1945 // ignore 1946 } 1947 return null; 1948 } 1949 loadFileInfo(String pathName)1950 public FileInfo loadFileInfo(String pathName) { 1951 if (!isOpened()) 1952 return null; 1953 try { 1954 FileInfo cached = fileInfoCache.get(pathName); 1955 if (cached != null) { 1956 return new FileInfo(cached); 1957 } 1958 FileInfo fileInfo = new FileInfo(pathName); 1959 if (loadByPathname(fileInfo)) { 1960 fileInfoCache.put(fileInfo); 1961 return new FileInfo(fileInfo); 1962 } 1963 } catch (Exception e) { 1964 // ignore 1965 } 1966 return null; 1967 } 1968 loadByPathname(FileInfo fileInfo)1969 private boolean loadByPathname(FileInfo fileInfo) { 1970 if (findBy(fileInfo, "pathname", fileInfo.getPathName())) { 1971 fileInfoCache.put(fileInfo); 1972 return true; 1973 } 1974 1975 FileInfo moved = findMovedFileInfo(fileInfo.getPathName()); 1976 if (moved != null) { 1977 fileInfo.assign(moved); 1978 return true; 1979 } 1980 1981 return false; 1982 } 1983 1984 // private boolean loadById( FileInfo fileInfo ) { 1985 // return findBy(fileInfo, "b.id", fileInfo.id); 1986 // } 1987 getBookId(FileInfo fileInfo)1988 private Long getBookId(FileInfo fileInfo) { 1989 Long bookId = null; 1990 if (fileInfo == null) 1991 return bookId; 1992 String pathName = fileInfo.getPathName(); 1993 FileInfo cached = fileInfoCache.get(pathName); 1994 if (cached != null) { 1995 bookId = cached.id; 1996 } 1997 if (bookId == null) 1998 bookId = fileInfo.id; 1999 if (bookId == null) 2000 loadByPathname(fileInfo); 2001 return bookId; 2002 } deleteBook(FileInfo fileInfo)2003 public Long deleteBook(FileInfo fileInfo) 2004 { 2005 if (fileInfo == null) 2006 return null; 2007 Long bookId = getBookId(fileInfo); 2008 fileInfoCache.remove(fileInfo); 2009 if (bookId == null) 2010 return null; 2011 execSQLIgnoreErrors("DELETE FROM bookmark WHERE book_fk=" + bookId); 2012 execSQLIgnoreErrors("DELETE FROM book_author WHERE book_fk=" + bookId); 2013 execSQLIgnoreErrors("DELETE FROM book_genre WHERE book_fk=" + bookId); 2014 execSQLIgnoreErrors("DELETE FROM book WHERE id=" + bookId); 2015 return bookId; 2016 } 2017 correctFilePaths()2018 public void correctFilePaths() { 2019 Log.i("cr3", "checking data for path correction"); 2020 beginReading(); 2021 int rowCount = 0; 2022 Map<String, Long> map = new HashMap<>(); 2023 String sql = "SELECT id, pathname FROM book"; 2024 try (Cursor rs = mDB.rawQuery(sql, null)) { 2025 if (rs.moveToFirst()) { 2026 // read DB 2027 do { 2028 Long id = rs.getLong(0); 2029 String pathname = rs.getString(1); 2030 String corrected = pathCorrector.normalize(pathname); 2031 if (pathname == null) 2032 continue; 2033 rowCount++; 2034 if (corrected == null) { 2035 Log.w("cr3", "DB contains unknown path " + pathname); 2036 } else if (!pathname.equals(corrected)) { 2037 map.put(pathname, id); 2038 } 2039 } while (rs.moveToNext()); 2040 } 2041 } catch (Exception e) { 2042 Log.e("cr3", "exception while loading list books to correct paths", e); 2043 } 2044 Log.i("cr3", "Total rows: " + rowCount + ", " + (map.size() > 0 ? "need to correct " + map.size() + " items" : "no corrections required")); 2045 if (map.size() > 0) { 2046 beginChanges(); 2047 int count = 0; 2048 for (Map.Entry<String, Long> entry : map.entrySet()) { 2049 String pathname = entry.getKey(); 2050 String corrected = pathCorrector.normalize(pathname); 2051 if (corrected != null && !corrected.equals(pathname)) { 2052 count++; 2053 execSQLIgnoreErrors("update book set pathname=" + quoteSqlString(corrected) + " WHERE id=" + entry.getValue()); 2054 } 2055 } 2056 flush(); 2057 log.i("Finished. Rows corrected: " + count); 2058 } 2059 } 2060 2061 private MountPathCorrector pathCorrector; setPathCorrector(MountPathCorrector corrector)2062 public void setPathCorrector(MountPathCorrector corrector) { 2063 this.pathCorrector = corrector; 2064 if (pathCorrectionRequired) { 2065 correctFilePaths(); 2066 pathCorrectionRequired = false; 2067 } 2068 } 2069 } 2070