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