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