1 package org.coolreader.crengine;
2 
3 import android.app.AlertDialog;
4 import android.graphics.Bitmap;
5 import android.graphics.drawable.Drawable;
6 import android.os.Environment;
7 import android.util.Log;
8 
9 import org.coolreader.R;
10 
11 import java.io.ByteArrayInputStream;
12 import java.io.File;
13 import java.io.FileInputStream;
14 import java.io.FileOutputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.Iterator;
24 import java.util.LinkedHashMap;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.MissingResourceException;
28 import java.util.zip.ZipEntry;
29 
30 /**
31  * CoolReader Engine class.
32  * <p>
33  * Only one instance is allowed.
34  */
35 public class Engine {
36 
37 	public static final Logger log = L.create("en");
38 	public static final Object lock = new Object();
39 
40 
41 	static final private String LIBRARY_NAME = "cr3engine-3-2-X";
42 
43 	private BaseActivity mActivity;
44 
45 
46 	// private final View mMainView;
47 	// private final ExecutorService mExecutor =
48 	// Executors.newFixedThreadPool(1);
49 
50 	/**
51 	 * Get storage root directories.
52 	 *
53 	 * @return array of r/w storage roots
54 	 */
getStorageDirectories(boolean writableOnly)55 	public static File[] getStorageDirectories(boolean writableOnly) {
56 		Collection<File> res = new HashSet<File>(2);
57 		for (File dir : mountedRootsList) {
58 			if (dir.isDirectory() && dir.canRead() && (!writableOnly || dir.canWrite())) {
59 				String[] list = dir.list();
60 				// dir.list() can return null when I/O error occurs.
61 				if (list != null && list.length > 0)
62 					res.add(dir);
63 			}
64 		}
65 		return res.toArray(new File[res.size()]);
66 	}
67 
getMountedRootsMap()68 	public static Map<String, String> getMountedRootsMap() {
69 		return mountedRootsMap;
70 	}
71 
72 	/**
73 	 * @param shortcut File system shortcut that return application "Files" by Google (package="com.android.externalstorage.documents")
74 	 *                 "primary" for internal storage,
75 	 *                 "XXXX-XXXX" (file system serial number) for any external storage like sdcard, usb disk, etc.
76 	 * @return path to mount root for given file system.
77 	 */
getMountRootByShortcut(String shortcut)78 	public static String getMountRootByShortcut(String shortcut) {
79 		String mountRoot = null;
80 		if ("primary".equals(shortcut)) {
81 			// "/document/primary:/.*"
82 			for (Map.Entry<String, String> entry : mountedRootsMap.entrySet()) {
83 				if ("SD".equals(entry.getValue())) {
84 					mountRoot = entry.getKey();
85 					break;
86 				}
87 			}
88 		} else {
89 			// "/document/XXXX-XXXX/.*"
90 			String pattern = "/storage/" + shortcut;
91 			for (Map.Entry<String, String> entry : mountedRootsMap.entrySet()) {
92 				// Android 6 ext storage, @see addMountRoot()
93 				if ("EXT SD".equals(entry.getValue()) && entry.getKey().startsWith(pattern)) {
94 					mountRoot = entry.getKey();
95 					break;
96 				}
97 			}
98 		}
99 		return mountRoot;
100 	}
101 
isRootsMountPoint(String path)102 	public boolean isRootsMountPoint(String path) {
103 		if (mountedRootsMap == null)
104 			return false;
105 		return mountedRootsMap.containsKey(path);
106 	}
107 
108 	/**
109 	 * Get or create writable subdirectory for specified base directory
110 	 *
111 	 * @param dir               is base directory
112 	 * @param subdir            is subdirectory name, null to use base directory
113 	 * @param createIfNotExists is true to force directory creation
114 	 * @return writable directory, null if not exist or not writable
115 	 */
getSubdir(File dir, String subdir, boolean createIfNotExists, boolean writableOnly)116 	public static File getSubdir(File dir, String subdir,
117 								 boolean createIfNotExists, boolean writableOnly) {
118 		if (dir == null)
119 			return null;
120 		File dataDir = dir;
121 		if (subdir != null) {
122 			dataDir = new File(dataDir, subdir);
123 			if (!dataDir.isDirectory() && createIfNotExists)
124 				dataDir.mkdir();
125 		}
126 		if (dataDir.isDirectory() && (!writableOnly || dataDir.canWrite()))
127 			return dataDir;
128 		return null;
129 	}
130 
131 	/**
132 	 * Returns array of writable data directories on external storage
133 	 *
134 	 * @param subdir
135 	 * @param createIfNotExists
136 	 * @return
137 	 */
getDataDirectories(String subdir, boolean createIfNotExists, boolean writableOnly)138 	public static File[] getDataDirectories(String subdir,
139 											boolean createIfNotExists, boolean writableOnly) {
140 		File[] roots = getStorageDirectories(writableOnly);
141 		ArrayList<File> res = new ArrayList<>(roots.length);
142 		for (File dir : roots) {
143 			File dataDir = getSubdir(dir, ".cr3", createIfNotExists,
144 					writableOnly);
145 			if (subdir != null)
146 				dataDir = getSubdir(dataDir, subdir, createIfNotExists,
147 						writableOnly);
148 			if (dataDir != null)
149 				res.add(dataDir);
150 		}
151 		return res.toArray(new File[]{});
152 	}
153 
154 	public interface EngineTask {
work()155 		public void work() throws Exception;
156 
done()157 		public void done();
158 
fail(Exception e)159 		public void fail(Exception e);
160 	}
161 
162 	public final static boolean LOG_ENGINE_TASKS = false;
163 
164 	private class TaskHandler implements Runnable {
165 		final EngineTask task;
166 
TaskHandler(EngineTask task)167 		public TaskHandler(EngineTask task) {
168 			this.task = task;
169 		}
170 
toString()171 		public String toString() {
172 			return "[handler for " + this.task.toString() + "]";
173 		}
174 
run()175 		public void run() {
176 			try {
177 				if (LOG_ENGINE_TASKS)
178 					log.i("running task.work() "
179 							+ task.getClass().getName());
180 				// run task
181 				task.work();
182 				if (LOG_ENGINE_TASKS)
183 					log.i("exited task.work() "
184 							+ task.getClass().getName());
185 				// post success callback
186 				BackgroundThread.instance().postGUI(() -> {
187 					if (LOG_ENGINE_TASKS)
188 						log.i("running task.done() "
189 								+ task.getClass().getName()
190 								+ " in gui thread");
191 					task.done();
192 				});
193 				// } catch ( final FatalError e ) {
194 				// TODO:
195 				// Handler h = view.getHandler();
196 				//
197 				// if ( h==null ) {
198 				// View root = view.getRootView();
199 				// h = root.getHandler();
200 				// }
201 				// if ( h==null ) {
202 				// //
203 				// e.handle();
204 				// } else {
205 				// h.postAtFrontOfQueue(new Runnable() {
206 				// public void run() {
207 				// e.handle();
208 				// }
209 				// });
210 				// }
211 			} catch (final Exception e) {
212 				log.e("exception while running task "
213 						+ task.getClass().getName(), e);
214 				// post error callback
215 				BackgroundThread.instance().postGUI(() -> {
216 					log.e("running task.fail(" + e.getMessage()
217 							+ ") " + task.getClass().getSimpleName()
218 							+ " in gui thread ");
219 					task.fail(e);
220 				});
221 			}
222 		}
223 	}
224 
225 	/**
226 	 * Execute task in Engine thread
227 	 *
228 	 * @param task is task to execute
229 	 */
execute(final EngineTask task)230 	public void execute(final EngineTask task) {
231 		if (LOG_ENGINE_TASKS)
232 			log.d("executing task " + task.getClass().getSimpleName());
233 		TaskHandler taskHandler = new TaskHandler(task);
234 		BackgroundThread.instance().executeBackground(taskHandler);
235 	}
236 
237 	/**
238 	 * Schedule task for execution in Engine thread
239 	 *
240 	 * @param task is task to execute
241 	 */
post(final EngineTask task)242 	public void post(final EngineTask task) {
243 		if (LOG_ENGINE_TASKS)
244 			log.d("executing task " + task.getClass().getSimpleName());
245 		TaskHandler taskHandler = new TaskHandler(task);
246 		BackgroundThread.instance().postBackground(taskHandler);
247 	}
248 
249 	/**
250 	 * Schedule Runnable for execution in GUI thread after all current Engine
251 	 * queue tasks done.
252 	 *
253 	 * @param task
254 	 */
runInGUI(final Runnable task)255 	public void runInGUI(final Runnable task) {
256 		execute(new EngineTask() {
257 
258 			public void done() {
259 				BackgroundThread.instance().postGUI(task);
260 			}
261 
262 			public void fail(Exception e) {
263 				// do nothing
264 			}
265 
266 			public void work() throws Exception {
267 				// do nothing
268 			}
269 		});
270 	}
271 
fatalError(String msg)272 	public void fatalError(String msg) {
273 		AlertDialog dlg = new AlertDialog.Builder(mActivity).setMessage(msg)
274 				.setTitle("CoolReader fatal error").show();
275 		try {
276 			Thread.sleep(10);
277 		} catch (InterruptedException e) {
278 			// do nothing
279 		}
280 		dlg.dismiss();
281 		mActivity.finish();
282 	}
283 
284 	private ProgressDialog mProgress;
285 	private boolean enable_progress = true;
286 	private boolean progressShown = false;
287 	private static int PROGRESS_STYLE = ProgressDialog.STYLE_HORIZONTAL;
288 	private Drawable progressIcon = null;
289 
290 	// public void setProgressDrawable( final BitmapDrawable drawable )
291 	// {
292 	// if ( enable_progress ) {
293 	// mBackgroundThread.executeGUI( new Runnable() {
294 	// public void run() {
295 	// // show progress
296 	// log.v("showProgress() - in GUI thread");
297 	// if ( mProgress!=null && progressShown ) {
298 	// hideProgress();
299 	// progressIcon = drawable;
300 	// showProgress(mProgressPos, mProgressMessage);
301 	// //mProgress.setIcon(drawable);
302 	// }
303 	// }
304 	// });
305 	// }
306 	// }
showProgress(final int mainProgress, final int resourceId)307 	public void showProgress(final int mainProgress, final int resourceId) {
308 		showProgress(mainProgress,
309 				mActivity.getResources().getString(resourceId));
310 	}
311 
312 	private String mProgressMessage = null;
313 	private int mProgressPos = 0;
314 
315 	private volatile int nextProgressId = 0;
316 
317 	public class DelayedProgress {
318 		private volatile boolean cancelled;
319 		private volatile boolean shown;
320 
321 		/**
322 		 * Cancel scheduled progress.
323 		 */
cancel()324 		public void cancel() {
325 			cancelled = true;
326 		}
327 
328 		/**
329 		 * Cancel and hide scheduled progress.
330 		 */
hide()331 		public void hide() {
332 			this.cancelled = true;
333 			BackgroundThread.instance().executeGUI(() -> {
334 				if (shown)
335 					hideProgress();
336 				shown = false;
337 			});
338 		}
339 
DelayedProgress(final int percent, final String msg, final int delayMillis)340 		DelayedProgress(final int percent, final String msg, final int delayMillis) {
341 			this.cancelled = false;
342 			BackgroundThread.instance().postGUI(() -> {
343 				if (!cancelled) {
344 					showProgress(percent, msg);
345 					shown = true;
346 				}
347 			}, delayMillis);
348 		}
349 	}
350 
351 	/**
352 	 * Display progress dialog after delay.
353 	 * (thread-safe)
354 	 *
355 	 * @param mainProgress is percent*100
356 	 * @param msg          is progress message text
357 	 * @param delayMillis  is delay before display of progress
358 	 * @return DelayedProgress object which can be use to hide or cancel this schedule
359 	 */
showProgressDelayed(final int mainProgress, final String msg, final int delayMillis)360 	public DelayedProgress showProgressDelayed(final int mainProgress, final String msg, final int delayMillis) {
361 		return new DelayedProgress(mainProgress, msg, delayMillis);
362 	}
363 
364 	/**
365 	 * Show progress dialog.
366 	 * (thread-safe)
367 	 *
368 	 * @param mainProgress is percent*100
369 	 * @param msg          is progress message
370 	 */
showProgress(final int mainProgress, final String msg)371 	public void showProgress(final int mainProgress, final String msg) {
372 		final int progressId = ++nextProgressId;
373 		mProgressMessage = msg;
374 		mProgressPos = mainProgress;
375 		if (mainProgress == 10000) {
376 			//log.v("mainProgress==10000 : calling hideProgress");
377 			hideProgress();
378 			return;
379 		}
380 		log.v("showProgress(" + mainProgress + ", \"" + msg
381 				+ "\") is called : " + Thread.currentThread().getName());
382 		if (enable_progress) {
383 			BackgroundThread.instance().executeGUI(() -> {
384 				// show progress
385 				//log.v("showProgress() - in GUI thread");
386 				if (progressId != nextProgressId) {
387 					//log.v("showProgress() - skipping duplicate progress event");
388 					return;
389 				}
390 				if (mProgress == null) {
391 					//log.v("showProgress() - creating progress window");
392 					try {
393 						if (mActivity != null && mActivity.isStarted()) {
394 							mProgress = new ProgressDialog(mActivity);
395 							mProgress
396 									.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
397 							if (progressIcon != null)
398 								mProgress.setIcon(progressIcon);
399 							else
400 								mProgress.setIcon(R.mipmap.cr3_logo);
401 							mProgress.setMax(10000);
402 							mProgress.setCancelable(false);
403 							mProgress.setProgress(mainProgress);
404 							mProgress
405 									.setTitle(mActivity
406 											.getResources()
407 											.getString(
408 													R.string.progress_please_wait));
409 							mProgress.setMessage(msg);
410 							mProgress.show();
411 							progressShown = true;
412 						}
413 					} catch (Exception e) {
414 						Log.e("cr3",
415 								"Exception while trying to show progress dialog",
416 								e);
417 						progressShown = false;
418 						mProgress = null;
419 					}
420 				} else {
421 					mProgress.setProgress(mainProgress);
422 					mProgress.setMessage(msg);
423 					if (!mProgress.isShowing()) {
424 						mProgress.show();
425 						progressShown = true;
426 					}
427 				}
428 			});
429 		}
430 	}
431 
432 	/**
433 	 * Hide progress dialog (if shown).
434 	 * (thread-safe)
435 	 */
hideProgress()436 	public void hideProgress() {
437 		final int progressId = ++nextProgressId;
438 		log.v("hideProgress() - is called : "
439 				+ Thread.currentThread().getName());
440 		// log.v("hideProgress() is called");
441 		BackgroundThread.instance().executeGUI(() -> {
442 			// hide progress
443 //				log.v("hideProgress() - in GUI thread");
444 			if (progressId != nextProgressId) {
445 //					Log.v("cr3",
446 //							"hideProgress() - skipping duplicate progress event");
447 				return;
448 			}
449 			if (mProgress != null) {
450 				// if ( mProgress.isShowing() )
451 				// mProgress.hide();
452 				progressShown = false;
453 				progressIcon = null;
454 				if (mProgress.isShowing())
455 					mProgress.dismiss();
456 				mProgress = null;
457 //					log.v("hideProgress() - in GUI thread, finished");
458 			}
459 		});
460 	}
461 
isProgressShown()462 	public boolean isProgressShown() {
463 		return progressShown;
464 	}
465 
loadFileUtf8(File file)466 	public static String loadFileUtf8(File file) {
467 		try (InputStream is = new FileInputStream(file)) {
468 			return loadResourceUtf8(is);
469 		} catch (Exception e) {
470 			log.w("cannot load resource from file " + file);
471 			return null;
472 		}
473 	}
474 
loadResourceUtf8(int id)475 	public String loadResourceUtf8(int id) {
476 		try {
477 			return loadResourceUtf8(mActivity.getResources().openRawResource(id));
478 		} catch (Exception e) {
479 			log.e("cannot load resource " + id);
480 			return null;
481 		}
482 	}
483 
loadResourceUtf8(InputStream is)484 	public static String loadResourceUtf8(InputStream is) {
485 		try (InputStream inputStream = is) {
486 			int available = inputStream.available();
487 			if (available <= 0)
488 				return null;
489 			byte[] buf = new byte[available];
490 			if (inputStream.read(buf) != available)
491 				throw new IOException("Resource not read fully");
492 			return new String(buf, 0, available, "UTF8");
493 		} catch (Exception e) {
494 			log.e("cannot load resource");
495 			return null;
496 		}
497 	}
498 
loadResourceBytes(int id)499 	public byte[] loadResourceBytes(int id) {
500 		try {
501 			return loadResourceBytes(mActivity.getResources().openRawResource(id));
502 		} catch (Exception e) {
503 			log.e("cannot load resource");
504 			return null;
505 		}
506 	}
507 
loadResourceBytes(File f)508 	public static byte[] loadResourceBytes(File f) {
509 		if (f == null || !f.isFile() || !f.exists())
510 			return null;
511 		try (FileInputStream is = new FileInputStream(f)) {
512 			return loadResourceBytes(is);
513 		} catch (IOException e) {
514 			log.e("Cannot open file " + f);
515 		}
516 		return null;
517 	}
518 
loadResourceBytes(InputStream is)519 	public static byte[] loadResourceBytes(InputStream is) {
520 		try (InputStream inputStream = is) {
521 			int available = inputStream.available();
522 			if (available <= 0)
523 				return null;
524 			byte[] buf = new byte[available];
525 			if (inputStream.read(buf) != available)
526 				throw new IOException("Resource not read fully");
527 			return buf;
528 		} catch (Exception e) {
529 			log.e("cannot load resource");
530 			return null;
531 		}
532 	}
533 
534 	private static Engine instance;
535 
getInstance(BaseActivity activity)536 	public static Engine getInstance(BaseActivity activity) {
537 		if (instance == null) {
538 			instance = new Engine(activity);
539 		} else {
540 			instance.setParams(activity);
541 		}
542 		return instance;
543 	}
544 
setParams(BaseActivity activity)545 	private void setParams(BaseActivity activity) {
546 		this.mActivity = activity;
547 	}
548 
549 	/**
550 	 * Initialize CoolReader Engine
551 	 *
552 	 * @param activity base application activity
553 	 */
Engine(BaseActivity activity)554 	private Engine(BaseActivity activity) {
555 		setParams(activity);
556 	}
557 
initAgain()558 	public void initAgain() {
559 		initMountRoots();
560 		File[] dataDirs = Engine.getDataDirectories(null, false, true);
561 		if (dataDirs != null && dataDirs.length > 0) {
562 			log.i("Engine.initAgain() : DataDir exist at start.");
563 			DATADIR_IS_EXIST_AT_START = true;
564 		} else {
565 			log.i("Engine.initAgain() : DataDir NOT exist at start.");
566 		}
567 		mFonts = findFonts();
568 		findExternalHyphDictionaries();
569 		if (!initInternal(mFonts, DeviceInfo.getSDKLevel())) {
570 			log.i("Engine.initInternal failed!");
571 			throw new RuntimeException("Cannot initialize CREngine JNI");
572 		}
573 		initCacheDirectory();
574 		log.i("Engine() : initialization done");
575 	}
576 
577 	// Native functions
initInternal(String[] fontList, int sdk_int)578 	private native static boolean initInternal(String[] fontList, int sdk_int);
579 
initDictionaries(HyphDict[] dicts)580 	private native static boolean initDictionaries(HyphDict[] dicts);
581 
uninitInternal()582 	private native static void uninitInternal();
583 
getFontFaceListInternal()584 	private native static String[] getFontFaceListInternal();
585 
getFontFileNameListInternal()586 	private native static String[] getFontFileNameListInternal();
587 
getArchiveItemsInternal(String arcName)588 	private native static String[] getArchiveItemsInternal(String arcName); // pairs: pathname, size
589 
setKeyBacklightInternal(int value)590 	private native static boolean setKeyBacklightInternal(int value);
591 
setCacheDirectoryInternal(String dir, int size)592 	private native static boolean setCacheDirectoryInternal(String dir, int size);
593 
scanBookPropertiesInternal(FileInfo info)594 	private native static boolean scanBookPropertiesInternal(FileInfo info);
595 
updateFileCRC32Internal(FileInfo info)596 	private native static boolean updateFileCRC32Internal(FileInfo info);
597 
scanBookCoverInternal(String path)598 	private native static byte[] scanBookCoverInternal(String path);
599 
drawBookCoverInternal(Bitmap bmp, byte[] data, String fontFace, String title, String authors, String seriesName, int seriesNumber, int bpp)600 	private native static void drawBookCoverInternal(Bitmap bmp, byte[] data, String fontFace, String title, String authors, String seriesName, int seriesNumber, int bpp);
601 
suspendLongOperationInternal()602 	private native static void suspendLongOperationInternal(); // cancel current long operation in engine thread (swapping to cache file) -- call it from GUI thread
603 
604 	/**
605 	 * Test if in embedded FontConfig language orthography catalog have record with language code langCode.
606 	 *
607 	 * @param langCode language code
608 	 * @return true if record with langCode found, false - otherwise.
609 	 * <p>
610 	 * Language code compared as is without any modifications.
611 	 */
haveFcLangCodeInternal(String langCode)612 	private native static boolean haveFcLangCodeInternal(String langCode);
613 
614 	/**
615 	 * Check the font for compatibility with the specified language.
616 	 *
617 	 * @param fontFace font face to check.
618 	 * @param langCode language code in embedded FontConfig language orthography catalog.
619 	 * @return true if font compatible with language, false - otherwise.
620 	 */
checkFontLanguageCompatibilityInternal(String fontFace, String langCode)621 	private native static boolean checkFontLanguageCompatibilityInternal(String fontFace, String langCode);
622 
listFilesInternal(File dir)623 	private native static File[] listFilesInternal(File dir);
624 
suspendLongOperation()625 	public static void suspendLongOperation() {
626 		suspendLongOperationInternal();
627 	}
628 
checkFontLanguageCompatibility(String fontFace, String langCode)629 	public synchronized static boolean checkFontLanguageCompatibility(String fontFace, String langCode) {
630 		return checkFontLanguageCompatibilityInternal(fontFace, langCode);
631 	}
632 
listFiles(File dir)633 	public static synchronized File[] listFiles(File dir) {
634 		return listFilesInternal(dir);
635 	}
636 
getDomVersionCurrent()637 	private native static int getDomVersionCurrent();
638 
639 	/**
640 	 * Finds the corresponding language code in embedded FontConfig language orthography catalog.
641 	 *
642 	 * @param language language code in free form: ISO 639-1, ISO 639-2 or full name of the language in English. Also allowed concatenation of country code in ISO 3166-1 alpha-2 or ISO 3166-1 alpha-3.
643 	 * @return language code in the FontConfig language orthography catalog if it's found, null - otherwise.
644 	 * <p>
645 	 * If a country code in any form is added to the language, but the record with the country code is not found - it is simply ignored and the search continues without a country code.
646 	 */
findCompatibleFcLangCode(String language)647 	public static String findCompatibleFcLangCode(String language) {
648 		String langCode = null;
649 
650 		String lang_part;
651 		String country_part;
652 		String testLang;
653 
654 		// Split language and country codes
655 		int pos = language.indexOf('-');
656 		if (-1 == pos)
657 			pos = language.indexOf('_');
658 		if (pos > 0) {
659 			lang_part = language.substring(0, pos);
660 			if (pos < language.length() - 1)
661 				country_part = language.substring(pos + 1);
662 			else
663 				country_part = "";
664 		} else {
665 			lang_part = language;
666 			country_part = "";
667 		}
668 		lang_part = lang_part.toLowerCase();
669 		country_part = country_part.toLowerCase();
670 
671 		if (country_part.length() > 0)
672 			testLang = lang_part + "_" + country_part;
673 		else
674 			testLang = lang_part;
675 		// 1. Check if testLang is already language code accepted by FontConfig languages symbols database
676 		if (haveFcLangCodeInternal(testLang))
677 			langCode = testLang;
678 		else {
679 			// Check if lang_part is the three-letter abbreviation: ISO 639-2 or ISO 639-3
680 			//   and if country_part code is the three-letter country code: ISO 3366-1 alpha 3
681 			// Then convert them to two-letter code and test
682 			String lang_2l = null;
683 			String country_2l = null;
684 			int found = 0;
685 			for (Locale loc : Locale.getAvailableLocales()) {
686 				try {
687 					if (lang_part.equals(loc.getISO3Language())) {
688 						lang_2l = loc.getLanguage();
689 						found |= 1;
690 					}
691 				} catch (MissingResourceException e) {
692 					// three-letter language abbreviation is not available for this locale
693 					// just ignore this exception
694 				}
695 				if (country_part.length() > 0) {
696 					try {
697 						if (country_part.equals(loc.getISO3Country().toLowerCase())) {
698 							country_2l = loc.getCountry().toLowerCase();
699 							found |= 2;
700 						}
701 					} catch (MissingResourceException e) {
702 						// the three-letter country abbreviation is not available for this locale
703 						// just ignore this exception
704 					}
705 					if (3 == found)
706 						break;
707 				} else if (1 == found)
708 					break;
709 			}
710 			if (country_part.length() > 0) {
711 				// 2. test lang_2l + country_part
712 				if (null != lang_2l) {
713 					testLang = lang_2l + "_" + country_part;
714 					if (haveFcLangCodeInternal(testLang))
715 						langCode = testLang;
716 				}
717 				if (null == langCode && null != country_2l) {
718 					// 3. test lang_part + country_2l
719 					testLang = lang_part + "_" + country_2l;
720 					if (haveFcLangCodeInternal(testLang))
721 						langCode = testLang;
722 				}
723 				if (null == langCode && null != country_2l && null != lang_2l) {
724 					// 4. test lang_2l + country_2l
725 					testLang = lang_2l + "_" + country_2l;
726 					if (haveFcLangCodeInternal(testLang))
727 						langCode = testLang;
728 				}
729 			}
730 			if (null == langCode) {
731 				if (null != lang_2l) {
732 					// 5. test lang_2l
733 					// if two-letter country code not found or county code omitted
734 					// but found two-letter language code
735 					testLang = lang_2l;
736 					if (haveFcLangCodeInternal(testLang))
737 						langCode = testLang;
738 				} else {
739 					// 6. test lang_part
740 					testLang = lang_part;
741 					if (haveFcLangCodeInternal(testLang))
742 						langCode = testLang;
743 				}
744 			}
745 		}
746 		if (null == langCode) {
747 			Locale locale = null;
748 			// 7. Try to find by full language name
749 			for (Locale loc : Locale.getAvailableLocales()) {
750 				if (language.equalsIgnoreCase(loc.getDisplayLanguage(Locale.ENGLISH))) {
751 					locale = loc;
752 					break;
753 				}
754 			}
755 			if (null != locale) {
756 				testLang = locale.getISO3Language();
757 				if (haveFcLangCodeInternal(testLang))
758 					langCode = testLang;
759 				else {
760 					testLang = locale.getLanguage();        // two-letter code
761 					if (haveFcLangCodeInternal(testLang))
762 						langCode = testLang;
763 				}
764 			}
765 		}
766 		return langCode;
767 	}
768 
769 	/**
770 	 * Checks whether specified directlry or file is symbolic link.
771 	 * (thread-safe)
772 	 *
773 	 * @param pathName is path to check
774 	 * @return path link points to if specified directory is link (symlink), null for regular file/dir
775 	 */
isLink(String pathName)776 	public native static String isLink(String pathName);
777 
folowLink(String pathName)778 	public static String folowLink(String pathName) {
779 		String lnk = isLink(pathName);
780 		if (lnk == null)
781 			return pathName;
782 		String lnk2 = isLink(lnk);
783 		if (lnk2 == null)
784 			return lnk;
785 		return lnk2;
786 	}
787 
788 	private static final int HYPH_NONE = 0;
789 	private static final int HYPH_ALGO = 1;
790 	private static final int HYPH_DICT = 2;
791 	private static final int HYPH_BOOK = 0;
792 
getArchiveItems(String zipFileName)793 	public ArrayList<ZipEntry> getArchiveItems(String zipFileName) {
794 		final int itemsPerEntry = 2;
795 		String[] in;
796 		synchronized (lock) {
797 			in = getArchiveItemsInternal(zipFileName);
798 		}
799 		ArrayList<ZipEntry> list = new ArrayList<ZipEntry>();
800 		for (int i = 0; i <= in.length - itemsPerEntry; i += itemsPerEntry) {
801 			ZipEntry e = new ZipEntry(in[i]);
802 			e.setSize(Integer.valueOf(in[i + 1]));
803 			e.setCompressedSize(Integer.valueOf(in[i + 1]));
804 			list.add(e);
805 		}
806 		return list;
807 	}
808 
809 	public static class HyphDict {
810 		private static HyphDict[] values = new HyphDict[]{};
811 		public final static HyphDict NONE = new HyphDict("@none", HYPH_NONE, 0, "[None]", "");
812 		public final static HyphDict ALGORITHM = new HyphDict("@algorithm", HYPH_ALGO, 0, "[Algorythmic]", "");
813 		public final static HyphDict BOOK_LANGUAGE = new HyphDict("BOOK LANGUAGE", HYPH_BOOK, 0, "[From Book Language]", "");
814 		public final static HyphDict RUSSIAN = new HyphDict("Russian_EnUS", HYPH_DICT, R.raw.russian_enus_hyphen, "Russian", "ru");
815 		public final static HyphDict RUSSIAN2 = new HyphDict("Russian", HYPH_DICT, R.raw.russian_enus_hyphen, "Russian", "ru", true);	// for compatibilty with textlang.cpp
816 		public final static HyphDict ENGLISH = new HyphDict("English_US", HYPH_DICT, R.raw.english_us_hyphen, "English US", "en");
817 		public final static HyphDict GERMAN = new HyphDict("German", HYPH_DICT, R.raw.german_hyphen, "German", "de");
818 		public final static HyphDict UKRAINIAN = new HyphDict("Ukrain", HYPH_DICT, R.raw.ukrainian_hyphen, "Ukrainian", "uk");
819 		public final static HyphDict SPANISH = new HyphDict("Spanish", HYPH_DICT, R.raw.spanish_hyphen, "Spanish", "es");
820 		public final static HyphDict FRENCH = new HyphDict("French", HYPH_DICT, R.raw.french_hyphen, "French", "fr");
821 		public final static HyphDict BULGARIAN = new HyphDict("Bulgarian", HYPH_DICT, R.raw.bulgarian_hyphen, "Bulgarian", "bg");
822 		public final static HyphDict SWEDISH = new HyphDict("Swedish", HYPH_DICT, R.raw.swedish_hyphen, "Swedish", "sv");
823 		public final static HyphDict POLISH = new HyphDict("Polish", HYPH_DICT, R.raw.polish_hyphen, "Polish", "pl");
824 		public final static HyphDict HUNGARIAN = new HyphDict("Hungarian", HYPH_DICT, R.raw.hungarian_hyphen, "Hungarian", "hu");
825 		public final static HyphDict GREEK = new HyphDict("Greek", HYPH_DICT, R.raw.greek_hyphen, "Greek", "el");
826 		public final static HyphDict FINNISH = new HyphDict("Finnish", HYPH_DICT, R.raw.finnish_hyphen, "Finnish", "fi");
827 		public final static HyphDict TURKISH = new HyphDict("Turkish", HYPH_DICT, R.raw.turkish_hyphen, "Turkish", "tr");
828 		public final static HyphDict DUTCH = new HyphDict("Dutch", HYPH_DICT, R.raw.dutch_hyphen, "Dutch", "nl");
829 		public final static HyphDict CATALAN = new HyphDict("Catalan", HYPH_DICT, R.raw.catalan_hyphen, "Catalan", "ca");
830 		// Remember that when adding a new hyphenation dictionary, you should update the _hyph_dict_table[] array in textlang.cpp
831 		// Field 'code' must be equal field 'hyph_filename_prefix' in the _hyph_dict_table[] array.
832 
833 		public final String code;
834 		public final int type;
835 		public final int resource;
836 		public final String name;
837 		public final File file;
838 		public String language;
839 		public boolean hide;
840 
841 
values()842 		public static HyphDict[] values() {
843 			return values;
844 		}
845 
add(HyphDict dict)846 		private static void add(HyphDict dict) {
847 			// Arrays.copyOf(values, values.length+1); -- absent until API level 9
848 			HyphDict[] list = new HyphDict[values.length + 1];
849 			for (int i = 0; i < values.length; i++)
850 				list[i] = values[i];
851 			list[list.length - 1] = dict;
852 			values = list;
853 		}
854 
HyphDict(String code, int type, int resource, String name, String language)855 		private HyphDict(String code, int type, int resource, String name, String language) {
856 			this.type = type;
857 			this.resource = resource;
858 			this.name = name;
859 			this.file = null;
860 			this.code = code;
861 			this.language = language;
862 			this.hide = false;
863 			// register in list
864 			add(this);
865 		}
866 
HyphDict(String code, int type, int resource, String name, String language, boolean hide)867 		private HyphDict(String code, int type, int resource, String name, String language, boolean hide) {
868 			this.type = type;
869 			this.resource = resource;
870 			this.name = name;
871 			this.file = null;
872 			this.code = code;
873 			this.language = language;
874 			this.hide = hide;
875 			// register in list
876 			add(this);
877 		}
878 
HyphDict(File file)879 		private HyphDict(File file) {
880 			this.type = HYPH_DICT;
881 			this.resource = 0;
882 			this.name = file.getName();
883 			this.file = file;
884 			this.code = this.name;
885 			this.language = "";
886 			// register in list
887 			add(this);
888 		}
889 
byLanguage(String language)890 		private static HyphDict byLanguage(String language) {
891 			if (language != null && !language.trim().equals("")) {
892 				for (HyphDict dict : values) {
893 					if (dict != BOOK_LANGUAGE) {
894 						if (dict.language.equals(language))
895 							return dict;
896 					}
897 				}
898 			}
899 			return NONE;
900 		}
901 
byCode(String code)902 		public static HyphDict byCode(String code) {
903 			for (HyphDict dict : values)
904 				if (dict.toString().equals(code))
905 					return dict;
906 			return NONE;
907 		}
908 
byName(String name)909 		public static HyphDict byName(String name) {
910 			for (HyphDict dict : values)
911 				if (dict.name.equals(name))
912 					return dict;
913 			return NONE;
914 		}
915 
byFileName(String fileName)916 		public static HyphDict byFileName(String fileName) {
917 			for (HyphDict dict : values)
918 				if (dict.file != null && dict.file.getName().equals(fileName))
919 					return dict;
920 			return NONE;
921 		}
922 
923 		@Override
toString()924 		public String toString() {
925 			return code;
926 		}
927 
getName()928 		public String getName() {
929 			if (this == BOOK_LANGUAGE) {
930 				if (language != null && !language.trim().equals("")) {
931 					return this.name + " (currently: " + this.language + ")";
932 				} else {
933 					return this.name + " (currently: none)";
934 				}
935 			} else {
936 				return name;
937 			}
938 		}
939 
fromFile(File file)940 		public static boolean fromFile(File file) {
941 			if (file == null || !file.exists() || !file.isFile() || !file.canRead())
942 				return false;
943 			String fn = file.getName();
944 			if (!fn.toLowerCase().endsWith(".pdb") && !fn.toLowerCase().endsWith(".pattern"))
945 				return false; // wrong file name
946 			if (byFileName(file.getName()) != NONE)
947 				return false; // already registered
948 			new HyphDict(file);
949 			return true;
950 		}
951 	}
952 
953 	@SuppressWarnings("unused")		// used in jni
loadHyphDictData(String id)954 	public static final byte[] loadHyphDictData(String id) {
955 		if (null == instance) {
956 			log.e("Engine not initialized yet!");
957 			return null;
958 		}
959 		byte[] data = null;
960 		HyphDict dict = HyphDict.byCode(id);
961 		if (null != dict && HYPH_DICT == dict.type) {
962 			if (dict.resource != 0) {
963 				data = instance.loadResourceBytes(dict.resource);
964 			} else if (dict.file != null) {
965 				data = loadResourceBytes(dict.file);
966 			}
967 		}
968 		return data;
969 	}
970 
getLanguage(final String language)971 	private String getLanguage(final String language) {
972 		if (language == null || "".equals(language.trim())) {
973 			return "";
974 		} else if (language.contains("-")) {
975 			return language.substring(0, language.indexOf("-")).toLowerCase();
976 		} else {
977 			return language.toLowerCase();
978 		}
979 	}
980 
scanBookProperties(FileInfo info)981 	public boolean scanBookProperties(FileInfo info) {
982 		synchronized (lock) {
983 			long start = android.os.SystemClock.uptimeMillis();
984 			boolean res = scanBookPropertiesInternal(info);
985 			long duration = android.os.SystemClock.uptimeMillis() - start;
986 			L.v("scanBookProperties took " + duration + " ms for " + info.getPathName());
987 			return res;
988 		}
989 	}
990 
scanBookCover(String path)991 	public byte[] scanBookCover(String path) {
992 		synchronized (lock) {
993 			long start = Utils.timeStamp();
994 			byte[] res = scanBookCoverInternal(path);
995 			long duration = Utils.timeInterval(start);
996 			L.v("scanBookCover took " + duration + " ms for " + path);
997 			return res;
998 		}
999 	}
1000 
updateFileCRC32(FileInfo info)1001 	public static boolean updateFileCRC32(FileInfo info) {
1002 		synchronized (lock) {
1003 			return updateFileCRC32Internal(info);
1004 		}
1005 	}
1006 
1007 	/**
1008 	 * Draw book coverpage into bitmap buffer.
1009 	 * If cover image specified, this image will be drawn (resized to buffer size).
1010 	 * If no cover image, default coverpage will be drawn, with author, title, series.
1011 	 *
1012 	 * @param bmp          is buffer to draw in.
1013 	 * @param data         is coverpage image data bytes, or empty array if no cover image
1014 	 * @param fontFace     is font face to use.
1015 	 * @param title        is book title.
1016 	 * @param authors      is book authors list
1017 	 * @param seriesName   is series name
1018 	 * @param seriesNumber is series number
1019 	 * @param bpp          is bits per pixel (specify <=8 for eink grayscale dithering)
1020 	 */
drawBookCover(Bitmap bmp, byte[] data, String fontFace, String title, String authors, String seriesName, int seriesNumber, int bpp)1021 	public void drawBookCover(Bitmap bmp, byte[] data, String fontFace, String title, String authors, String seriesName, int seriesNumber, int bpp) {
1022 		synchronized (lock) {
1023 			long start = Utils.timeStamp();
1024 			drawBookCoverInternal(bmp, data, fontFace, title, authors, seriesName, seriesNumber, bpp);
1025 			long duration = Utils.timeInterval(start);
1026 			L.v("drawBookCover took " + duration + " ms");
1027 		}
1028 	}
1029 
getFontFaceList()1030 	public static String[] getFontFaceList() {
1031 		synchronized (lock) {
1032 			return getFontFaceListInternal();
1033 		}
1034 	}
1035 
getFontFileNameList()1036 	public static String[] getFontFileNameList() {
1037 		synchronized (lock) {
1038 			return getFontFileNameListInternal();
1039 		}
1040 	}
1041 
1042 	private int currentKeyBacklightLevel = 1;
1043 
getKeyBacklight()1044 	public int getKeyBacklight() {
1045 		return currentKeyBacklightLevel;
1046 	}
1047 
setKeyBacklight(int value)1048 	public boolean setKeyBacklight(int value) {
1049 		currentKeyBacklightLevel = value;
1050 		// thread safe
1051 		return setKeyBacklightInternal(value);
1052 	}
1053 
1054 	final static int CACHE_DIR_SIZE = 32000000;
1055 
createCacheDir(File baseDir, String subDir)1056 	private static String createCacheDir(File baseDir, String subDir) {
1057 		String cacheDirName = null;
1058 		if (baseDir.isDirectory()) {
1059 			if (baseDir.canWrite()) {
1060 				if (subDir != null) {
1061 					baseDir = new File(baseDir, subDir);
1062 					baseDir.mkdir();
1063 				}
1064 				if (baseDir.exists() && baseDir.canWrite()) {
1065 					File cacheDir = new File(baseDir, "cache");
1066 					if (cacheDir.exists() || cacheDir.mkdirs()) {
1067 						if (cacheDir.canWrite()) {
1068 							cacheDirName = cacheDir.getAbsolutePath();
1069 							CR3_SETTINGS_DIR_NAME = baseDir.getAbsolutePath();
1070 						}
1071 					}
1072 				}
1073 			} else {
1074 				log.i(baseDir.toString() + " is read only");
1075 			}
1076 		} else {
1077 			log.i(baseDir.toString() + " is not found");
1078 		}
1079 		return cacheDirName;
1080 	}
1081 
getExternalSettingsDirName()1082 	public static String getExternalSettingsDirName() {
1083 		return CR3_SETTINGS_DIR_NAME;
1084 	}
1085 
getExternalSettingsDir()1086 	public static File getExternalSettingsDir() {
1087 		return CR3_SETTINGS_DIR_NAME != null ? new File(CR3_SETTINGS_DIR_NAME) : null;
1088 	}
1089 
moveFile(File oldPlace, File newPlace)1090 	public static boolean moveFile(File oldPlace, File newPlace) {
1091 		boolean removeNewFile = true;
1092 		log.i("Moving file " + oldPlace.getAbsolutePath() + " to " + newPlace.getAbsolutePath());
1093 		if (!oldPlace.exists()) {
1094 			log.e("File " + oldPlace.getAbsolutePath() + " does not exist!");
1095 			return false;
1096 		}
1097 		try {
1098 			FileInputStream is = new FileInputStream(oldPlace);
1099 			FileOutputStream os = new FileOutputStream(newPlace);
1100 			byte[] buf = new byte[0x10000];				// 64kB
1101 			for (; ; ) {
1102 				int bytesRead = is.read(buf);
1103 				if (bytesRead <= 0)
1104 					break;
1105 				os.write(buf, 0, bytesRead);
1106 			}
1107 			os.close();
1108 			is.close();
1109 			removeNewFile = false;
1110 			oldPlace.delete();
1111 			return true;
1112 		} catch (IOException e) {
1113 			return false;
1114 		} finally {
1115 			if (removeNewFile) {
1116 				// Write to new file failed, remove it.
1117 				log.e("Failed to write into file " + newPlace.getAbsolutePath() + "!");
1118 				newPlace.delete();
1119 			}
1120 		}
1121 	}
1122 
1123 	/**
1124 	 * Checks whether file under old path exists, and moves it to better place when necessary.
1125 	 * Can be slow if big file is being moved.
1126 	 *
1127 	 * @param bestPlace is desired directory for file (e.g. new place after migration)
1128 	 * @param oldPlace  is old (obsolete) directory for file (e.g. location from older releases)
1129 	 * @param filename  is name of file
1130 	 * @return file to use (from old or new place)
1131 	 */
checkOrMoveFile(File bestPlace, File oldPlace, String filename)1132 	public static File checkOrMoveFile(File bestPlace, File oldPlace, String filename) {
1133 		if (!bestPlace.exists()) {
1134 			bestPlace.mkdirs();
1135 		}
1136 		File oldFile = new File(oldPlace, filename);
1137 		if (bestPlace.isDirectory() && bestPlace.canWrite()) {
1138 			File bestFile = new File(bestPlace, filename);
1139 			if (bestFile.exists())
1140 				return bestFile; // already exists
1141 			if (oldFile.exists() && oldFile.isFile()) {
1142 				// move file
1143 				if (moveFile(oldFile, bestFile))
1144 					return bestFile;
1145 				return oldFile;
1146 			}
1147 			return bestFile;
1148 		}
1149 		return oldFile;
1150 	}
1151 
1152 	private static String CR3_SETTINGS_DIR_NAME;
1153 
1154 	public final static String CACHE_BASE_DIR_NAME = ".cr3"; // "Books"
1155 
initCacheDirectory()1156 	private static void initCacheDirectory() {
1157 		String cacheDirName = null;
1158 		// SD card
1159 		cacheDirName = createCacheDir(
1160 				DeviceInfo.EINK_NOOK ? new File("/media/") : Environment.getExternalStorageDirectory(), CACHE_BASE_DIR_NAME);
1161 		// non-standard SD mount points
1162 		log.i(cacheDirName
1163 				+ " will be used for cache, maxCacheSize=" + CACHE_DIR_SIZE);
1164 		if (cacheDirName == null) {
1165 			for (String dirname : mountedRootsMap.keySet()) {
1166 				cacheDirName = createCacheDir(new File(dirname),
1167 						CACHE_BASE_DIR_NAME);
1168 				if (cacheDirName != null)
1169 					break;
1170 			}
1171 		}
1172 		// internal flash
1173 //		if (cacheDirName == null) {
1174 //			File cacheDir = mActivity.getCacheDir();
1175 //			if (!cacheDir.isDirectory())
1176 //				cacheDir.mkdir();
1177 //			cacheDirName = createCacheDir(cacheDir, null);
1178 //			// File cacheDir = mActivity.getDir("cache", Context.MODE_PRIVATE);
1179 ////			if (cacheDir.isDirectory() && cacheDir.canWrite())
1180 ////				cacheDirName = cacheDir.getAbsolutePath();
1181 //		}
1182 		// set cache directory for engine
1183 		if (cacheDirName != null) {
1184 			log.i(cacheDirName
1185 					+ " will be used for cache, maxCacheSize=" + CACHE_DIR_SIZE);
1186 			setCacheDirectoryInternal(cacheDirName, CACHE_DIR_SIZE);
1187 		} else {
1188 			log.w("No directory for cache is available!");
1189 		}
1190 	}
1191 
addMountRoot(Map<String, String> list, String pathname, int resourceId)1192 	private static boolean addMountRoot(Map<String, String> list, String pathname, int resourceId) {
1193 		return addMountRoot(list, pathname, pathname); //mActivity.getResources().getString(resourceId));
1194 	}
1195 
isStorageDir(String path)1196 	public static boolean isStorageDir(String path) {
1197 		if (path == null)
1198 			return false;
1199 		String normalized = pathCorrector.normalizeIfPossible(path);
1200 		String sdpath = pathCorrector.normalizeIfPossible(Environment.getExternalStorageDirectory().getAbsolutePath());
1201 		if (sdpath != null && sdpath.equals(normalized))
1202 			return true;
1203 		return false;
1204 	}
1205 
isExternalStorageDir(String path)1206 	public static boolean isExternalStorageDir(String path) {
1207 		if (path == null)
1208 			return false;
1209 		if (path.contains("/ext"))
1210 			return true;
1211 		return false;
1212 	}
1213 
addMountRoot(Map<String, String> list, String path, String name)1214 	private static boolean addMountRoot(Map<String, String> list, String path, String name) {
1215 		if (list.containsKey(path))
1216 			return false;
1217 		if (path.equals("/storage/emulated/legacy")) {
1218 			for (String key : list.keySet()) {
1219 				if (key.equals("/storage/emulated/0"))
1220 					return false; // don't add "/storage/emulated/legacy" after "/storage/emulated/0"
1221 			}
1222 		}
1223 		String plink = folowLink(path);
1224 		for (String key : list.keySet()) {
1225 			//if (pathCorrector.normalizeIfPossible(path).equals(pathCorrector.normalizeIfPossible(key))) {
1226 			if (plink.equals(folowLink(key))) { // path.startsWith(key + "/")
1227 				log.w("Skipping duplicate path " + path + " == " + key);
1228 				return false; // duplicate subpath
1229 			}
1230 		}
1231 		try {
1232 			File dir = new File(path);
1233 			if (dir.isDirectory()) {
1234 //				String[] d = dir.list();
1235 //				if ((d!=null && d.length>0) || dir.canWrite()) {
1236 				// Android 6 ext storage
1237 				if (name.startsWith("/storage/") && name.length() == 18 && name.charAt(13) == '-')
1238 					name = "EXT SD";
1239 				log.i("Adding FS root: " + path + " " + name);
1240 				list.put(path, name);
1241 //					return true;
1242 //				} else {
1243 //					log.i("Skipping mount point " + path + " : no files or directories found here, and writing is disabled");
1244 //				}
1245 			}
1246 		} catch (Exception e) {
1247 			// ignore
1248 		}
1249 		return false;
1250 	}
1251 
listStorageDir()1252 	public static HashSet<String> listStorageDir() {
1253 		final HashSet<String> out = new HashSet<String>();
1254 		File dir = new File("/storage");
1255 		try {
1256 			if (dir.exists() && dir.isDirectory()) {
1257 				File[] files = dir.listFiles();
1258 				for (File file : files) {
1259 					if (file.isDirectory() && file.canRead() && !"/storage/emulated".equals(file.getName())) {
1260 						log.d("listStorageDir path found: " + file.getAbsolutePath());
1261 						out.add(file.getAbsolutePath());
1262 					}
1263 				}
1264 			}
1265 		} catch (Exception e) {
1266 			// ignore
1267 		}
1268 		return out;
1269 	}
1270 
getExternalMounts()1271 	public static HashSet<String> getExternalMounts() {
1272 		final HashSet<String> out = new HashSet<String>();
1273 		try {
1274 			String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
1275 			String reg2 = "(?i).*fuse.*(vfat|ntfs|exfat|fat32|ext3|ext4|fuse).*rw.*";
1276 			StringBuilder s = new StringBuilder();
1277 			try {
1278 				final Process process = new ProcessBuilder().command("mount")
1279 						.redirectErrorStream(true).start();
1280 				ProcessIOWithTimeout processIOWithTimeout = new ProcessIOWithTimeout(process, 1024);
1281 				int exitCode = processIOWithTimeout.waitForProcess(100);
1282 				if (exitCode == ProcessIOWithTimeout.EXIT_CODE_TIMEOUT) {
1283 					// Timeout
1284 					log.e("Timed out waiting for mount command output, " +
1285 							"please add CoolReader to MagiskHide list!");
1286 					process.destroy();
1287 					return out;
1288 				}
1289 				try (ByteArrayInputStream inputStream = new ByteArrayInputStream(processIOWithTimeout.receivedData())) {
1290 					byte[] buffer = new byte[1024];
1291 					int rb;
1292 					while ((rb = inputStream.read(buffer)) != -1) {
1293 						s.append(new String(buffer, 0, rb));
1294 					}
1295 				}
1296 			} catch (final Exception e) {
1297 				e.printStackTrace();
1298 			}
1299 
1300 			// parse output
1301 			final String[] lines = s.toString().split("\n");
1302 			for (String line : lines) {
1303 				if (!line.toLowerCase(Locale.US).contains("asec")) {
1304 					log.d("mount entry: " + line);
1305 					if (line.matches(reg) || line.matches(reg2)) {
1306 						String[] parts = line.split(" ");
1307 						for (String part : parts) {
1308 							if (part.startsWith("/"))
1309 								if (!part.toLowerCase(Locale.US).contains("vold"))
1310 									out.add(part);
1311 						}
1312 					}
1313 				}
1314 			}
1315 		} catch (Exception e) {
1316 			// ignore
1317 			log.d("exception", e);
1318 		}
1319 		log.d("mount pathes: " + out);
1320 		return out;
1321 	}
1322 
readMountsFile()1323 	private static HashSet<String> readMountsFile() {
1324 		final HashSet<String> out = new HashSet<String>();
1325 		/*
1326 		 * Scan the /proc/mounts file and look for lines like this:
1327 		 * /dev/block/vold/179:1 /mnt/sdcard vfat
1328 		 * rw,dirsync,nosuid,nodev,noexec,
1329 		 * relatime,uid=1000,gid=1015,fmask=0602,dmask
1330 		 * =0602,allow_utime=0020,codepage
1331 		 * =cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
1332 		 *
1333 		 * When one is found, split it into its elements and then pull out the
1334 		 * path to the that mount point and add it to the arraylist
1335 		 */
1336 
1337 		try {
1338 			String s = loadFileUtf8(new File("/proc/mounts"));
1339 			if (s != null) {
1340 				String[] rows = s.split("\n");
1341 				for (String line : rows) {
1342 					if (line.startsWith("/dev/block/vold/") || line.startsWith("/dev/fuse ")) {
1343 						String[] lineElements = line.split(" ");
1344 						String element = lineElements[1];
1345 						if (element.startsWith("/"))
1346 							out.add(element);
1347 					}
1348 				}
1349 			}
1350 		} catch (Exception e) {
1351 			// ignore
1352 		}
1353 		return out;
1354 	}
1355 
1356 
initMountRoots()1357 	private static void initMountRoots() {
1358 
1359 		log.i("initMountRoots()");
1360 		HashSet<String> mountedPathsFromMountCmd = getExternalMounts();
1361 		HashSet<String> mountedPathsFromMountFile = readMountsFile();
1362 		HashSet<String> mountedPathsStorageDir = listStorageDir();
1363 		log.i("mountedPathsFromMountCmd: " + mountedPathsFromMountCmd);
1364 		log.i("mountedPathsFromMountFile: " + mountedPathsFromMountFile);
1365 		log.i("mountedPathsStorageDir: " + mountedPathsStorageDir);
1366 
1367 		Map<String, String> map = new LinkedHashMap<String, String>();
1368 
1369 		// standard external directory
1370 		String sdpath = DeviceInfo.EINK_NOOK ? "/media/" : Environment.getExternalStorageDirectory().getAbsolutePath();
1371 		// dirty fix
1372 		if ("/nand".equals(sdpath) && new File("/sdcard").isDirectory())
1373 			sdpath = "/sdcard";
1374 		//String sdlink = isLink(sdpath);
1375 		//if (sdlink != null)
1376 		//	sdpath = sdlink;
1377 
1378 		// main storage
1379 		addMountRoot(map, sdpath, "SD");
1380 
1381 		// retrieve list of mount points from system
1382 		String[] fstabLocations = new String[]{
1383 				"/system/etc/vold.conf",
1384 				"/system/etc/vold.fstab",
1385 				"/etc/vold.conf",
1386 				"/etc/vold.fstab",
1387 		};
1388 		String s = null;
1389 		String fstabFileName = null;
1390 		for (String fstabFile : fstabLocations) {
1391 			fstabFileName = fstabFile;
1392 			s = loadFileUtf8(new File(fstabFile));
1393 			if (s != null)
1394 				log.i("found fstab file " + fstabFile);
1395 		}
1396 		if (s == null)
1397 			log.w("fstab file not found");
1398 		if (s != null) {
1399 			String[] rows = s.split("\n");
1400 			int rulesFound = 0;
1401 			for (String row : rows) {
1402 				if (row != null && row.startsWith("dev_mount")) {
1403 					log.d("mount rule: " + row);
1404 					rulesFound++;
1405 					String[] cols = Utils.splitByWhitespace(row);
1406 					if (cols.length >= 5) {
1407 						String name = Utils.ntrim(cols[1]);
1408 						String point = Utils.ntrim(cols[2]);
1409 						String mode = Utils.ntrim(cols[3]);
1410 						String dev = Utils.ntrim(cols[4]);
1411 						if (Utils.empty(name) || Utils.empty(point) || Utils.empty(mode) || Utils.empty(dev))
1412 							continue;
1413 						String label = null;
1414 						boolean hasusb = dev.indexOf("usb") >= 0;
1415 						boolean hasmmc = dev.indexOf("mmc") >= 0;
1416 						log.i("*** mount point '" + name + "' *** " + point + "  (" + dev + ")");
1417 						if ("auto".equals(mode)) {
1418 							// assume AUTO is for externally automount devices
1419 							if (hasusb)
1420 								label = "USB Storage";
1421 							else if (hasmmc)
1422 								label = "External SD";
1423 							else
1424 								label = "External Storage";
1425 						} else {
1426 							if (hasmmc)
1427 								label = "Internal SD";
1428 							else
1429 								label = "Internal Storage";
1430 						}
1431 						if (!point.equals(sdpath)) {
1432 							// external SD
1433 							addMountRoot(map, point, label + " (" + point + ")");
1434 						}
1435 					}
1436 				}
1437 			}
1438 			if (rulesFound == 0)
1439 				log.w("mount point rules not found in " + fstabFileName);
1440 		}
1441 
1442 		// TODO: probably, hardcoded list is not necessary after /etc/vold parsing
1443 		String[] knownMountPoints = new String[]{
1444 				"/system/media/sdcard", // internal SD card on Nook
1445 				"/media",
1446 				"/nand",
1447 				"/PocketBook701", // internal SD card on PocketBook 701 IQ
1448 				"/mnt/extsd",
1449 				"/mnt/external1",
1450 				"/mnt/external_sd",
1451 				"/mnt/udisk",
1452 				"/mnt/sdcard2",
1453 				"/mnt/ext.sd",
1454 				"/ext.sd",
1455 				"/extsd",
1456 				"/storage/sdcard",
1457 				"/storage/sdcard0",
1458 				"/storage/sdcard1",
1459 				"/storage/sdcard2",
1460 				"/mnt/extSdCard",
1461 				"/sdcard",
1462 				"/sdcard2",
1463 				"/mnt/udisk",
1464 				"/sdcard-ext",
1465 				"/sd-ext",
1466 				"/mnt/external1",
1467 				"/mnt/external2",
1468 				"/mnt/sdcard1",
1469 				"/mnt/sdcard2",
1470 				"/mnt/usb_storage",
1471 				"/mnt/external_SD",
1472 				"/emmc",
1473 				"/external",
1474 				"/Removable/SD",
1475 				"/Removable/MicroSD",
1476 				"/Removable/USBDisk1",
1477 				"/storage/sdcard1",
1478 				"/mnt/sdcard/extStorages/SdCard",
1479 				"/storage/extSdCard",
1480 				"/storage/external_SD",
1481 		};
1482 		// collect mount points from all possible sources
1483 		HashSet<String> mountPointsToAdd = new HashSet<>();
1484 		mountPointsToAdd.addAll(Arrays.asList(knownMountPoints));
1485 		mountPointsToAdd.addAll(mountedPathsFromMountCmd);
1486 		mountPointsToAdd.addAll(mountedPathsFromMountFile);
1487 		mountPointsToAdd.addAll(mountedPathsStorageDir);
1488 		mountPointsToAdd.add(Environment.getExternalStorageDirectory().getAbsolutePath());
1489 		String storageList = System.getenv("SECONDARY_STORAGE");
1490 		if (storageList != null) {
1491 			for (String point : storageList.split(":")) {
1492 				if (point.startsWith("/"))
1493 					mountPointsToAdd.add(point);
1494 			}
1495 		}
1496 		// add mount points
1497 
1498 		for (String point : mountPointsToAdd) {
1499 			if (point == null)
1500 				continue;
1501 			point = point.trim();
1502 			if (point.length() == 0)
1503 				continue;
1504 			File dir = new File(point);
1505 			if (dir.isDirectory() && dir.canRead()) {
1506 				String[] files = dir.list();
1507 				if (files != null && files.length > 0) {
1508 					String link = isLink(point);
1509 					if (link != null) {
1510 						log.d("found mount point path is link: " + point + " > " + link);
1511 						addMountRoot(map, link, link);
1512 					} else {
1513 						addMountRoot(map, point, point);
1514 					}
1515 				}
1516 			}
1517 		}
1518 
1519 		// auto detection
1520 		//autoAddRoots(map, "/", SYSTEM_ROOT_PATHS);
1521 		//autoAddRoots(map, "/mnt", new String[] {});
1522 
1523 		mountedRootsMap = map;
1524 		Collection<File> list = new ArrayList<File>();
1525 
1526 		for (String f : map.keySet()) {
1527 			File path = new File(f);
1528 			list.add(path);
1529 		}
1530 
1531 		mountedRootsList = list.toArray(new File[]{});
1532 		pathCorrector = new MountPathCorrector(mountedRootsList);
1533 
1534 		for (String point : mountPointsToAdd) {
1535 			String link = isLink(point);
1536 			if (link != null) {
1537 				log.d("pathCorrector.addRootLink(" + point + ", " + link + ")");
1538 				pathCorrector.addRootLink(point, link);
1539 			}
1540 		}
1541 
1542 		Log.i("cr3", "Root list: " + list + ", root links: " + pathCorrector);
1543 
1544 
1545 		log.i("Mount ROOTS:");
1546 		for (String f : map.keySet()) {
1547 			File path = new File(f);
1548 			String label = map.get(f);
1549 			if (isStorageDir(f)) {
1550 				label = "SD";
1551 				map.put(f, label);
1552 			} else if (isExternalStorageDir(f)) {
1553 				label = "Ext SD";
1554 				map.put(f, label);
1555 			}
1556 
1557 			log.i("*** " + f + " '" + label + "' isDirectory=" + path.isDirectory() + " canRead=" + path.canRead() + " canWrite=" + path.canRead() + " isLink=" + isLink(f));
1558 		}
1559 
1560 //		testPathNormalization("/sdcard/books/test.fb2");
1561 //		testPathNormalization("/mnt/sdcard/downloads/test.fb2");
1562 //		testPathNormalization("/mnt/sd/dir/test.fb2");
1563 	}
1564 
1565 //	private void testPathNormalization(String path) {
1566 //		Log.i("cr3", "normalization: " + path + " => " + normalizePathUsingRootLinks(new File(path)));
1567 //	}
1568 
1569 
1570 	// public void waitTasksCompletion()
1571 	// {
1572 	// log.i("waiting for engine tasks completion");
1573 	// try {
1574 	// mExecutor.awaitTermination(0, TimeUnit.SECONDS);
1575 	// } catch (InterruptedException e) {
1576 	// // ignore
1577 	// }
1578 	// }
1579 
1580 	/**
1581 	 * Uninitialize engine.
1582 	 */
uninit()1583 	public void uninit() {
1584 //		log.i("Engine.uninit() is called for " + hashCode());
1585 //		if (initialized) {
1586 //			synchronized(this) {
1587 //				uninitInternal();
1588 //			}
1589 //			initialized = false;
1590 //		}
1591 		instance = null;
1592 		// to suppress further messages about data directory removed
1593 		// if activity destroyed but process is not unloaded from memory
1594 		// and if application data directory already exist at this point
1595 		if (null != CR3_SETTINGS_DIR_NAME)
1596 			DATADIR_IS_EXIST_AT_START = true;
1597 	}
1598 
finalize()1599 	protected void finalize() throws Throwable {
1600 		log.i("Engine.finalize() is called for " + hashCode());
1601 		// if ( initialized ) {
1602 		// //uninitInternal();
1603 		// initialized = false;
1604 		// }
1605 	}
1606 
findFonts()1607 	private static String[] findFonts() {
1608 		ArrayList<File> dirs = new ArrayList<File>();
1609 		File[] dataDirs = getDataDirectories("fonts", false, false);
1610 		for (File dir : dataDirs)
1611 			dirs.add(dir);
1612 		File[] rootDirs = getStorageDirectories(false);
1613 		for (File dir : rootDirs)
1614 			dirs.add(new File(dir, "fonts"));
1615 		dirs.add(new File(Environment.getRootDirectory(), "fonts"));
1616 		ArrayList<String> fontPaths = new ArrayList<>();
1617 		for (File fontDir : dirs) {
1618 			if (fontDir.isDirectory()) {
1619 				log.v("Scanning directory " + fontDir.getAbsolutePath()
1620 						+ " for font files");
1621 				// get font names
1622 				String[] fileList = fontDir.list((dir, filename) -> {
1623 					String lc = filename.toLowerCase();
1624 					return (lc.endsWith(".ttf") || lc.endsWith(".otf")
1625 							|| lc.endsWith(".pfb") || lc.endsWith(".pfa")
1626 							|| lc.endsWith(".ttc"))
1627 //								&& !filename.endsWith("Fallback.ttf")
1628 							;
1629 				});
1630 				// append path
1631 				for (String s : fileList) {
1632 					String pathName = new File(fontDir, s)
1633 							.getAbsolutePath();
1634 					fontPaths.add(pathName);
1635 					log.v("found font: " + pathName);
1636 				}
1637 			}
1638 		}
1639 		Collections.sort(fontPaths);
1640 		return fontPaths.toArray(new String[]{});
1641 	}
1642 
getFontsDirs()1643 	public static final ArrayList<String> getFontsDirs() {
1644 		HashMap<String, Integer> dirsCapacity = new HashMap<>();
1645 		ArrayList<File> dirs = new ArrayList<>();
1646 		File[] dataDirs = getDataDirectories("fonts", false, false);
1647 		for (File dir : dataDirs)
1648 			dirs.add(dir);
1649 		File[] rootDirs = getStorageDirectories(false);
1650 		for (File dir : rootDirs)
1651 			dirs.add(new File(dir, "fonts"));
1652 		dirs.add(new File(Environment.getRootDirectory(), "fonts"));
1653 		for (File fontDir : dirs) {
1654 			if (fontDir.isDirectory())
1655 				dirsCapacity.put(fontDir.getAbsolutePath(), Integer.valueOf(0));
1656 		}
1657 		String[] fontFileNameList = getFontFileNameList();
1658 		for (String fontFileName : fontFileNameList) {
1659 			log.d("enum registered font: " + fontFileName);
1660 			for (File fontDir : dirs) {
1661 				if (fontFileName.startsWith(fontDir.getAbsolutePath())) {
1662 					Integer prevCount = dirsCapacity.get(fontDir.getAbsolutePath());
1663 					if (null == prevCount)
1664 						prevCount = Integer.valueOf(0);
1665 					dirsCapacity.put(fontDir.getAbsolutePath(), new Integer(prevCount.intValue() + 1));
1666 				}
1667 			}
1668 		}
1669 		ArrayList<String> resArray = new ArrayList<>();
1670 		Map.Entry<String, Integer> entry;
1671 		Iterator<Map.Entry<String, Integer>> it = dirsCapacity.entrySet().iterator();
1672 		while (it.hasNext()) {
1673 			entry = it.next();
1674 			resArray.add(entry.getKey() + ": " + entry.getValue().toString() + " registered font(s)");
1675 		}
1676 		return resArray;
1677 	}
1678 
1679 	private String SO_NAME = "lib" + LIBRARY_NAME + ".so";
1680 //	private static boolean force_install_library = false;
1681 
installLibrary()1682 	private static void installLibrary() {
1683 		try {
1684 //			if (force_install_library)
1685 //				throw new Exception("forcing install");
1686 			// try loading library w/o manual installation
1687 			log.i("trying to load library " + LIBRARY_NAME
1688 					+ " w/o installation");
1689 			System.loadLibrary(LIBRARY_NAME);
1690 			// try invoke native method
1691 			//log.i("trying execute native method ");
1692 			//setHyphenationMethod(HYPH_NONE, new byte[] {});
1693 			log.i(LIBRARY_NAME + " loaded successfully");
1694 //		} catch (Exception ee) {
1695 //			log.i(SO_NAME + " not found using standard paths, will install manually");
1696 //			File sopath = mActivity.getDir("libs", Context.MODE_PRIVATE);
1697 //			File soname = new File(sopath, SO_NAME);
1698 //			try {
1699 //				sopath.mkdirs();
1700 //				File zip = new File(mActivity.getPackageCodePath());
1701 //				ZipFile zipfile = new ZipFile(zip);
1702 //				ZipEntry zipentry = zipfile.getEntry("lib/armeabi/" + SO_NAME);
1703 //				if (!soname.exists() || zipentry.getSize() != soname.length()) {
1704 //					InputStream is = zipfile.getInputStream(zipentry);
1705 //					OutputStream os = new FileOutputStream(soname);
1706 //					Log.i("cr3",
1707 //							"Installing JNI library "
1708 //									+ soname.getAbsolutePath());
1709 //					final int BUF_SIZE = 0x10000;
1710 //					byte[] buf = new byte[BUF_SIZE];
1711 //					int n;
1712 //					while ((n = is.read(buf)) > 0)
1713 //						os.write(buf, 0, n);
1714 //					is.close();
1715 //					os.close();
1716 //				} else {
1717 //					log.i("JNI library " + soname.getAbsolutePath()
1718 //							+ " is up to date");
1719 //				}
1720 //				System.load(soname.getAbsolutePath());
1721 //				//setHyphenationMethod(HYPH_NONE, new byte[] {});
1722 		} catch (Exception e) {
1723 			log.e("cannot install " + LIBRARY_NAME + " library", e);
1724 			throw new RuntimeException("Cannot load JNI library");
1725 //			}
1726 		}
1727 	}
1728 
1729 	// See ${topsrc}/crengine/include/lvrend.h
1730 	public static final int BLOCK_RENDERING_FLAGS_LEGACY = 0;
1731 	public static final int BLOCK_RENDERING_FLAGS_FLAT = 0x01031031;
1732 	public static final int BLOCK_RENDERING_FLAGS_BOOK = 0x1375131;
1733 	public static final int BLOCK_RENDERING_FLAGS_WEB = 0x7FFFFFFF;
1734 	/// Current version of DOM parsing engine (See lvtinydom.cpp)
1735 	public static int DOM_VERSION_CURRENT;
1736 
1737 	public static final BackgroundTextureInfo NO_TEXTURE = new BackgroundTextureInfo(
1738 			BackgroundTextureInfo.NO_TEXTURE_ID, "(SOLID COLOR)", 0);
1739 	private static final BackgroundTextureInfo[] internalTextures = {
1740 			NO_TEXTURE,
1741 			new BackgroundTextureInfo("bg_paper1", "Paper 1",
1742 					R.drawable.bg_paper1),
1743 			new BackgroundTextureInfo("bg_paper1_dark", "Paper 1 (dark)",
1744 					R.drawable.bg_paper1_dark),
1745 			new BackgroundTextureInfo("bg_paper2", "Paper 2",
1746 					R.drawable.bg_paper2),
1747 			new BackgroundTextureInfo("bg_paper2_dark", "Paper 2 (dark)",
1748 					R.drawable.bg_paper2_dark),
1749 			new BackgroundTextureInfo("tx_wood", "Wood",
1750 					DeviceInfo.getSDKLevel() == 3 ? R.drawable.tx_wood_v3 : R.drawable.tx_wood),
1751 			new BackgroundTextureInfo("tx_wood_dark", "Wood (dark)",
1752 					DeviceInfo.getSDKLevel() == 3 ? R.drawable.tx_wood_dark_v3 : R.drawable.tx_wood_dark),
1753 			new BackgroundTextureInfo("tx_fabric", "Fabric",
1754 					R.drawable.tx_fabric),
1755 			new BackgroundTextureInfo("tx_fabric_dark", "Fabric (dark)",
1756 					R.drawable.tx_fabric_dark),
1757 			new BackgroundTextureInfo("tx_fabric_indigo_fibre", "Fabric fibre",
1758 					R.drawable.tx_fabric_indigo_fibre),
1759 			new BackgroundTextureInfo("tx_fabric_indigo_fibre_dark",
1760 					"Fabric fibre (dark)",
1761 					R.drawable.tx_fabric_indigo_fibre_dark),
1762 			new BackgroundTextureInfo("tx_gray_sand", "Gray sand",
1763 					R.drawable.tx_gray_sand),
1764 			new BackgroundTextureInfo("tx_gray_sand_dark", "Gray sand (dark)",
1765 					R.drawable.tx_gray_sand_dark),
1766 			new BackgroundTextureInfo("tx_green_wall", "Green wall",
1767 					R.drawable.tx_green_wall),
1768 			new BackgroundTextureInfo("tx_green_wall_dark",
1769 					"Green wall (dark)", R.drawable.tx_green_wall_dark),
1770 			new BackgroundTextureInfo("tx_metal_red_light", "Metall red",
1771 					R.drawable.tx_metal_red_light),
1772 			new BackgroundTextureInfo("tx_metal_red_dark", "Metall red (dark)",
1773 					R.drawable.tx_metal_red_dark),
1774 			new BackgroundTextureInfo("tx_metall_copper", "Metall copper",
1775 					R.drawable.tx_metall_copper),
1776 			new BackgroundTextureInfo("tx_metall_copper_dark",
1777 					"Metall copper (dark)", R.drawable.tx_metall_copper_dark),
1778 			new BackgroundTextureInfo("tx_metall_old_blue", "Metall blue",
1779 					R.drawable.tx_metall_old_blue),
1780 			new BackgroundTextureInfo("tx_metall_old_blue_dark",
1781 					"Metall blue (dark)", R.drawable.tx_metall_old_blue_dark),
1782 			new BackgroundTextureInfo("tx_old_book", "Old book",
1783 					R.drawable.tx_old_book),
1784 			new BackgroundTextureInfo("tx_old_book_dark", "Old book (dark)",
1785 					R.drawable.tx_old_book_dark),
1786 			new BackgroundTextureInfo("tx_old_paper", "Old paper",
1787 					R.drawable.tx_old_paper),
1788 			new BackgroundTextureInfo("tx_old_paper_dark", "Old paper (dark)",
1789 					R.drawable.tx_old_paper_dark),
1790 			new BackgroundTextureInfo("tx_paper", "Paper", R.drawable.tx_paper),
1791 			new BackgroundTextureInfo("tx_paper_dark", "Paper (dark)",
1792 					R.drawable.tx_paper_dark),
1793 			new BackgroundTextureInfo("tx_rust", "Rust", R.drawable.tx_rust),
1794 			new BackgroundTextureInfo("tx_rust_dark", "Rust (dark)",
1795 					R.drawable.tx_rust_dark),
1796 			new BackgroundTextureInfo("tx_sand", "Sand", R.drawable.tx_sand),
1797 			new BackgroundTextureInfo("tx_sand_dark", "Sand (dark)",
1798 					R.drawable.tx_sand_dark),
1799 			new BackgroundTextureInfo("tx_stones", "Stones",
1800 					R.drawable.tx_stones),
1801 			new BackgroundTextureInfo("tx_stones_dark", "Stones (dark)",
1802 					R.drawable.tx_stones_dark),};
1803 	public static final String DEF_DAY_BACKGROUND_TEXTURE = "bg_paper1";
1804 	public static final String DEF_NIGHT_BACKGROUND_TEXTURE = "bg_paper1_dark";
1805 
getAvailableTextures()1806 	public BackgroundTextureInfo[] getAvailableTextures() {
1807 		ArrayList<BackgroundTextureInfo> list = new ArrayList<BackgroundTextureInfo>(
1808 				internalTextures.length);
1809 		list.add(NO_TEXTURE);
1810 		findExternalTextures(list);
1811 		for (int i = 1; i < internalTextures.length; i++)
1812 			list.add(internalTextures[i]);
1813 		return list.toArray(new BackgroundTextureInfo[]{});
1814 	}
1815 
findHyphDictionariesFromDirectory(File dir)1816 	public static void findHyphDictionariesFromDirectory(File dir) {
1817 		for (File f : dir.listFiles()) {
1818 			if (f.isFile()) {
1819 				if (HyphDict.fromFile(f))
1820 					log.i("Registered external hyphenation dict " + f.getAbsolutePath());
1821 			}
1822 		}
1823 	}
1824 
findExternalHyphDictionaries()1825 	public static void findExternalHyphDictionaries() {
1826 		for (File d : getStorageDirectories(false)) {
1827 			File base = new File(d, ".cr3");
1828 			if (!base.isDirectory())
1829 				base = new File(d, "cr3");
1830 			if (!base.isDirectory())
1831 				continue;
1832 			File subdir = new File(base, "hyph");
1833 			if (subdir.isDirectory())
1834 				findHyphDictionariesFromDirectory(subdir);
1835 		}
1836 	}
1837 
findTexturesFromDirectory(File dir, Collection<BackgroundTextureInfo> listToAppend)1838 	public void findTexturesFromDirectory(File dir,
1839 										  Collection<BackgroundTextureInfo> listToAppend) {
1840 		for (File f : dir.listFiles()) {
1841 			if (f.isFile()) {
1842 				BackgroundTextureInfo item = BackgroundTextureInfo.fromFile(f
1843 						.getAbsolutePath());
1844 				if (item != null)
1845 					listToAppend.add(item);
1846 			}
1847 		}
1848 	}
1849 
findExternalTextures( Collection<BackgroundTextureInfo> listToAppend)1850 	public void findExternalTextures(
1851 			Collection<BackgroundTextureInfo> listToAppend) {
1852 		for (File d : getStorageDirectories(false)) {
1853 			File base = new File(d, ".cr3");
1854 			if (!base.isDirectory())
1855 				base = new File(d, "cr3");
1856 			if (!base.isDirectory())
1857 				continue;
1858 			File subdirTextures = new File(base, "textures");
1859 			File subdirBackgrounds = new File(base, "backgrounds");
1860 			if (subdirTextures.isDirectory())
1861 				findTexturesFromDirectory(subdirTextures, listToAppend);
1862 			if (subdirBackgrounds.isDirectory())
1863 				findTexturesFromDirectory(subdirBackgrounds, listToAppend);
1864 		}
1865 	}
1866 
1867 	enum DataDirType {
1868 		TexturesDirs,
1869 		BackgroundsDirs,
1870 		HyphsDirs
1871 	}
1872 
getDataDirs(DataDirType dirType)1873 	public static ArrayList<String> getDataDirs(DataDirType dirType) {
1874 		ArrayList<String> res = new ArrayList<String>();
1875 		for (File d : getStorageDirectories(false)) {
1876 			File base = new File(d, ".cr3");
1877 			if (!base.isDirectory())
1878 				base = new File(d, "cr3");
1879 			if (!base.isDirectory())
1880 				continue;
1881 			switch (dirType) {
1882 				case TexturesDirs:
1883 					File subdirTextures = new File(base, "textures");
1884 					if (subdirTextures.isDirectory())
1885 						res.add(subdirTextures.getAbsolutePath());
1886 					else
1887 						res.add(subdirTextures.getAbsolutePath() + " [not found]");
1888 					break;
1889 				case BackgroundsDirs:
1890 					File subdirBackgrounds = new File(base, "backgrounds");
1891 					if (subdirBackgrounds.isDirectory())
1892 						res.add(subdirBackgrounds.getAbsolutePath());
1893 					else
1894 						res.add(subdirBackgrounds.getAbsolutePath() + " [not found]");
1895 					break;
1896 				case HyphsDirs:
1897 					File subdirHyph = new File(base, "hyph");
1898 					if (subdirHyph.isDirectory())
1899 						res.add(subdirHyph.getAbsolutePath());
1900 					else
1901 						res.add(subdirHyph.getAbsolutePath() + " [not found]");
1902 					break;
1903 			}
1904 		}
1905 		return res;
1906 	}
1907 
getImageData(BackgroundTextureInfo texture)1908 	public byte[] getImageData(BackgroundTextureInfo texture) {
1909 		if (texture.isNone())
1910 			return null;
1911 		if (texture.resourceId != 0) {
1912 			byte[] data = loadResourceBytes(texture.resourceId);
1913 			return data;
1914 		} else if (texture.id != null && texture.id.startsWith("/")) {
1915 			File f = new File(texture.id);
1916 			byte[] data = loadResourceBytes(f);
1917 			return data;
1918 		}
1919 		return null;
1920 	}
1921 
getTextureInfoById(String id)1922 	public BackgroundTextureInfo getTextureInfoById(String id) {
1923 		if (id == null)
1924 			return NO_TEXTURE;
1925 		if (id.startsWith("/")) {
1926 			BackgroundTextureInfo item = BackgroundTextureInfo.fromFile(id);
1927 			if (item != null)
1928 				return item;
1929 		} else {
1930 			for (BackgroundTextureInfo item : internalTextures)
1931 				if (item.id.equals(id))
1932 					return item;
1933 		}
1934 		return NO_TEXTURE;
1935 	}
1936 
1937 	/**
1938 	 * Create progress dialog control.
1939 	 *
1940 	 * @param resourceId is string resource Id of dialog title, 0 to disable progress
1941 	 * @return created control object.
1942 	 */
createProgress(int resourceId)1943 	public ProgressControl createProgress(int resourceId) {
1944 		return new ProgressControl(resourceId);
1945 	}
1946 
1947 	private static final int PROGRESS_UPDATE_INTERVAL = DeviceInfo.EINK_SCREEN ? 4000 : 500;
1948 	private static final int PROGRESS_SHOW_INTERVAL = DeviceInfo.EINK_SCREEN ? 4000 : 1500;
1949 
1950 	public class ProgressControl {
1951 		private final int resourceId;
1952 		private long createTime = Utils.timeStamp();
1953 		private long lastUpdateTime;
1954 		private boolean shown;
1955 
ProgressControl(int resourceId)1956 		private ProgressControl(int resourceId) {
1957 			this.resourceId = resourceId;
1958 		}
1959 
hide()1960 		public void hide() {
1961 			if (resourceId == 0)
1962 				return; // disabled
1963 			if (shown)
1964 				hideProgress();
1965 			shown = false;
1966 		}
1967 
setProgress(int percent)1968 		public void setProgress(int percent) {
1969 			if (resourceId == 0)
1970 				return; // disabled
1971 			if (Utils.timeInterval(createTime) < PROGRESS_SHOW_INTERVAL)
1972 				return;
1973 			if (Utils.timeInterval(lastUpdateTime) < PROGRESS_UPDATE_INTERVAL)
1974 				return;
1975 			shown = true;
1976 			lastUpdateTime = Utils.timeStamp();
1977 			showProgress(percent, resourceId);
1978 		}
1979 	}
1980 
getPathCorrector()1981 	public MountPathCorrector getPathCorrector() {
1982 		return pathCorrector;
1983 	}
1984 
1985 	private static File[] mountedRootsList;
1986 	private static Map<String, String> mountedRootsMap;
1987 	private static MountPathCorrector pathCorrector;
1988 	private static String[] mFonts;
1989 	public static boolean DATADIR_IS_EXIST_AT_START = false;
1990 
1991 	// static initialization
1992 	static {
1993 		log.i("Engine() : static initialization");
installLibrary()1994 		installLibrary();
initMountRoots()1995 		initMountRoots();
1996 		File[] dataDirs = Engine.getDataDirectories(null, false, true);
1997 		if (dataDirs != null && dataDirs.length > 0) {
1998 			log.i("Engine() : DataDir exist at start.");
1999 			DATADIR_IS_EXIST_AT_START = true;
2000 		} else {
2001 			log.i("Engine() : DataDir NOT exist at start.");
2002 		}
2003 		mFonts = findFonts();
findExternalHyphDictionaries()2004 		findExternalHyphDictionaries();
2005 		if (!initInternal(mFonts, DeviceInfo.getSDKLevel())) {
2006 			log.i("Engine.initInternal failed!");
2007 			throw new RuntimeException("Cannot initialize CREngine JNI");
2008 		}
HyphDict.values()2009 		initDictionaries(HyphDict.values());
initCacheDirectory()2010 		initCacheDirectory();
2011 		DOM_VERSION_CURRENT = getDomVersionCurrent();
2012 		log.i("Engine() : initialization done");
2013 	}
2014 }
2015