1 package org.coolreader.crengine; 2 3 import android.util.Log; 4 import org.coolreader.R; 5 import org.coolreader.db.CRDBService; 6 import org.coolreader.plugins.OnlineStorePluginManager; 7 import org.coolreader.plugins.OnlineStoreWrapper; 8 9 import java.io.File; 10 import java.util.ArrayList; 11 import java.util.HashMap; 12 import java.util.HashSet; 13 import java.util.Map; 14 import java.util.Set; 15 import java.util.zip.ZipEntry; 16 17 public class Scanner extends FileInfoChangeSource { 18 19 public static final Logger log = L.create("sc"); 20 21 HashMap<String, FileInfo> mFileList = new HashMap<>(); 22 // ArrayList<FileInfo> mFilesForParsing = new ArrayList<FileInfo>(); 23 FileInfo mRoot; 24 25 boolean mHideEmptyDirs = true; 26 setHideEmptyDirs( boolean flgHide )27 public void setHideEmptyDirs( boolean flgHide ) { 28 mHideEmptyDirs = flgHide; 29 } 30 31 private boolean dirScanEnabled = true; getDirScanEnabled()32 public boolean getDirScanEnabled() 33 { 34 return dirScanEnabled; 35 } 36 setDirScanEnabled(boolean dirScanEnabled)37 public void setDirScanEnabled(boolean dirScanEnabled) 38 { 39 this.dirScanEnabled = dirScanEnabled; 40 } 41 scanZip( FileInfo zip )42 private FileInfo scanZip( FileInfo zip ) 43 { 44 try { 45 File zf = new File(zip.pathname); 46 long arcsize = zf.length(); 47 //ZipFile file = new ZipFile(zf); 48 ArrayList<ZipEntry> entries = engine.getArchiveItems(zip.pathname); 49 ArrayList<FileInfo> items = new ArrayList<FileInfo>(); 50 //for ( Enumeration<?> e = file.entries(); e.hasMoreElements(); ) { 51 for ( ZipEntry entry : entries ) { 52 if ( entry.isDirectory() ) 53 continue; 54 String name = entry.getName(); 55 FileInfo item = new FileInfo(); 56 item.format = DocumentFormat.byExtension(name); 57 if ( item.format==null ) 58 continue; 59 File f = new File(name); 60 item.filename = f.getName(); 61 item.path = f.getPath(); 62 item.pathname = entry.getName(); 63 item.size = (int)entry.getSize(); 64 //item.createTime = entry.getTime(); 65 item.createTime = zf.lastModified(); 66 item.arcname = zip.pathname; 67 //item.arcsize = (int)entry.getCompressedSize(); 68 item.arcsize = zip.size; 69 item.isArchive = true; 70 items.add(item); 71 } 72 if ( items.size()==0 ) { 73 L.i("Supported files not found in " + zip.pathname); 74 return null; 75 } else if ( items.size()==1 ) { 76 // single supported file in archive 77 FileInfo item = items.get(0); 78 item.isArchive = true; 79 item.isDirectory = false; 80 return item; 81 } else { 82 zip.isArchive = true; 83 zip.isDirectory = true; 84 zip.isListed = true; 85 for ( FileInfo item : items ) { 86 item.parent = zip; 87 zip.addFile(item); 88 } 89 return zip; 90 } 91 } catch ( Exception e ) { 92 L.e("IOException while opening " + zip.pathname + " " + e.getMessage()); 93 } 94 return null; 95 } 96 listDirectory(FileInfo baseDir)97 public boolean listDirectory(FileInfo baseDir) { 98 return listDirectory(baseDir, true); 99 } 100 101 /** 102 * Adds dir and file children to directory FileInfo item. 103 * @param baseDir is directory to list files and dirs for 104 * @return true if successful. 105 */ listDirectory(FileInfo baseDir, boolean onlySupportedFormats)106 public boolean listDirectory(FileInfo baseDir, boolean onlySupportedFormats) 107 { 108 Set<String> knownItems = null; 109 if ( baseDir.isListed ) { 110 knownItems = new HashSet<String>(); 111 for ( int i=baseDir.itemCount()-1; i>=0; i-- ) { 112 FileInfo item = baseDir.getItem(i); 113 if ( !item.exists() ) { 114 // remove item from list 115 baseDir.removeChild(item); 116 } else { 117 knownItems.add(item.getBasePath()); 118 } 119 } 120 } 121 try { 122 File dir = new File(baseDir.pathname); 123 //File[] items = dir.listFiles(); 124 // To resolve unhandled exception 125 // 'JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0' 126 // that can be produced by invalid filename (broken sdcard, etc) 127 // or 'JNI WARNING: input is not valid Modified UTF-8: illegal start byte 0xf0' 128 // that can be generated if 4-byte UTF-8 sequence found in the filename, 129 // we implement own directory listing method instead of File.listFiles(). 130 // TODO: replace other occurrences of the method File.listFiles(). 131 File[] items = Engine.listFiles(dir); 132 // process normal files 133 if ( items!=null ) { 134 for ( File f : items ) { 135 // check whether file is a link 136 if (Engine.isLink(f.getAbsolutePath()) != null) { 137 log.w("skipping " + f + " because it's a link"); 138 continue; 139 } 140 if (!f.isDirectory()) { 141 // regular file 142 if (f.getName().startsWith(".")) 143 continue; // treat files beginning with '.' as hidden 144 if (f.getName().equalsIgnoreCase("LOST.DIR")) 145 continue; // system directory 146 String pathName = f.getAbsolutePath(); 147 if ( knownItems!=null && knownItems.contains(pathName) ) 148 continue; 149 if (engine.isRootsMountPoint(pathName)) { 150 // skip mount root 151 continue; 152 } 153 boolean isZip = pathName.toLowerCase().endsWith(".zip"); 154 FileInfo item = mFileList.get(pathName); 155 boolean isNew = false; 156 if ( item==null ) { 157 item = new FileInfo( f ); 158 if ( isZip ) { 159 item = scanZip( item ); 160 if ( item==null ) 161 continue; 162 if ( item.isDirectory ) { 163 // many supported files in ZIP 164 item.parent = baseDir; 165 baseDir.addDir(item); 166 for ( int i=0; i<item.fileCount(); i++ ) { 167 FileInfo file = item.getFile(i); 168 mFileList.put(file.getPathName(), file); 169 } 170 } else { 171 item.parent = baseDir; 172 baseDir.addFile(item); 173 mFileList.put(pathName, item); 174 } 175 continue; 176 } 177 isNew = true; 178 } 179 if ( !onlySupportedFormats || item.format!=null ) { 180 item.parent = baseDir; 181 baseDir.addFile(item); 182 if ( isNew ) 183 mFileList.put(pathName, item); 184 } 185 } 186 } 187 // process directories 188 for ( File f : items ) { 189 if ( f.isDirectory() ) { 190 if ( f.getName().startsWith(".") ) 191 continue; // treat dirs beginning with '.' as hidden 192 FileInfo item = new FileInfo( f ); 193 if ( knownItems!=null && knownItems.contains(item.getPathName()) ) 194 continue; 195 item.parent = baseDir; 196 baseDir.addDir(item); 197 } 198 } 199 } 200 baseDir.isListed = true; 201 return true; 202 } catch ( Exception e ) { 203 L.e("Exception while listing directory " + baseDir.pathname, e); 204 baseDir.isListed = true; 205 return false; 206 } 207 } 208 209 public static class ScanControl { 210 volatile private boolean stopped = false; isStopped()211 public boolean isStopped() { 212 return stopped; 213 } stop()214 public void stop() { 215 stopped = true; 216 } 217 } 218 219 /** 220 * Call this method (in GUI thread) to update views if directory content is changed outside. 221 * @param dir is directory with changed content 222 */ onDirectoryContentChanged(FileInfo dir)223 public void onDirectoryContentChanged(FileInfo dir) { 224 log.v("onDirectoryContentChanged(" + dir.getPathName() + ")"); 225 onChange(dir, false); 226 } 227 228 /** 229 * For all files in directory, retrieve metadata from DB or scan and save into DB. 230 * Call in GUI thread only! 231 * @param baseDir is directory with files to lookup/scan; file items will be updated with info from file metadata or DB 232 * @param readyCallback is Runable to call when operation is finished or stopped (will be called in GUI thread) 233 * @param control allows to stop long operation 234 */ scanDirectoryFiles(final CRDBService.LocalBinder db, final FileInfo baseDir, final ScanControl control, final Engine.ProgressControl progress, final Runnable readyCallback)235 private void scanDirectoryFiles(final CRDBService.LocalBinder db, final FileInfo baseDir, final ScanControl control, final Engine.ProgressControl progress, final Runnable readyCallback) { 236 // GUI thread 237 BackgroundThread.ensureGUI(); 238 log.d("scanDirectoryFiles(" + baseDir.getPathName() + ") "); 239 240 // store list of files to scan 241 ArrayList<String> pathNames = new ArrayList<>(); 242 for (int i=0; i < baseDir.fileCount(); i++) { 243 pathNames.add(baseDir.getFile(i).getPathName()); 244 } 245 246 if (pathNames.size() == 0) { 247 readyCallback.run(); 248 return; 249 } 250 251 // list all subdirectories 252 for (int i=0; i < baseDir.dirCount(); i++) { 253 if (control.isStopped()) 254 break; 255 listDirectory(baseDir.getDir(i)); 256 } 257 258 // load book infos for files 259 db.loadFileInfos(pathNames, list -> { 260 log.v("onFileInfoListLoaded"); 261 // GUI thread 262 final ArrayList<FileInfo> filesForParsing = new ArrayList<>(); 263 ArrayList<FileInfo> filesForSave = new ArrayList<>(); 264 Map<String, FileInfo> mapOfFilesFoundInDb = new HashMap<>(); 265 for (FileInfo f : list) 266 mapOfFilesFoundInDb.put(f.getPathName(), f); 267 268 for (int i=0; i<baseDir.fileCount(); i++) { 269 FileInfo item = baseDir.getFile(i); 270 FileInfo fromDB = mapOfFilesFoundInDb.get(item.getPathName()); 271 // check the relevance of data in the database 272 if (fromDB != null) { 273 if (fromDB.crc32 == 0 || fromDB.size != item.size || fromDB.arcsize != item.arcsize ) { 274 // to force rescan and update data in DB 275 log.v("The found entry in the database is outdated (crc32=0), need to rescan " + fromDB.toString()); 276 fromDB = null; 277 } 278 if (null != fromDB && DocumentFormat.FB2 == fromDB.format && null == fromDB.genres) { 279 // to force rescan and update data in DB 280 log.v("The found entry in the database is outdated (genres=null), need to rescan " + fromDB.toString()); 281 fromDB = null; 282 } 283 } else { 284 // not found in DB 285 // for new files set latest DOM level and max block rendering flags 286 item.domVersion = Engine.DOM_VERSION_CURRENT; 287 item.blockRenderingFlags = Engine.BLOCK_RENDERING_FLAGS_WEB; 288 } 289 if (fromDB != null) { 290 // use DB value 291 baseDir.setFile(i, fromDB); 292 } else { 293 if (item.format.canParseProperties()) { 294 filesForParsing.add(new FileInfo(item)); 295 } else { 296 Engine.updateFileCRC32(item); 297 filesForSave.add(new FileInfo(item)); 298 } 299 } 300 } 301 if (filesForSave.size() > 0) { 302 db.saveFileInfos(filesForSave); 303 } 304 if (filesForParsing.size() == 0 || control.isStopped()) { 305 readyCallback.run(); 306 return; 307 } 308 // scan files in Background thread 309 BackgroundThread.instance().postBackground(() -> { 310 // Background thread 311 final ArrayList<FileInfo> filesForSave1 = new ArrayList<>(); 312 try { 313 int count = filesForParsing.size(); 314 for ( int i=0; i<count; i++ ) { 315 if (control.isStopped()) 316 break; 317 progress.setProgress(i * 10000 / count); 318 FileInfo item = filesForParsing.get(i); 319 engine.scanBookProperties(item); 320 filesForSave1.add(item); 321 } 322 } catch (Exception e) { 323 L.e("Exception while scanning", e); 324 } 325 progress.hide(); 326 // jump to GUI thread 327 BackgroundThread.instance().postGUI(() -> { 328 // GUI thread 329 try { 330 if (filesForSave1.size() > 0) { 331 db.saveFileInfos(filesForSave1); 332 } 333 for (FileInfo file : filesForSave1) 334 baseDir.setFile(file); 335 } catch (Exception e ) { 336 L.e("Exception while scanning", e); 337 } 338 // call finish handler 339 readyCallback.run(); 340 }); 341 }); 342 }); 343 } 344 345 /** 346 * Scan single directory for dir and file properties in background thread. 347 * @param baseDir is directory to scan 348 * @param readyCallback is called on completion 349 * @param recursiveScan is true to scan subdirectories recursively, false to scan current directory only 350 * @param scanControl is to stop long scanning 351 */ scanDirectory(final CRDBService.LocalBinder db, final FileInfo baseDir, final Runnable readyCallback, final boolean recursiveScan, final ScanControl scanControl)352 public void scanDirectory(final CRDBService.LocalBinder db, final FileInfo baseDir, final Runnable readyCallback, final boolean recursiveScan, final ScanControl scanControl) { 353 // Call in GUI thread only! 354 BackgroundThread.ensureGUI(); 355 356 log.d("scanDirectory(" + baseDir.getPathName() + ") " + (recursiveScan ? "recursive" : "")); 357 358 listDirectory(baseDir); 359 listSubtree( baseDir, 5, android.os.SystemClock.uptimeMillis() + 700 ); 360 if ( (!getDirScanEnabled() || baseDir.isScanned) && !recursiveScan ) { 361 readyCallback.run(); 362 return; 363 } 364 Engine.ProgressControl progress = engine.createProgress(recursiveScan ? 0 : R.string.progress_scanning); 365 scanDirectoryFiles(db, baseDir, scanControl, progress, new Runnable() { 366 @Override 367 public void run() { 368 // GUI thread 369 onDirectoryContentChanged(baseDir); 370 try { 371 if (scanControl.isStopped()) { 372 // scan is stopped 373 readyCallback.run(); 374 } else { 375 baseDir.isScanned = true; 376 377 if ( recursiveScan ) { 378 if (scanControl.isStopped()) { 379 // scan is stopped 380 readyCallback.run(); 381 return; 382 } 383 // make list of subdirectories to scan 384 final ArrayList<FileInfo> dirsToScan = new ArrayList<>(); 385 for ( int i=baseDir.dirCount()-1; i>=0; i-- ) { 386 File dir = new File(baseDir.getDir(i).getPathName()); 387 if (!engine.getPathCorrector().isRecursivePath(dir)) 388 dirsToScan.add(baseDir.getDir(i)); 389 } 390 final Runnable dirIterator = new Runnable() { 391 @Override 392 public void run() { 393 // process next directory from list 394 if (dirsToScan.size() == 0 || scanControl.isStopped()) { 395 readyCallback.run(); 396 return; 397 } 398 final FileInfo dir = dirsToScan.get(0); 399 dirsToScan.remove(0); 400 final Runnable callback = this; 401 BackgroundThread.instance().postGUI(() -> scanDirectory(db, dir, callback, true, scanControl)); 402 } 403 }; 404 dirIterator.run(); 405 } else { 406 readyCallback.run(); 407 } 408 } 409 } catch (Exception e) { 410 // treat as finished 411 readyCallback.run(); 412 } 413 } 414 }); 415 } 416 addRoot( String pathname, int resourceId, boolean listIt)417 private boolean addRoot( String pathname, int resourceId, boolean listIt) { 418 return addRoot( pathname, mActivity.getResources().getString(resourceId), listIt); 419 } 420 findRoot(String pathname)421 private FileInfo findRoot(String pathname) { 422 String normalized = engine.getPathCorrector().normalizeIfPossible(pathname); 423 for (int i = 0; i<mRoot.dirCount(); i++) { 424 FileInfo dir = mRoot.getDir(i); 425 if (normalized.equals(engine.getPathCorrector().normalizeIfPossible(dir.getPathName()))) 426 return dir; 427 } 428 return null; 429 } 430 addRoot( String pathname, String filename, boolean listIt)431 private boolean addRoot( String pathname, String filename, boolean listIt) { 432 FileInfo dir = new FileInfo(); 433 dir.isDirectory = true; 434 dir.pathname = pathname; 435 dir.filename = filename; 436 dir.title = filename; 437 if (findRoot(pathname) != null) { 438 log.w("skipping duplicate root " + pathname); 439 return false; // exclude duplicates 440 } 441 if (listIt) { 442 log.i("Checking FS root " + pathname); 443 if (!dir.isReadableDirectory()) { // isWritableDirectory 444 log.w("Skipping " + pathname + " - it's not a readable directory"); 445 return false; 446 } 447 if (!listDirectory(dir)) { 448 log.w("Skipping " + pathname + " - listing failed"); 449 return false; 450 } 451 log.i("Adding FS root: " + pathname + " " + filename); 452 } 453 mRoot.addDir(dir); 454 dir.parent = mRoot; 455 if (!listIt) { 456 dir.isListed = true; 457 dir.isScanned = true; 458 } 459 return true; 460 } 461 pathToFileInfo(String path)462 public FileInfo pathToFileInfo(String path) { 463 if (path == null || path.length() == 0) 464 return null; 465 if (FileInfo.OPDS_LIST_TAG.equals(path)) 466 return createOPDSRoot(); 467 else if (FileInfo.SEARCH_SHORTCUT_TAG.equals(path)) 468 return createSearchRoot(); 469 else if (FileInfo.RECENT_DIR_TAG.equals(path)) 470 return getRecentDir(); 471 else if (FileInfo.GENRES_TAG.equals(path)) 472 return createGenresRoot(); 473 else if (FileInfo.AUTHORS_TAG.equals(path)) 474 return createAuthorsRoot(); 475 else if (FileInfo.TITLE_TAG.equals(path)) 476 return createTitleRoot(); 477 else if (FileInfo.SERIES_TAG.equals(path)) 478 return createSeriesRoot(); 479 else if (FileInfo.RATING_TAG.equals(path)) 480 return createBooksByRatingRoot(); 481 else if (FileInfo.STATE_READING_TAG.equals(path)) 482 return createBooksByStateReadingRoot(); 483 else if (FileInfo.STATE_TO_READ_TAG.equals(path)) 484 return createBooksByStateToReadRoot(); 485 else if (FileInfo.STATE_FINISHED_TAG.equals(path)) 486 return createBooksByStateFinishedRoot(); 487 else if (path.startsWith(FileInfo.ONLINE_CATALOG_PLUGIN_PREFIX)) { 488 OnlineStoreWrapper w = OnlineStorePluginManager.getPlugin(mActivity, path); 489 if (w != null) 490 return w.createRootDirectory(); 491 return null; 492 } else if (path.startsWith(FileInfo.OPDS_DIR_PREFIX)) 493 return createOPDSDir(path); 494 else 495 return new FileInfo(path); 496 } 497 createOPDSRoot()498 public FileInfo createOPDSRoot() { 499 final FileInfo dir = new FileInfo(); 500 dir.isDirectory = true; 501 dir.pathname = FileInfo.OPDS_LIST_TAG; 502 dir.filename = mActivity.getString(R.string.mi_book_opds_root); 503 dir.isListed = true; 504 dir.isScanned = true; 505 return dir; 506 } 507 createOnlineLibraryPluginItem(String packageName, String label)508 public static FileInfo createOnlineLibraryPluginItem(String packageName, String label) { 509 final FileInfo dir = new FileInfo(); 510 dir.isDirectory = true; 511 if (packageName.startsWith(FileInfo.ONLINE_CATALOG_PLUGIN_PREFIX)) 512 dir.pathname = packageName; 513 else 514 dir.pathname = FileInfo.ONLINE_CATALOG_PLUGIN_PREFIX + packageName; 515 dir.filename = label; 516 dir.isListed = true; 517 dir.isScanned = true; 518 return dir; 519 } 520 addRoot(FileInfo dir)521 private void addRoot(FileInfo dir) { 522 dir.parent = mRoot; 523 mRoot.addDir(dir); 524 } 525 createRecentRoot()526 public FileInfo createRecentRoot() { 527 FileInfo dir = new FileInfo(); 528 dir.isDirectory = true; 529 dir.pathname = FileInfo.RECENT_DIR_TAG; 530 dir.filename = mActivity.getString(R.string.dir_recent_books); 531 dir.isListed = true; 532 dir.isScanned = true; 533 return dir; 534 } 535 addOPDSRoot()536 private void addOPDSRoot() { 537 addRoot(createOPDSRoot()); 538 } 539 createSearchRoot()540 public FileInfo createSearchRoot() { 541 FileInfo dir = new FileInfo(); 542 dir.isDirectory = true; 543 dir.pathname = FileInfo.SEARCH_SHORTCUT_TAG; 544 dir.filename = mActivity.getString(R.string.mi_book_search); 545 dir.isListed = true; 546 dir.isScanned = true; 547 return dir; 548 } 549 addSearchRoot()550 private void addSearchRoot() { 551 addRoot(createSearchRoot()); 552 } 553 createGenresRoot()554 public FileInfo createGenresRoot() { 555 FileInfo dir = new FileInfo(); 556 dir.isDirectory = true; 557 dir.pathname = FileInfo.GENRES_TAG; 558 dir.filename = mActivity.getString(R.string.folder_name_books_by_genre); 559 dir.isListed = true; 560 dir.isScanned = true; 561 return dir; 562 } 563 createAuthorsRoot()564 public FileInfo createAuthorsRoot() { 565 FileInfo dir = new FileInfo(); 566 dir.isDirectory = true; 567 dir.pathname = FileInfo.AUTHORS_TAG; 568 dir.filename = mActivity.getString(R.string.folder_name_books_by_author); 569 dir.isListed = true; 570 dir.isScanned = true; 571 return dir; 572 } 573 addAuthorsRoot()574 private void addAuthorsRoot() { 575 addRoot(createAuthorsRoot()); 576 } 577 createOPDSDir(String path)578 public FileInfo createOPDSDir(String path) { 579 FileInfo opds = mRoot.findItemByPathName(FileInfo.OPDS_LIST_TAG); 580 if (opds == null) 581 return null; 582 return opds.findItemByPathName(path); 583 } 584 createSeriesRoot()585 public FileInfo createSeriesRoot() { 586 FileInfo dir = new FileInfo(); 587 dir.isDirectory = true; 588 dir.pathname = FileInfo.SERIES_TAG; 589 dir.filename = mActivity.getString(R.string.folder_name_books_by_series); 590 dir.isListed = true; 591 dir.isScanned = true; 592 return dir; 593 } 594 createBooksByRatingRoot()595 public FileInfo createBooksByRatingRoot() { 596 FileInfo dir = new FileInfo(); 597 dir.isDirectory = true; 598 dir.pathname = FileInfo.RATING_TAG; 599 dir.filename = mActivity.getString(R.string.folder_name_books_by_rating); 600 dir.isListed = true; 601 dir.isScanned = true; 602 return dir; 603 } 604 createBooksByStateToReadRoot()605 public FileInfo createBooksByStateToReadRoot() { 606 FileInfo dir = new FileInfo(); 607 dir.isDirectory = true; 608 dir.pathname = FileInfo.STATE_TO_READ_TAG; 609 dir.filename = mActivity.getString(R.string.folder_name_books_by_state_to_read); 610 dir.isListed = true; 611 dir.isScanned = true; 612 return dir; 613 } 614 createBooksByStateReadingRoot()615 public FileInfo createBooksByStateReadingRoot() { 616 FileInfo dir = new FileInfo(); 617 dir.isDirectory = true; 618 dir.pathname = FileInfo.STATE_READING_TAG; 619 dir.filename = mActivity.getString(R.string.folder_name_books_by_state_reading); 620 dir.isListed = true; 621 dir.isScanned = true; 622 return dir; 623 } 624 createBooksByStateFinishedRoot()625 public FileInfo createBooksByStateFinishedRoot() { 626 FileInfo dir = new FileInfo(); 627 dir.isDirectory = true; 628 dir.pathname = FileInfo.STATE_FINISHED_TAG; 629 dir.filename = mActivity.getString(R.string.folder_name_books_by_state_finished); 630 dir.isListed = true; 631 dir.isScanned = true; 632 return dir; 633 } 634 addSeriesRoot()635 private void addSeriesRoot() { 636 addRoot(createSeriesRoot()); 637 } 638 createTitleRoot()639 public FileInfo createTitleRoot() { 640 FileInfo dir = new FileInfo(); 641 dir.isDirectory = true; 642 dir.pathname = FileInfo.TITLE_TAG; 643 dir.filename = mActivity.getString(R.string.folder_name_books_by_title); 644 dir.isListed = true; 645 dir.isScanned = true; 646 return dir; 647 } 648 addTitleRoot()649 private void addTitleRoot() { 650 addRoot(createTitleRoot()); 651 } 652 653 /** 654 * Lists all directories from root to directory of specified file, returns found directory. 655 * @param file 656 * @param root 657 * @return 658 */ findParentInternal(FileInfo file, FileInfo root)659 private FileInfo findParentInternal(FileInfo file, FileInfo root) { 660 if ( root==null || file==null || root.isRecentDir() ) 661 return null; 662 if (!root.isRootDir() && 663 !(file.getPathName().startsWith(root.getPathName()) || 664 root.isOnSDCard() && file.getPathName().toLowerCase().startsWith( root.getPathName().toLowerCase() ) ) ) 665 return null; 666 // to list all directories starting root dir 667 if ( root.isDirectory && !root.isSpecialDir() ) 668 listDirectory(root); 669 for ( int i=0; i<root.dirCount(); i++ ) { 670 FileInfo found = findParentInternal( file, root.getDir(i)); 671 if ( found!=null ) 672 return found; 673 } 674 for ( int i=0; i<root.fileCount(); i++ ) { 675 if ( root.getFile(i).getPathName().equals(file.getPathName()) || 676 root.isOnSDCard() && root.getFile(i).getPathName().equalsIgnoreCase(file.getPathName()) ) 677 return root; 678 if ( root.getFile(i).getPathName().startsWith(file.getPathName() + "@/") || 679 root.isOnSDCard() && root.getFile(i).getPathName().toLowerCase().startsWith(file.getPathName().toLowerCase() + "@/") ) 680 return root; 681 } 682 return null; 683 } 684 685 public final static int MAX_DIR_LIST_TIME = 500; // 0.5 seconds 686 687 /** 688 * Lists all directories from root to directory of specified file, returns found directory. 689 * @param file 690 * @param root 691 * @return 692 */ findParent(FileInfo file, FileInfo root)693 public FileInfo findParent(FileInfo file, FileInfo root) { 694 FileInfo parent = findParentInternal(file, root); 695 if ( parent==null ) { 696 autoAddRootForFile(new File(file.pathname) ); 697 parent = findParentInternal(file, root); 698 if ( parent==null ) { 699 L.e("Cannot find root directory for file " + file.pathname); 700 return null; 701 } 702 } 703 long maxTs = android.os.SystemClock.uptimeMillis() + MAX_DIR_LIST_TIME; 704 listSubtrees(root, mHideEmptyDirs ? 5 : 1, maxTs); 705 return parent; 706 } 707 findFileInTree(FileInfo f)708 public FileInfo findFileInTree(FileInfo f) { 709 FileInfo parent = findParent(f, getRoot()); 710 if (parent == null) 711 return null; 712 FileInfo item = parent.findItemByPathName(f.getPathName()); 713 return item; 714 } 715 716 /** 717 * List directories in subtree, limited by runtime and depth; remove empty branches (w/o books). 718 * @param root is directory to start with 719 * @param maxDepth is maximum depth 720 * @param limitTs is limit for android.os.SystemClock.uptimeMillis() 721 * @return true if completed, false if stopped by limit. 722 */ listSubtree(FileInfo root, int maxDepth, long limitTs)723 private boolean listSubtree(FileInfo root, int maxDepth, long limitTs) { 724 long ts = android.os.SystemClock.uptimeMillis(); 725 if ( ts>limitTs || maxDepth<=0 ) 726 return false; 727 boolean fullDepthScan = true; 728 listDirectory(root); 729 for ( int i=root.dirCount()-1; i>=-0; i-- ) { 730 boolean res = listSubtree(root.getDir(i), maxDepth-1, limitTs); 731 if ( !res ) { 732 fullDepthScan = false; 733 break; 734 } 735 } 736 if ( fullDepthScan && mHideEmptyDirs ) 737 root.removeEmptyDirs(); 738 return true; 739 } 740 741 /** 742 * List directories in subtree, limited by runtime and depth; remove empty branches (w/o books). 743 * @param root is directory to start with 744 * @param maxDepth is maximum depth 745 * @param limitTs is limit for android.os.SystemClock.uptimeMillis() 746 * @return true if completed, false if stopped by limit. 747 */ listSubtrees(FileInfo root, int maxDepth, long limitTs)748 public boolean listSubtrees(FileInfo root, int maxDepth, long limitTs) { 749 for ( int depth = 1; depth<=maxDepth; depth++ ) { 750 boolean res = listSubtree( root, depth, limitTs ); 751 if ( res ) 752 return true; 753 long ts = android.os.SystemClock.uptimeMillis(); 754 if ( ts>limitTs ) 755 return false; // limited by time 756 // iterate deeper 757 } 758 return false; // limited by depth 759 } 760 setSearchResults( FileInfo[] results )761 public FileInfo setSearchResults( FileInfo[] results ) { 762 FileInfo existingResults = null; 763 for ( int i=0; i<mRoot.dirCount(); i++ ) { 764 FileInfo dir = mRoot.getDir(i); 765 if ( dir.isSearchDir() ) { 766 existingResults = dir; 767 dir.clear(); 768 break; 769 } 770 } 771 if ( existingResults==null ) { 772 FileInfo dir = new FileInfo(); 773 dir.isDirectory = true; 774 dir.pathname = FileInfo.SEARCH_RESULT_DIR_TAG; 775 dir.filename = mActivity.getResources().getString(R.string.dir_search_results); 776 dir.parent = mRoot; 777 dir.isListed = true; 778 dir.isScanned = true; 779 mRoot.addDir(dir); 780 existingResults = dir; 781 } 782 for ( FileInfo item : results ) 783 existingResults.addFile(item); 784 return existingResults; 785 } 786 initRoots(Map<String, String> fsRoots)787 public void initRoots(Map<String, String> fsRoots) { 788 Log.d("cr3", "Scanner.initRoots(" + fsRoots + ")"); 789 mRoot.clear(); 790 // create recent books dir 791 addRoot( FileInfo.RECENT_DIR_TAG, R.string.dir_recent_books, false); 792 793 // create system dirs 794 for (Map.Entry<String, String> entry : fsRoots.entrySet()) 795 addRoot( entry.getKey(), entry.getValue(), true); 796 797 // create OPDS dir 798 addOPDSRoot(); 799 800 // create search dir 801 addSearchRoot(); 802 803 // create books by author root 804 addAuthorsRoot(); 805 // create books by series root 806 addSeriesRoot(); 807 // create books by title root 808 addTitleRoot(); 809 } 810 autoAddRootForFile( File f )811 public boolean autoAddRootForFile( File f ) { 812 File p = f.getParentFile(); 813 while ( p!=null ) { 814 if ( p.getParentFile()==null || p.getParentFile().getParentFile()==null ) 815 break; 816 p = p.getParentFile(); 817 } 818 if ( p!=null ) { 819 L.i("Found possible mount point " + p.getAbsolutePath()); 820 return addRoot(p.getAbsolutePath(), p.getAbsolutePath(), true); 821 } 822 return false; 823 } 824 825 // public boolean scan() 826 // { 827 // L.i("Started scanning"); 828 // long start = System.currentTimeMillis(); 829 // mFileList.clear(); 830 // mFilesForParsing.clear(); 831 // mRoot.clear(); 832 // // create recent books dir 833 // FileInfo recentDir = new FileInfo(); 834 // recentDir.isDirectory = true; 835 // recentDir.pathname = "@recent"; 836 // recentDir.filename = "Recent Books"; 837 // mRoot.addDir(recentDir); 838 // recentDir.parent = mRoot; 839 // // scan directories 840 // lastPercent = -1; 841 // lastProgressUpdate = System.currentTimeMillis() - 500; 842 // boolean res = scanDirectories( mRoot ); 843 // // process found files 844 // lookupDB(); 845 // parseBookProperties(); 846 // updateProgress(9999); 847 // L.i("Finished scanning (" + (System.currentTimeMillis()-start)+ " ms)"); 848 // return res; 849 // } 850 851 getLibraryItems()852 public ArrayList<FileInfo> getLibraryItems() { 853 ArrayList<FileInfo> result = new ArrayList<FileInfo>(); 854 result.add(pathToFileInfo(FileInfo.SEARCH_SHORTCUT_TAG)); 855 result.add(pathToFileInfo(FileInfo.GENRES_TAG)); 856 result.add(pathToFileInfo(FileInfo.AUTHORS_TAG)); 857 result.add(pathToFileInfo(FileInfo.TITLE_TAG)); 858 result.add(pathToFileInfo(FileInfo.SERIES_TAG)); 859 result.add(pathToFileInfo(FileInfo.RATING_TAG)); 860 result.add(pathToFileInfo(FileInfo.STATE_TO_READ_TAG)); 861 result.add(pathToFileInfo(FileInfo.STATE_READING_TAG)); 862 result.add(pathToFileInfo(FileInfo.STATE_FINISHED_TAG)); 863 return result; 864 } 865 getDownloadDirectory()866 public FileInfo getDownloadDirectory() { 867 for ( int i=0; i<mRoot.dirCount(); i++ ) { 868 FileInfo item = mRoot.getDir(i); 869 if (!item.isWritableDirectory()) 870 continue; 871 if ( !item.isSpecialDir() && !item.isArchive ) { 872 if (!item.isListed) 873 listDirectory(item); 874 FileInfo books = item.findItemByPathName(item.pathname + "/Books"); 875 if (books == null) 876 books = item.findItemByPathName(item.pathname + "/books"); 877 if (books != null && books.exists()) 878 return books; 879 File dir = new File(item.getPathName()); 880 if (dir.isDirectory()) { 881 if (!dir.canWrite()) 882 Log.w("cr3", "Directory " + dir + " is readonly"); 883 File f = new File( dir, "Books" ); 884 if ( f.mkdirs() || f.isDirectory() ) { 885 books = new FileInfo(f); 886 books.parent = item; 887 item.addDir(books); 888 books.isScanned = true; 889 books.isListed = true; 890 return books; 891 } 892 } 893 } 894 } 895 File fd = mActivity.getFilesDir(); 896 File downloadDir = new File(fd, "downloads"); 897 if (downloadDir.isDirectory() || downloadDir.mkdirs()) { 898 Log.d("cr3", "download dir: " + downloadDir); 899 FileInfo books = null; 900 books = new FileInfo(downloadDir); 901 //books.parent = item; 902 //item.addDir(books); 903 books.isScanned = true; 904 books.isListed = true; 905 return books; 906 } 907 try { 908 throw new Exception("download directory not found and cannot be created"); 909 } catch (Exception e) { 910 Log.e("cr3", "download directory is not found!!!", e); 911 } 912 return null; 913 } 914 getSharedDownloadDirectory()915 public FileInfo getSharedDownloadDirectory() { 916 for ( int i=0; i<mRoot.dirCount(); i++ ) { 917 FileInfo item = mRoot.getDir(i); 918 if (!item.isWritableDirectory()) 919 continue; 920 if ( !item.isSpecialDir() && !item.isArchive ) { 921 if (!item.isListed) 922 listDirectory(item); 923 FileInfo download = item.findItemByPathName(item.pathname + "/Download"); 924 if (download == null) 925 download = item.findItemByPathName(item.pathname + "/download"); 926 if (download != null && download.exists()) 927 return download; 928 } 929 } 930 Log.e("cr3", "shared download directory is not found!!!"); 931 return null; 932 } 933 isValidFolder(FileInfo info)934 public boolean isValidFolder(FileInfo info){ 935 File dir = new File( info.pathname ); 936 return dir.isDirectory(); 937 } 938 getRoot()939 public FileInfo getRoot() 940 { 941 return mRoot; 942 } 943 getOPDSRoot()944 public FileInfo getOPDSRoot() 945 { 946 for ( int i=0; i<mRoot.dirCount(); i++ ) { 947 if ( mRoot.getDir(i).isOPDSRoot() ) 948 return mRoot.getDir(i); 949 } 950 L.w("OPDS root directory not found!"); 951 return null; 952 } 953 getRecentDir()954 public FileInfo getRecentDir() 955 { 956 for ( int i=0; i<mRoot.dirCount(); i++ ) { 957 if ( mRoot.getDir(i).isRecentDir()) 958 return mRoot.getDir(i); 959 } 960 L.w("Recent books directory not found!"); 961 return null; 962 } 963 Scanner( BaseActivity coolReader, Engine engine )964 public Scanner( BaseActivity coolReader, Engine engine ) 965 { 966 this.engine = engine; 967 this.mActivity = coolReader; 968 mRoot = new FileInfo(); 969 mRoot.path = FileInfo.ROOT_DIR_TAG; 970 mRoot.filename = "File Manager"; 971 mRoot.pathname = FileInfo.ROOT_DIR_TAG; 972 mRoot.isListed = true; 973 mRoot.isScanned = true; 974 mRoot.isDirectory = true; 975 } 976 977 private final Engine engine; 978 private final BaseActivity mActivity; 979 } 980