1 package org.coolreader.crengine;
2 
3 import android.util.Log;
4 
5 import org.coolreader.R;
6 import org.coolreader.plugins.OnlineStoreBook;
7 
8 import java.io.File;
9 import java.util.ArrayList;
10 import java.util.Arrays;
11 import java.util.Collection;
12 import java.util.Collections;
13 import java.util.Comparator;
14 import java.util.zip.ZipEntry;
15 
16 public class FileInfo {
17 
18 	public final static String RECENT_DIR_TAG = "@recent";
19 	public final static String SEARCH_RESULT_DIR_TAG = "@searchResults";
20 	public final static String ROOT_DIR_TAG = "@root";
21 	public final static String OPDS_LIST_TAG = "@opds";
22 	public final static String OPDS_DIR_PREFIX = "@opds:";
23 	public final static String ONLINE_CATALOG_PLUGIN_PREFIX = "@plugin:";
24 	public final static String GENRES_TAG = "@genresRoot";
25 	public final static String GENRES_GROUP_PREFIX = "@genresGroup:";
26 	public final static String GENRES_PREFIX = "@genre:";
27 	public final static String AUTHORS_TAG = "@authorsRoot";
28 	public final static String AUTHOR_GROUP_PREFIX = "@authorGroup:";
29 	public final static String AUTHOR_PREFIX = "@author:";
30 	public final static String SERIES_TAG = "@seriesRoot";
31 	public final static String SERIES_GROUP_PREFIX = "@seriesGroup:";
32 	public final static String SERIES_PREFIX = "@series:";
33 	public final static String RATING_TAG = "@ratingRoot";
34 	public final static String STATE_TO_READ_TAG = "@stateToReadRoot";
35 	public final static String STATE_READING_TAG = "@stateReadingRoot";
36 	public final static String STATE_FINISHED_TAG = "@stateFinishedRoot";
37 	public final static String TITLE_TAG = "@titlesRoot";
38 	public final static String TITLE_GROUP_PREFIX = "@titleGroup:";
39 	public final static String SEARCH_SHORTCUT_TAG = "@search";
40 
41 
42 
43 	public Long id; // db id
44 	public String title; // book title
45 	public String authors; // authors, delimited with '|'
46 	public String series; // series name w/o number
47 	public int seriesNumber; // number of book inside series
48 	public String genres; // genre codes, delimited with '|'
49 	public String path; // path to directory where file or archive is located
50 	public String filename; // file name w/o path for normal file, with optional path for file inside archive
51 	public String pathname; // full path+arcname+filename
52 	public String arcname; // archive file name w/o path
53 	public String language; // document language
54 	public String description;	// book description
55 	public String username; // username for online catalogs
56 	public String password; // password for online catalogs
57 	public DocumentFormat format;
58 	public int size; // full file size
59 	public int arcsize; // compressed size
60 	public long createTime;
61 	public long lastAccessTime;
62 	public int flags;
63 	public boolean isArchive;
64 	public boolean isDirectory;
65 	public boolean isListed;
66 	public boolean isScanned;
67 	public long crc32;
68 	public int domVersion;
69 	public int blockRenderingFlags;
70 	public FileInfo parent; // parent item
71 	public Object tag; // some additional information
72 
73 	private ArrayList<FileInfo> files;// files
74 	private ArrayList<FileInfo> dirs; // directories
75 
76 	// 16 lower bits reserved for document flags
77 	public static final int DONT_USE_DOCUMENT_STYLES_FLAG = 1;
78 	public static final int DONT_REFLOW_TXT_FILES_FLAG = 2;
79 	public static final int USE_DOCUMENT_FONTS_FLAG = 4;
80 
81 	// bits 16..19 - reading state (0..15 max)
82 	public static final int READING_STATE_SHIFT = 16;
83 	public static final int READING_STATE_MASK = 0x0F;
84 	public static final int STATE_NEW = 0;
85 	public static final int STATE_TO_READ = 1;
86 	public static final int STATE_READING = 2;
87 	public static final int STATE_FINISHED = 3;
88 
89 	// bits 20..23 - rate (0..15 max, 0..5 currently)
90 	public static final int RATE_SHIFT = 20;
91 	public static final int RATE_MASK = 0x0F;
92 	public static final int RATE_VALUE_NOT_RATED = 0;
93 	public static final int RATE_VALUE_1 = 1;
94 	public static final int RATE_VALUE_2 = 2;
95 	public static final int RATE_VALUE_3 = 3;
96 	public static final int RATE_VALUE_4 = 4;
97 	public static final int RATE_VALUE_5 = 5;
98 
99     //bit 24,25 - info type
100     private static final int TYPE_SHIFT = 24;
101     private static final int TYPE_MASK = 0x03;
102     public static final int TYPE_NOT_SET = 0;
103     public static final int TYPE_FS_ROOT = 1;
104     public static final int TYPE_DOWNLOAD_DIR = 2;
105 
106     // bits 26..29 - profile id (0..15 max)
107 	public static final int PROFILE_ID_SHIFT = 26;
108 	public static final int PROFILE_ID_MASK = 0x0F;
109 
110 	// bitmask for field 'tag' when obtained genres list as special folders
111 	public static final int GENRE_DATA_INCCHILD_MASK = 0x80000000;
112 	public static final int GENRE_DATA_BOOKCOUNT_MASK = 0x00FFFFFF;
113 
114 	/**
115 	 * Get book reading state.
116 	 * @return reading state (one of STATE_XXX constants)
117 	 */
getReadingState()118 	public int getReadingState() {
119         return getBitValue(READING_STATE_SHIFT,READING_STATE_MASK);
120     }
121 
122 	/**
123 	 * Set new reading state.
124 	 * @param state is new reading state (one of STATE_XXX constants)
125 	 */
setReadingState(int state)126 	public boolean setReadingState(int state) {
127         return setBitValue(state, READING_STATE_SHIFT, READING_STATE_MASK);
128 	}
129 
130 	/**
131 	 * Get book reading state.
132 	 * @return reading state (one of STATE_XXX constants)
133 	 */
getRate()134 	public int getRate() {
135         return getBitValue(RATE_SHIFT, RATE_MASK);
136     }
137 
138 	/**
139 	 * Set new rate.
140 	 * @param rate is new rate (one of RATE_XXX constants)
141 	 */
setRate(int rate)142 	public boolean setRate(int rate) {
143         return setBitValue(rate, RATE_SHIFT, RATE_MASK);
144 	}
145 
146     /**
147    	 * Get FileInfo type.
148    	 * @return folder type (one of TYPE_XXX constants)
149    	 */
getType()150    	public int getType() {
151         return getBitValue(TYPE_SHIFT, TYPE_MASK);
152     }
153 
154    	/**
155    	 * Set FileInfo type.
156    	 * @param type is new type
157    	 */
setType(int type)158    	public boolean setType(int type) {
159         return setBitValue(type, TYPE_SHIFT, TYPE_MASK);
160    	}
161 
162 	/**
163 	 * To separate archive name from file name inside archive.
164 	 */
165 	public static final String ARC_SEPARATOR = "@/";
166 
167 
setFlag( int flag, boolean value )168 	public void setFlag( int flag, boolean value ) {
169 		flags = flags & (~flag) | (value? flag : 0);
170 	}
171 
getFlag( int flag )172 	public boolean getFlag( int flag ) {
173 		return (flags & flag)!=0;
174 	}
175 
getProfileId()176 	public int getProfileId() {
177         return getBitValue(PROFILE_ID_SHIFT,PROFILE_ID_MASK);
178     }
179 
setProfileId(int id)180     public void setProfileId(int id) {
181         setBitValue(id,PROFILE_ID_SHIFT,PROFILE_ID_MASK);
182 	}
183 
setBitValue(int value, int shift, int mask)184     private boolean setBitValue(int value, int shift, int mask) {
185         int oldFlags = flags;
186         flags = (flags & ~(mask << shift))
187                 | ((value & mask) << shift);
188         return flags != oldFlags;
189     }
190 
getBitValue(int shift, int mask)191     private int getBitValue(int shift, int mask) {
192         return (flags >> shift) & mask;
193     }
194 
getTitleOrFileName()195     public String getTitleOrFileName() {
196 		if (title != null && title.length() > 0)
197 			return title;
198 		if (authors != null && authors.length() > 0)
199 			return "";
200 		if (series != null && series.length() > 0)
201 			return "";
202 		return filename;
203 	}
204 
205 	/**
206 	 * Split archive + file path name by ARC_SEPARATOR
207 	 * @param pathName is pathname like /arc_file_path@/filepath_inside_arc or /file_path
208 	 * @return item[0] is pathname, item[1] is archive name (null if no archive)
209 	 */
splitArcName( String pathName )210 	public static String[] splitArcName( String pathName )
211 	{
212 		String[] res = new String[2];
213 		int arcSeparatorPos = pathName.indexOf(ARC_SEPARATOR);
214 		if ( arcSeparatorPos>=0 ) {
215 			// from archive
216 			res[1] = pathName.substring(0, arcSeparatorPos);
217 			res[0] = pathName.substring(arcSeparatorPos + ARC_SEPARATOR.length());
218 		} else {
219 			res[0] = pathName;
220 		}
221 		return res;
222 	}
223 
FileInfo( String pathName )224 	public FileInfo( String pathName )
225 	{
226 		String[] parts = splitArcName( pathName );
227 		if ( parts[1]!=null ) {
228 			// from archive
229 			isArchive = true;
230 			arcname = parts[1];
231 			pathname = parts[0];
232 			File f = new File(pathname);
233 			filename = f.getName();
234 			path = f.getPath();
235 			File arc = new File(arcname);
236 			if (arc.isFile() && arc.exists()) {
237 				arcsize = (int)arc.length();
238 				isArchive = true;
239 				try {
240 					//ZipFile zip = new ZipFile(new File(arcname));
241 					ArrayList<ZipEntry> entries = Services.getEngine().getArchiveItems(arcname);
242 					//for ( Enumeration<?> e = zip.entries(); e.hasMoreElements(); ) {
243 					for (ZipEntry entry : entries) {
244 						String name = entry.getName();
245 
246 						if ( !entry.isDirectory() && pathname.equals(name) ) {
247 							File itemf = new File(name);
248 							filename = itemf.getName();
249 							path = itemf.getPath();
250 							format = DocumentFormat.byExtension(name);
251 							size = (int)entry.getSize();
252 							arcsize = (int)entry.getCompressedSize();
253 							createTime = entry.getTime();
254 							domVersion = Engine.DOM_VERSION_CURRENT;
255 							blockRenderingFlags = Engine.BLOCK_RENDERING_FLAGS_WEB;
256 							break;
257 						}
258 					}
259 				} catch ( Exception e ) {
260 					Log.e("cr3", "error while reading contents of " + arcname);
261 				}
262 			}
263 		} else {
264 			fromFile(new File(pathName));
265 		}
266 	}
267 
getFileNameToDisplay()268 	public String getFileNameToDisplay() {
269 		boolean isSingleFileArchive = (isArchive && parent!=null && !parent.isArchive && arcname!=null);
270 		return isSingleFileArchive
271 			? new File(arcname).getName() : filename;
272 	}
273 
fromFile( File f )274 	private void fromFile( File f )
275 	{
276 		if ( !f.isDirectory() ) {
277 			DocumentFormat fmt = DocumentFormat.byExtension(f.getName());
278 			filename = f.getName();
279 			path = f.getParent();
280 			pathname = f.getAbsolutePath();
281 			format = fmt;
282 			createTime = f.lastModified();
283 			size = (int)f.length();
284 			domVersion = Engine.DOM_VERSION_CURRENT;
285 			blockRenderingFlags = Engine.BLOCK_RENDERING_FLAGS_WEB;
286 		} else {
287 			filename = f.getName();
288 			path = f.getParent();
289 			pathname = f.getAbsolutePath();
290 			isDirectory = true;
291 		}
292 		File parent_ = f.getParentFile();
293 		if (null != parent_)
294 			parent = new FileInfo(parent_);
295 	}
296 
FileInfo( File f )297 	public FileInfo( File f )
298 	{
299 		fromFile(f);
300 	}
301 
FileInfo()302 	public FileInfo()
303 	{
304 		domVersion = Engine.DOM_VERSION_CURRENT;
305 		blockRenderingFlags = Engine.BLOCK_RENDERING_FLAGS_WEB;
306 	}
307 
308 	/// doesn't copy parent and children
FileInfo(FileInfo v)309 	public FileInfo(FileInfo v)
310 	{
311 		assign(v);
312 	}
313 
assign(FileInfo v)314 	public void assign(FileInfo v)
315 	{
316 		title = v.title;
317 		authors = v.authors;
318 		series = v.series;
319 		seriesNumber = v.seriesNumber;
320 		path = v.path;
321 		filename = v.filename;
322 		pathname = v.pathname;
323 		arcname = v.arcname;
324 		format = v.format;
325 		flags = v.flags;
326 		size = v.size;
327 		arcsize = v.arcsize;
328 		isArchive = v.isArchive;
329 		isDirectory = v.isDirectory;
330 		createTime = v.createTime;
331 		lastAccessTime = v.lastAccessTime;
332 		language = v.language;
333 		genres = v.genres;
334 		description = v.description;
335 		username = v.username;
336 		password = v.password;
337 		crc32 = v.crc32;
338 		domVersion = v.domVersion;
339 		blockRenderingFlags = v.blockRenderingFlags;
340 		id = v.id;
341 	}
342 
343 	/**
344 	 * @return archive file path and name, null if this object is neither archive nor a file inside archive
345 	 */
getArchiveName()346 	public String getArchiveName()
347 	{
348 		return arcname;
349 	}
350 
351 	/**
352 	 * @return file name inside archive, null if this object is not a file inside archive
353 	 */
getArchiveItemName()354 	public String getArchiveItemName()
355 	{
356 		if ( isArchive && !isDirectory && pathname!=null )
357 			return pathname;
358 		return null;
359 	}
360 
isRecentDir()361 	public boolean isRecentDir()
362 	{
363 		return RECENT_DIR_TAG.equals(pathname);
364 	}
365 
isSearchDir()366 	public boolean isSearchDir()
367 	{
368 		return SEARCH_RESULT_DIR_TAG.equals(pathname);
369 	}
370 
isRootDir()371 	public boolean isRootDir()
372 	{
373 		return ROOT_DIR_TAG.equals(pathname);
374 	}
375 
isSpecialDir()376 	public boolean isSpecialDir()
377 	{
378 		return pathname!=null && pathname.startsWith("@");
379 	}
380 
isOnlineCatalogPluginDir()381 	public boolean isOnlineCatalogPluginDir()
382 	{
383 		return pathname!=null && pathname.startsWith(ONLINE_CATALOG_PLUGIN_PREFIX);
384 	}
385 
isOnlineCatalogPluginBook()386 	public boolean isOnlineCatalogPluginBook()
387 	{
388 		return !isDirectory && pathname != null && pathname.startsWith(ONLINE_CATALOG_PLUGIN_PREFIX) && getOnlineStoreBookInfo() != null;
389 	}
390 
isOPDSDir()391 	public boolean isOPDSDir()
392 	{
393 		return pathname!=null && pathname.startsWith(OPDS_DIR_PREFIX) && (getOPDSEntryInfo() == null || getOPDSEntryInfo().getBestAcquisitionLink() == null);
394 	}
395 
isOPDSBook()396 	public boolean isOPDSBook()
397 	{
398 		return pathname!=null && pathname.startsWith(OPDS_DIR_PREFIX) && getOPDSEntryInfo() != null && getOPDSEntryInfo().getBestAcquisitionLink() != null;
399 	}
400 
getOPDSEntryInfo()401 	private OPDSUtil.EntryInfo getOPDSEntryInfo() {
402 		if (tag !=null && tag instanceof OPDSUtil.EntryInfo)
403 			return (OPDSUtil.EntryInfo)tag;
404 		return null;
405 	}
406 
getOnlineStoreBookInfo()407 	public OnlineStoreBook getOnlineStoreBookInfo() {
408 		if (tag !=null && tag instanceof OnlineStoreBook)
409 			return (OnlineStoreBook)tag;
410 		return null;
411 	}
412 
isOPDSRoot()413 	public boolean isOPDSRoot()
414 	{
415 		return OPDS_LIST_TAG.equals(pathname);
416 	}
417 
isSearchShortcut()418 	public boolean isSearchShortcut()
419 	{
420 		return SEARCH_SHORTCUT_TAG.equals(pathname);
421 	}
422 
isBooksByGenreRoot()423 	public boolean isBooksByGenreRoot()
424 	{
425 		return GENRES_TAG.equals(pathname);
426 	}
427 
isBooksByAuthorRoot()428 	public boolean isBooksByAuthorRoot()
429 	{
430 		return AUTHORS_TAG.equals(pathname);
431 	}
432 
isBooksBySeriesRoot()433 	public boolean isBooksBySeriesRoot()
434 	{
435 		return SERIES_TAG.equals(pathname);
436 	}
437 
isBooksByRatingRoot()438 	public boolean isBooksByRatingRoot()
439 	{
440 		return RATING_TAG.equals(pathname);
441 	}
442 
isBooksByStateToReadRoot()443 	public boolean isBooksByStateToReadRoot()
444 	{
445 		return STATE_TO_READ_TAG.equals(pathname);
446 	}
447 
isBooksByStateReadingRoot()448 	public boolean isBooksByStateReadingRoot()
449 	{
450 		return STATE_READING_TAG.equals(pathname);
451 	}
452 
isBooksByStateFinishedRoot()453 	public boolean isBooksByStateFinishedRoot()
454 	{
455 		return STATE_FINISHED_TAG.equals(pathname);
456 	}
457 
isBooksByTitleRoot()458 	public boolean isBooksByTitleRoot()
459 	{
460 		return TITLE_TAG.equals(pathname);
461 	}
462 
isBooksByGenreDir()463 	public boolean isBooksByGenreDir()
464 	{
465 		return pathname!=null && pathname.startsWith(GENRES_PREFIX);
466 	}
467 
isBooksByAuthorDir()468 	public boolean isBooksByAuthorDir()
469 	{
470 		return pathname!=null && pathname.startsWith(AUTHOR_PREFIX);
471 	}
472 
isBooksBySeriesDir()473 	public boolean isBooksBySeriesDir()
474 	{
475 		return pathname!=null && pathname.startsWith(SERIES_PREFIX);
476 	}
477 
isOnSDCard()478 	public boolean isOnSDCard() {
479 		if (null == parent)
480 			return false;
481 		if ( ( ( "SD".equals(filename) && "SD".equals(title)) ||
482 				("EXT SD".equals(filename) && "EXT SD".equals(title)) ) &&
483 				isDirectory && !isArchive && 0 == size && 0 == arcsize &&
484 				ROOT_DIR_TAG.equals(parent.pathname) )
485 			return true;
486 		return parent.isOnSDCard();
487 	}
488 
getGenreCode()489 	public String getGenreCode() {
490 		if (pathname.startsWith(GENRES_PREFIX)) {
491 			return pathname.substring(GENRES_PREFIX.length());
492 		}
493 		return "";
494 	}
495 
getAuthorId()496 	public long getAuthorId()
497 	{
498 		if (!isBooksByAuthorDir())
499 			return 0;
500 		return id;
501 	}
502 
getSeriesId()503 	public long getSeriesId()
504 	{
505 		if (!isBooksBySeriesDir())
506 			return 0;
507 		return id;
508 	}
509 
isHidden()510 	public boolean isHidden()
511 	{
512 		return pathname.startsWith(".");
513 	}
514 
getOPDSUrl()515 	public String getOPDSUrl()
516 	{
517 		if ( !pathname.startsWith(OPDS_DIR_PREFIX) )
518 			return null;
519 		return pathname.substring(OPDS_DIR_PREFIX.length());
520 	}
521 
getOnlineCatalogPluginPackage()522 	public String getOnlineCatalogPluginPackage()
523 	{
524 		if ( !pathname.startsWith(ONLINE_CATALOG_PLUGIN_PREFIX) )
525 			return null;
526 		String s = pathname.substring(ONLINE_CATALOG_PLUGIN_PREFIX.length());
527 		int p = s.indexOf(":");
528 		if (p < 0)
529 			return s;
530 		else
531 			return s.substring(0, p);
532 	}
533 
getOnlineCatalogPluginPath()534 	public String getOnlineCatalogPluginPath()
535 	{
536 		if ( !pathname.startsWith(ONLINE_CATALOG_PLUGIN_PREFIX) )
537 			return null;
538 		String s = pathname.substring(ONLINE_CATALOG_PLUGIN_PREFIX.length());
539 		int p = s.indexOf(":");
540 		if (p < 0)
541 			return null;
542 		else
543 			return s.substring(p + 1);
544 	}
545 
getOnlineCatalogPluginId()546 	public String getOnlineCatalogPluginId()
547 	{
548 		String s = getOnlineCatalogPluginPath();
549 		if (s == null)
550 			return null;
551 		int p = s.indexOf("=");
552 		if (p < 0)
553 			return null;
554 		else
555 			return s.substring(p + 1);
556 	}
557 
558 	/**
559 	 * Get absolute path to file.
560 	 * For plain files, returns /abs_path_to_file/filename.ext
561 	 * For archives, returns /abs_path_to_archive/arc_file_name.zip@/filename_inside_archive.ext
562 	 * @return full path + filename
563 	 */
getPathName()564 	public String getPathName()
565 	{
566 		if ( arcname!=null )
567 			return arcname + ARC_SEPARATOR + pathname;
568 		return pathname;
569 	}
570 
getBasePath()571 	public String getBasePath()
572 	{
573 		if ( arcname!=null )
574 			return arcname;
575 		return pathname;
576 	}
577 
dirCount()578 	public int dirCount()
579 	{
580 		return dirs!=null ? dirs.size() : 0;
581 	}
582 
fileCount()583 	public int fileCount()
584 	{
585 		return files!=null ? files.size() : 0;
586 	}
587 
itemCount()588 	public int itemCount()
589 	{
590 		return dirCount() + fileCount();
591 	}
592 
addDir( FileInfo dir )593 	public void addDir( FileInfo dir )
594 	{
595 		if ( dirs==null )
596 			dirs = new ArrayList<FileInfo>();
597 		dirs.add(dir);
598 		if (dir.parent == null)
599 			dir.parent = this;
600 	}
addFile( FileInfo file )601 	public void addFile( FileInfo file )
602 	{
603 		if ( files==null )
604 			files = new ArrayList<FileInfo>();
605 		files.add(file);
606 	}
addItems( Collection<FileInfo> items )607 	public void addItems( Collection<FileInfo> items )
608 	{
609 		for ( FileInfo item : items ) {
610 			if ( item.isDirectory )
611 				addDir(item);
612 			else
613 				addFile(item);
614 			item.parent = this;
615 		}
616 	}
replaceItems( Collection<FileInfo> items )617 	public void replaceItems( Collection<FileInfo> items )
618 	{
619 		files = null;
620 		dirs = null;
621 		addItems( items );
622 	}
isEmpty()623 	public boolean isEmpty()
624 	{
625 		return fileCount()==0 && dirCount()==0;
626 	}
getItem( int index )627 	public FileInfo getItem( int index )
628 	{
629 		if ( index<0 )
630 			throw new IndexOutOfBoundsException();
631 		if ( index<dirCount())
632 			return dirs.get(index);
633 		index -= dirCount();
634 		if ( index<fileCount())
635 			return files.get(index);
636 		Log.e("cr3", "Index out of bounds " + index + " at FileInfo.getItem() : returning 0");
637 		//throw new IndexOutOfBoundsException();
638 		return null;
639 	}
findItemByPathName( String pathName )640 	public FileInfo findItemByPathName( String pathName )
641 	{
642 		if ( dirs!=null )
643 			for ( FileInfo dir : dirs )
644 				if ( isOnSDCard() && pathName.compareToIgnoreCase(dir.getPathName()) == 0 || pathName.equals(dir.getPathName()) )
645 					return dir;
646 		if ( files!=null )
647 			for ( FileInfo file : files ) {
648 				if ( isOnSDCard() && pathName.compareToIgnoreCase(file.getPathName()) == 0 || pathName.equals(file.getPathName()) )
649 					return file;
650 				if ( isOnSDCard() && file.getPathName().toLowerCase().startsWith(pathName.toLowerCase()+"@/") || file.getPathName().startsWith(pathName+"@/" ))
651 					return file;
652 			}
653 		return null;
654 	}
655 
eq(String s1, String s2)656 	public static boolean eq(String s1, String s2) {
657 		if (s1 == null)
658 			return s2 == null;
659 		return s1.equals(s2);
660 	}
661 
pathNameEquals(FileInfo item)662 	public boolean pathNameEquals(FileInfo item) {
663 		return isDirectory == item.isDirectory && eq(arcname, item.arcname) && eq(pathname, item.pathname);
664 	}
665 
hasItem(FileInfo item)666 	public boolean hasItem(FileInfo item) {
667 		return getItemIndex(item) >= 0;
668 	}
669 
getItemIndex( FileInfo item )670 	public int getItemIndex( FileInfo item )
671 	{
672 		if ( item==null )
673 			return -1;
674 		for ( int i=0; i<dirCount(); i++ ) {
675 			if ( item.pathNameEquals(getDir(i)) )
676 				return i;
677 		}
678 		for ( int i=0; i<fileCount(); i++ ) {
679 			if (item.pathNameEquals(getFile(i)))
680 				return i + dirCount();
681 		}
682 		return -1;
683 	}
684 
getFileIndex( FileInfo item )685 	public int getFileIndex( FileInfo item )
686 	{
687 		if ( item==null )
688 			return -1;
689 		for ( int i=0; i<fileCount(); i++ ) {
690 			if (item.pathNameEquals(getFile(i)))
691 				return i;
692 		}
693 		return -1;
694 	}
695 
getDir( int index )696 	public FileInfo getDir( int index )
697 	{
698 		if ( index<0 )
699 			throw new IndexOutOfBoundsException();
700 		if ( index<dirCount())
701 			return dirs.get(index);
702 		throw new IndexOutOfBoundsException();
703 	}
704 
getFile( int index )705 	public FileInfo getFile( int index )
706 	{
707 		if ( index<0 )
708 			throw new IndexOutOfBoundsException();
709 		if ( index<fileCount())
710 			return files.get(index);
711 		throw new IndexOutOfBoundsException();
712 	}
713 
setFileProperties(FileInfo file)714 	public boolean setFileProperties(FileInfo file)
715 	{
716 		boolean modified = false;
717 		modified = setTitle(file.getTitle()) || modified;
718 		modified = setAuthors(file.getAuthors()) || modified;
719 		modified = setSeriesName(file.getSeriesName()) || modified;
720 		modified = setSeriesNumber(file.getSeriesNumber()) || modified;
721 		modified = setReadingState(file.getReadingState()) || modified;
722 		modified = setRate(file.getRate()) || modified;
723 		return modified;
724 	}
725 
setFile(int index, FileInfo file)726     public void setFile(int index, FileInfo file)
727     {
728         if ( index<0 )
729 			throw new IndexOutOfBoundsException();
730 		if (index < fileCount()) {
731 			files.set(index, file);
732 			file.parent = this;
733 			return;
734 		}
735 		throw new IndexOutOfBoundsException();
736     }
737 
setFile(FileInfo file)738 	public void setFile(FileInfo file)
739 	{
740 		int index = getFileIndex(file);
741 		if ( index<0 )
742 			return;
743 		setFile(index, file);
744 	}
745 
setItems(FileInfo copyFrom)746 	public void setItems(FileInfo copyFrom)
747 	{
748 		clear();
749 		for (int i=0; i<copyFrom.fileCount(); i++) {
750 			addFile(copyFrom.getFile(i));
751 			copyFrom.getFile(i).parent = this;
752 		}
753 		for (int i=0; i<copyFrom.dirCount(); i++) {
754 			addDir(copyFrom.getDir(i));
755 			copyFrom.getDir(i).parent = this;
756 		}
757 	}
758 
setItems(Collection<FileInfo> list)759 	public void setItems(Collection<FileInfo> list)
760 	{
761 		clear();
762 		if (list == null)
763 			return;
764 		for (FileInfo item : list) {
765 			if (item.isDirectory)
766 				addDir(item);
767 			else
768 				addFile(item);
769 			item.parent = this;
770 		}
771 	}
772 
removeEmptyDirs()773 	public void removeEmptyDirs()
774 	{
775 		if ( parent==null || pathname.startsWith("@") || !isListed || dirs==null )
776 			return;
777 		for ( int i=dirCount()-1; i>=0; i-- ) {
778 			FileInfo dir = getDir(i);
779 			if ( dir.isListed && dir.dirCount() == 0 && dir.fileCount() == 0)
780 				dirs.remove(i);
781 		}
782 	}
783 
removeChild( FileInfo item )784 	public void removeChild( FileInfo item )
785 	{
786 		if ( item.isSpecialDir() )
787 			return;
788 		if ( files!=null ) {
789 			int n = files.indexOf(item);
790 			if ( n>=0 && n<files.size() ) {
791 				files.remove(n);
792 				return;
793 			}
794 		}
795 		if ( dirs!=null ) {
796 			int n = dirs.indexOf(item);
797 			if ( n>=0 && n<dirs.size() ) {
798 				dirs.remove(n);
799 			}
800 		}
801 	}
802 
deleteFile()803 	public boolean deleteFile()
804 	{
805 		if ( isArchive ) {
806 			if ( isDirectory )
807 				return false;
808 			File f = new File(arcname);
809 			if ( f.exists() && !f.isDirectory() ) {
810 				if ( !f.delete() )
811 					return false;
812 				if ( parent!=null ) {
813 					if ( parent.isArchive ) {
814 						// remove all files belonging to this archive
815 					} else {
816 						parent.removeChild(this);
817 					}
818 				}
819 				return true;
820 			}
821 		}
822 		if ( isDirectory )
823 			return false;
824 		if ( !fileExists() )
825 			return false;
826 		File f = new File(pathname);
827 		if ( f.delete() ) {
828 			if ( parent!=null ) {
829 				parent.removeChild(this);
830 			}
831 			return true;
832 		}
833 		return false;
834 	}
835 
fileExists()836 	public boolean fileExists()
837 	{
838 		if (isDirectory)
839 			return false;
840 		if ( isArchive ) {
841 			if ( arcname!=null )
842 				return new File(arcname).exists();
843 			return false;
844 		}
845 		return new File(pathname).exists();
846 	}
847 
848 	/**
849 	 * @return true if item (file, directory, or archive) exists
850 	 */
exists()851 	public boolean exists()
852 	{
853 		if ( isArchive ) {
854 			if ( arcname==null )
855 				return false;
856 			File f = new File(arcname);
857 			return f.exists();
858 		}
859 		File f = new File(pathname);
860 		return f.exists();
861 	}
862 
863 	/**
864 	 * @return true if item is a directory, which exists and can be written to
865 	 */
isWritableDirectory()866 	public boolean isWritableDirectory()
867 	{
868 		if (!isDirectory || isArchive || isSpecialDir())
869 			return false;
870 		File f = new File(pathname);
871 		boolean isDir = f.isDirectory();
872 		boolean canWr = f.canWrite();
873 //		if (!canWr) {
874 //			File testFile = new File(f, "cr3test.tmp");
875 //			try {
876 //				OutputStream os = new FileOutputStream(testFile, false);
877 //				os.close();
878 //				testFile.delete();
879 //				canWr = true;
880 //			} catch (FileNotFoundException e) {
881 //				L.e("cannot write " + testFile, e);
882 //			} catch (IOException e) {
883 //				L.e("cannot write " + testFile, e);
884 //			}
885 //		}
886 		return isDir && canWr;
887 	}
888 
889 	/**
890 	 * @return true if item is a directory, which exists and can be written to
891 	 */
isReadableDirectory()892 	public boolean isReadableDirectory()
893 	{
894 		if (!isDirectory || isArchive || isSpecialDir())
895 			return false;
896 		File f = new File(pathname);
897 		boolean isDir = f.isDirectory();
898 		boolean canRd = f.canRead();
899 		return isDir && canRd;
900 	}
901 
getAuthors()902 	public String getAuthors() {
903 		return authors;
904 	}
905 
setAuthors(String authors)906 	public boolean setAuthors(String authors) {
907 		if (eq(this.authors, authors))
908 			return false;
909 		this.authors = authors;
910 		return true;
911 	}
912 
getTitle()913 	public String getTitle() {
914 		return title;
915 	}
916 
setTitle(String title)917 	public boolean setTitle(String title) {
918 		if (eq(this.title, title))
919 			return false;
920 		this.title = title;
921 		return true;
922 	}
923 
getSeriesName()924 	public String getSeriesName() {
925 		return series;
926 	}
927 
setSeriesName(String series)928 	public boolean setSeriesName(String series) {
929 		if (eq(this.series, series))
930 			return false;
931 		this.series = series;
932 		return true;
933 	}
934 
setSeriesNumber(int seriesNumber)935 	public boolean setSeriesNumber(int seriesNumber) {
936 		if (this.seriesNumber == seriesNumber)
937 			return false;
938 		this.seriesNumber = seriesNumber;
939 		return true;
940 	}
941 
getSeriesNumber()942 	public int getSeriesNumber() {
943 		return series != null && series.length() > 0 ? seriesNumber : 0;
944 	}
945 
getLanguage()946 	public String getLanguage() {
947 		return language;
948 	}
949 
clear()950 	public void clear()
951 	{
952 		dirs = null;
953 		files = null;
954 	}
955 
956 	public static enum SortOrder {
957 		FILENAME(R.string.mi_book_sort_order_filename, new Comparator<FileInfo>() {
958 			public int compare( FileInfo f1, FileInfo f2 )
959 			{
960 				if ( f1==null || f2==null )
961 					return 0;
962 				return Utils.cmp(f1.getFileNameToDisplay(), f2.getFileNameToDisplay());
963 			}
964 		}),
965 		FILENAME_DESC(R.string.mi_book_sort_order_filename_desc, new Comparator<FileInfo>() {
966 			public int compare( FileInfo f1, FileInfo f2 )
967 			{
968 				if ( f1==null || f2==null )
969 					return 0;
970 				return Utils.cmp(f2.getFileNameToDisplay(), f1.getFileNameToDisplay());
971 			}
972 		}),
973 		TIMESTAMP(R.string.mi_book_sort_order_timestamp, new Comparator<FileInfo>() {
974 			public int compare( FileInfo f1, FileInfo f2 )
975 			{
976 				if ( f1==null || f2==null )
977 					return 0;
978 				return firstNz( cmp(f1.createTime, f2.createTime), Utils.cmp(f1.filename, f2.filename) );
979 			}
980 		}),
981 		TIMESTAMP_DESC(R.string.mi_book_sort_order_timestamp_desc, new Comparator<FileInfo>() {
982 			public int compare( FileInfo f1, FileInfo f2 )
983 			{
984 				if ( f1==null || f2==null )
985 					return 0;
986 				return firstNz( cmp(f2.createTime, f1.createTime), Utils.cmp(f2.filename, f1.filename) );
987 			}
988 		}),
989 		AUTHOR_TITLE(R.string.mi_book_sort_order_author, new Comparator<FileInfo>() {
990 			public int compare( FileInfo f1, FileInfo f2 )
991 			{
992 				if ( f1==null || f2==null )
993 					return 0;
994 				return firstNz(
995 						cmpNotNullFirst(Utils.formatAuthors(f1.authors), Utils.formatAuthors(f2.authors))
996 						,cmpNotNullFirst(f1.series, f2.series)
997 						,cmp(f1.getSeriesNumber(), f2.getSeriesNumber())
998 						,cmpNotNullFirst(f1.title, f2.title)
999 						,Utils.cmp(f1.filename, f2.filename)
1000 						);
1001 			}
1002 		}),
1003 		AUTHOR_TITLE_DESC(R.string.mi_book_sort_order_author_desc, new Comparator<FileInfo>() {
1004 			public int compare( FileInfo f1, FileInfo f2 )
1005 			{
1006 				if ( f1==null || f2==null )
1007 					return 0;
1008 				return firstNz(
1009 						cmpNotNullFirst(Utils.formatAuthors(f2.authors), Utils.formatAuthors(f1.authors))
1010 						,cmpNotNullFirst(f2.series, f1.series)
1011 						,cmp(f2.getSeriesNumber(), f1.getSeriesNumber())
1012 						,cmpNotNullFirst(f2.title, f1.title)
1013 						,Utils.cmp(f2.filename, f1.filename)
1014 				);
1015 			}
1016 		}),
1017 		TITLE_AUTHOR(R.string.mi_book_sort_order_title, new Comparator<FileInfo>() {
1018 			public int compare( FileInfo f1, FileInfo f2 )
1019 			{
1020 				if ( f1==null || f2==null )
1021 					return 0;
1022 				return firstNz(
1023 						cmpNotNullFirst(f1.series, f2.series)
1024 						,cmp(f1.getSeriesNumber(), f2.getSeriesNumber())
1025 						,cmpNotNullFirst(f1.title, f2.title)
1026 						,cmpNotNullFirst(Utils.formatAuthors(f1.authors), Utils.formatAuthors(f2.authors))
1027 						,Utils.cmp(f1.filename, f2.filename)
1028 						);
1029 			}
1030 		}),
1031 		TITLE_AUTHOR_DESC(R.string.mi_book_sort_order_title_desc, new Comparator<FileInfo>() {
1032 			public int compare( FileInfo f1, FileInfo f2 )
1033 			{
1034 				if ( f1==null || f2==null )
1035 					return 0;
1036 				return firstNz(
1037 						cmpNotNullFirst(f2.series, f1.series)
1038 						,cmp(f2.getSeriesNumber(), f1.getSeriesNumber())
1039 						,cmpNotNullFirst(f2.title, f1.title)
1040 						,cmpNotNullFirst(Utils.formatAuthors(f2.authors), Utils.formatAuthors(f1.authors))
1041 						,Utils.cmp(f2.filename, f1.filename)
1042 				);
1043 			}
1044 		});
1045 		//================================================
1046 		private final Comparator<FileInfo> comparator;
1047 		public final int resourceId;
SortOrder( int resourceId, Comparator<FileInfo> comparator )1048 		private SortOrder( int resourceId, Comparator<FileInfo> comparator )
1049 		{
1050 			this.resourceId = resourceId;
1051 			this.comparator = comparator;
1052 		}
1053 
getComparator()1054 		public final Comparator<FileInfo> getComparator()
1055 		{
1056 			return comparator;
1057 		}
1058 
1059 		/**
1060 		 * Same as cmp, but not-null comes first
1061 		 * @param str1
1062 		 * @param str2
1063 		 * @return
1064 		 */
cmpNotNullFirst( String str1, String str2 )1065 		private static int cmpNotNullFirst( String str1, String str2 )
1066 		{
1067 			if ( str1==null && str2==null )
1068 				return 0;
1069 			if ( str1==null )
1070 				return 1;
1071 			if ( str2==null )
1072 				return -1;
1073 			return Utils.cmp(str1, str2);
1074 		}
1075 
cmp( long n1, long n2 )1076 		static int cmp( long n1, long n2 )
1077 		{
1078 			if ( n1<n2 )
1079 				return -1;
1080 			if ( n1>n2 )
1081 				return 1;
1082 			return 0;
1083 		}
1084 
firstNz( int... v)1085 		private static int firstNz( int... v)
1086 		{
1087 			for ( int i=0; i<v.length; i++ ) {
1088 				if ( v[i]!=0 )
1089 					return v[i];
1090 			}
1091 			return 0;
1092 		}
fromName( String name )1093 		public static SortOrder fromName( String name ) {
1094 			if ( name!=null )
1095 				for ( SortOrder order : values() )
1096 					if ( order.name().equals(name) )
1097 						return order;
1098 			return DEF_SORT_ORDER;
1099 		}
1100 	}
1101 	public final static SortOrder DEF_SORT_ORDER = SortOrder.AUTHOR_TITLE;
1102 
sort( SortOrder SortOrder )1103 	public void sort( SortOrder SortOrder )
1104 	{
1105 		if ( dirs!=null ) {
1106 			ArrayList<FileInfo> newDirs = new ArrayList<FileInfo>(dirs);
1107 			Collections.sort( newDirs, SortOrder.getComparator() );
1108 			dirs = newDirs;
1109 		}
1110 		if ( files!=null ) {
1111 			ArrayList<FileInfo> newFiles = new ArrayList<FileInfo>(files);
1112 			Collections.sort( newFiles, SortOrder.getComparator() );
1113 			files = newFiles;
1114 		}
1115 	}
1116 
1117 
1118 
1119 	@Override
hashCode()1120 	public int hashCode() {
1121 		final int prime = 31;
1122 		int result = 1;
1123 		result = prime * result + ((arcname == null) ? 0 : arcname.hashCode());
1124 		result = prime * result + arcsize;
1125 		result = prime * result + ((authors == null) ? 0 : authors.hashCode());
1126 		result = prime * result + (int) (createTime ^ (createTime >>> 32));
1127 		result = prime * result + ((dirs == null) ? 0 : dirs.hashCode());
1128 		result = prime * result
1129 				+ ((filename == null) ? 0 : filename.hashCode());
1130 		result = prime * result + ((files == null) ? 0 : files.hashCode());
1131 		result = prime * result + flags;
1132 		result = prime * result + ((format == null) ? 0 : format.hashCode());
1133 		result = prime * result + (isArchive ? 1231 : 1237);
1134 		result = prime * result + (isDirectory ? 1231 : 1237);
1135 		result = prime * result + (isListed ? 1231 : 1237);
1136 		result = prime * result + (isScanned ? 1231 : 1237);
1137 		result = prime * result
1138 				+ ((language == null) ? 0 : language.hashCode());
1139 		result = prime * result
1140 				+ (int) (lastAccessTime ^ (lastAccessTime >>> 32));
1141 		result = prime * result + ((parent == null) ? 0 : parent.hashCode());
1142 		result = prime * result + ((path == null) ? 0 : path.hashCode());
1143 		result = prime * result
1144 				+ ((pathname == null) ? 0 : pathname.hashCode());
1145 		result = prime * result + ((series == null) ? 0 : series.hashCode());
1146 		result = prime * result + seriesNumber;
1147 		result = prime * result + size;
1148 		result = prime * result + ((title == null) ? 0 : title.hashCode());
1149 		return result;
1150 	}
1151 
1152 	@Override
equals(Object obj)1153 	public boolean equals(Object obj) {
1154 		if (this == obj)
1155 			return true;
1156 		if (obj == null)
1157 			return false;
1158 		if (getClass() != obj.getClass())
1159 			return false;
1160 		FileInfo other = (FileInfo) obj;
1161 		if (arcname == null) {
1162 			if (other.arcname != null)
1163 				return false;
1164 		} else if (!arcname.equals(other.arcname))
1165 			return false;
1166 		if (arcsize != other.arcsize)
1167 			return false;
1168 		if (authors == null) {
1169 			if (other.authors != null)
1170 				return false;
1171 		} else if (!authors.equals(other.authors))
1172 			return false;
1173 		if (createTime != other.createTime)
1174 			return false;
1175 		if (dirs == null) {
1176 			if (other.dirs != null)
1177 				return false;
1178 		} else if (!dirs.equals(other.dirs))
1179 			return false;
1180 		if (filename == null) {
1181 			if (other.filename != null)
1182 				return false;
1183 		} else if (!filename.equals(other.filename))
1184 			return false;
1185 		if (files == null) {
1186 			if (other.files != null)
1187 				return false;
1188 		} else if (!files.equals(other.files))
1189 			return false;
1190 		if (flags != other.flags)
1191 			return false;
1192 		if (format != other.format)
1193 			return false;
1194 		if (isArchive != other.isArchive)
1195 			return false;
1196 		if (isDirectory != other.isDirectory)
1197 			return false;
1198 		if (isListed != other.isListed)
1199 			return false;
1200 		if (isScanned != other.isScanned)
1201 			return false;
1202 		if (language == null) {
1203 			if (other.language != null)
1204 				return false;
1205 		} else if (!language.equals(other.language))
1206 			return false;
1207 		// do not compare genres of books, because in the absence of certain genres in the handbook,
1208 		// the 'genres' field obtained from the database will not be equal to the field obtained when parsing the book file.
1209 		/*
1210 		if (!eqGenre(genres, other.genres))
1211 			return false;
1212 		*/
1213 		if (description == null) {
1214 			if (other.description != null)
1215 				return false;
1216 		} else if (!description.equals(other.description))
1217 			return false;
1218 		if (lastAccessTime != other.lastAccessTime)
1219 			return false;
1220 		if (parent == null) {
1221 			if (other.parent != null)
1222 				return false;
1223 		} else if (!parent.equals(other.parent))
1224 			return false;
1225 		if (path == null) {
1226 			if (other.path != null)
1227 				return false;
1228 		} else if (!path.equals(other.path))
1229 			return false;
1230 		if (pathname == null) {
1231 			if (other.pathname != null)
1232 				return false;
1233 		} else if (!pathname.equals(other.pathname))
1234 			return false;
1235 		if (series == null) {
1236 			if (other.series != null && other.series.length() != 0)
1237 				return false;
1238 		} else if (!series.equals(other.series) && !(series.length() == 0 && other.series == null))
1239 			return false;
1240 		if (seriesNumber != other.seriesNumber)
1241 			return false;
1242 		if (size != other.size)
1243 			return false;
1244 		if (title == null) {
1245 			if (other.title != null)
1246 				return false;
1247 		} else if (!title.equals(other.title))
1248 			return false;
1249 		if (crc32 != other.crc32)
1250 			return false;
1251 		if (domVersion != other.domVersion)
1252 			return false;
1253 		if (blockRenderingFlags != other.blockRenderingFlags)
1254 			return false;
1255 		return true;
1256 	}
1257 
baseEquals(FileInfo other)1258 	public boolean baseEquals(FileInfo other) {
1259 		if (this == other)
1260 			return true;
1261 		if (other == null)
1262 			return false;
1263 		if (arcname == null) {
1264 			if (other.arcname != null)
1265 				return false;
1266 		} else if (!arcname.equals(other.arcname))
1267 			return false;
1268 		if (arcsize != other.arcsize)
1269 			return false;
1270 		if (authors == null) {
1271 			if (other.authors != null)
1272 				return false;
1273 		} else if (!authors.equals(other.authors))
1274 			return false;
1275 		if (filename == null) {
1276 			if (other.filename != null)
1277 				return false;
1278 		} else if (!filename.equals(other.filename))
1279 			return false;
1280 		if (flags != other.flags)
1281 			return false;
1282 		if (format != other.format)
1283 			return false;
1284 		if (isArchive != other.isArchive)
1285 			return false;
1286 		if (isDirectory != other.isDirectory)
1287 			return false;
1288 		if (language == null) {
1289 			if (other.language != null)
1290 				return false;
1291 		} else if (!language.equals(other.language))
1292 			return false;
1293 		// do not compare genres of books, because in the absence of certain genres in the handbook,
1294 		// the 'genres' field obtained from the database will not be equal to the field obtained when parsing the book file.
1295 		/*
1296 		if (!eqGenre(genres, other.genres))
1297 			return false;
1298 		*/
1299 		if (description == null) {
1300 			if (other.description != null)
1301 				return false;
1302 		} else if (!description.equals(other.description))
1303 			return false;
1304 		if (path == null) {
1305 			if (other.path != null)
1306 				return false;
1307 		} else if (!path.equals(other.path))
1308 			return false;
1309 		if (pathname == null) {
1310 			if (other.pathname != null)
1311 				return false;
1312 		} else if (!pathname.equals(other.pathname))
1313 			return false;
1314 		if (series == null) {
1315 			if (other.series != null && other.series.length() != 0)
1316 				return false;
1317 		} else if (!series.equals(other.series) && !(series.length() == 0 && other.series == null))
1318 			return false;
1319 		if (seriesNumber != other.seriesNumber)
1320 			return false;
1321 		if (size != other.size)
1322 			return false;
1323 		if (title == null) {
1324 			if (other.title != null)
1325 				return false;
1326 		} else if (!title.equals(other.title))
1327 			return false;
1328 		if (crc32 != other.crc32)
1329 			return false;
1330 		return true;
1331 	}
1332 
eqGenre(String g1, String g2)1333 	private static boolean eqGenre(String g1, String g2) {
1334 		if (g1 == null) {
1335 			if (g2 != null && g2.length() != 0)
1336 				return false;
1337 		}
1338 		if (g1.equals(g2))
1339 			return true;
1340 		String[] g1_array = g1.split("\\|");
1341 		String[] g2_array = g2.split("\\|");
1342 		if (g1_array.length == g2_array.length) {
1343 			Arrays.sort(g1_array);
1344 			Arrays.sort(g2_array);
1345 			return Arrays.equals(g1_array, g2_array);
1346 		}
1347 		return false;
1348 	}
1349 
1350 	@Override
toString()1351 	public String toString()
1352 	{
1353 		return pathname;
1354 	}
1355 
allowSorting()1356 	public boolean allowSorting() {
1357 		return isDirectory && !isRootDir() && !isRecentDir() && !isOPDSDir() && !isBooksBySeriesDir();
1358 	}
1359 }
1360