1 package org.coolreader.crengine; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.text.SimpleDateFormat; 8 import java.util.ArrayList; 9 import java.util.HashSet; 10 import java.util.List; 11 import java.util.Locale; 12 import java.util.Map; 13 import java.util.concurrent.Callable; 14 15 import org.coolreader.CoolReader; 16 import org.coolreader.R; 17 import org.coolreader.crengine.InputDialog.InputHandler; 18 import org.koekak.android.ebookdownloader.SonyBookSelector; 19 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.ColorFilter; 27 import android.graphics.Paint; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.text.ClipboardManager; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.view.GestureDetector; 36 import android.view.GestureDetector.SimpleOnGestureListener; 37 import android.view.KeyEvent; 38 import android.view.MotionEvent; 39 import android.view.SurfaceHolder; 40 import android.view.SurfaceView; 41 import android.view.View; 42 import android.view.View.OnFocusChangeListener; 43 import android.view.View.OnKeyListener; 44 import android.view.View.OnTouchListener; 45 46 public class ReaderView implements android.view.SurfaceHolder.Callback, Settings, DocProperties, OnKeyListener, OnTouchListener, OnFocusChangeListener { 47 48 public static final Logger log = L.create("rv", Log.VERBOSE); 49 public static final Logger alog = L.create("ra", Log.WARN); 50 51 private final SurfaceView surface; 52 private final BookView bookView; 53 getSurface()54 public SurfaceView getSurface() { 55 return surface; 56 } 57 58 public interface BookView { draw()59 void draw(); 60 draw(boolean isPartially)61 void draw(boolean isPartially); 62 invalidate()63 void invalidate(); 64 onPause()65 void onPause(); 66 onResume()67 void onResume(); 68 } 69 70 public class ReaderSurface extends SurfaceView implements BookView { 71 ReaderSurface(Context context)72 public ReaderSurface(Context context) { 73 super(context); 74 // TODO Auto-generated constructor stub 75 } 76 77 @Override onPause()78 public void onPause() { 79 80 } 81 82 @Override onResume()83 public void onResume() { 84 85 } 86 87 @Override onDraw(Canvas canvas)88 protected void onDraw(Canvas canvas) { 89 try { 90 log.d("onDraw() called"); 91 draw(); 92 } catch (Exception e) { 93 log.e("exception while drawing", e); 94 } 95 } 96 97 @Override onDetachedFromWindow()98 protected void onDetachedFromWindow() { 99 super.onDetachedFromWindow(); 100 log.d("View.onDetachedFromWindow() is called"); 101 } 102 103 @Override onTrackballEvent(MotionEvent event)104 public boolean onTrackballEvent(MotionEvent event) { 105 log.d("onTrackballEvent(" + event + ")"); 106 if (mSettings.getBool(PROP_APP_TRACKBALL_DISABLED, false)) { 107 log.d("trackball is disabled in settings"); 108 return true; 109 } 110 mActivity.onUserActivity(); 111 return super.onTrackballEvent(event); 112 } 113 114 @Override onSizeChanged(final int w, final int h, int oldw, int oldh)115 protected void onSizeChanged(final int w, final int h, int oldw, int oldh) { 116 log.i("onSizeChanged(" + w + ", " + h + ")" + " activity.isDialogActive=" + getActivity().isDialogActive()); 117 super.onSizeChanged(w, h, oldw, oldh); 118 requestResize(w, h); 119 } 120 121 @Override onWindowVisibilityChanged(int visibility)122 public void onWindowVisibilityChanged(int visibility) { 123 if (visibility == VISIBLE) { 124 if (DeviceInfo.EINK_SCREEN) 125 mEinkScreen.refreshScreen(surface); 126 startStats(); 127 checkSize(); 128 } else 129 stopStats(); 130 super.onWindowVisibilityChanged(visibility); 131 } 132 133 @Override onWindowFocusChanged(boolean hasWindowFocus)134 public void onWindowFocusChanged(boolean hasWindowFocus) { 135 if (hasWindowFocus) { 136 if (DeviceInfo.EINK_SCREEN) 137 BackgroundThread.instance().postGUI(() -> mEinkScreen.refreshScreen(surface), 400); 138 startStats(); 139 checkSize(); 140 } else 141 stopStats(); 142 super.onWindowFocusChanged(hasWindowFocus); 143 } 144 doDraw(Canvas canvas)145 protected void doDraw(Canvas canvas) { 146 try { 147 log.d("doDraw() called"); 148 if (isProgressActive()) { 149 log.d("onDraw() -- drawing progress " + (currentProgressPosition / 100)); 150 drawPageBackground(canvas); 151 doDrawProgress(canvas, currentProgressPosition, currentProgressTitle); 152 } else if (mInitialized && mCurrentPageInfo != null && mCurrentPageInfo.bitmap != null) { 153 log.d("onDraw() -- drawing page image"); 154 155 if (currentAutoScrollAnimation != null) { 156 currentAutoScrollAnimation.draw(canvas); 157 } else if (currentAnimation != null) { 158 currentAnimation.draw(canvas); 159 } else { 160 Rect dst = new Rect(0, 0, canvas.getWidth(), canvas.getHeight()); 161 Rect src = new Rect(0, 0, mCurrentPageInfo.bitmap.getWidth(), mCurrentPageInfo.bitmap.getHeight()); 162 if (dontStretchWhileDrawing) { 163 if (dst.right > src.right) 164 dst.right = src.right; 165 if (dst.bottom > src.bottom) 166 dst.bottom = src.bottom; 167 if (src.right > dst.right) 168 src.right = dst.right; 169 if (src.bottom > dst.bottom) 170 src.bottom = dst.bottom; 171 if (centerPageInsteadOfResizing) { 172 int ddx = (canvas.getWidth() - dst.width()) / 2; 173 int ddy = (canvas.getHeight() - dst.height()) / 2; 174 dst.left += ddx; 175 dst.right += ddx; 176 dst.top += ddy; 177 dst.bottom += ddy; 178 } 179 } 180 if (dst.width() != canvas.getWidth() || dst.height() != canvas.getHeight()) 181 canvas.drawColor(Color.rgb(32, 32, 32)); 182 drawDimmedBitmap(canvas, mCurrentPageInfo.bitmap, src, dst); 183 } 184 if (isCloudSyncProgressActive()) { 185 // draw progressbar on top 186 doDrawCloudSyncProgress(canvas, currentCloudSyncProgressPosition); 187 } 188 } else { 189 log.d("onDraw() -- drawing empty screen"); 190 drawPageBackground(canvas); 191 if (isCloudSyncProgressActive()) { 192 // draw progressbar on top 193 doDrawCloudSyncProgress(canvas, currentCloudSyncProgressPosition); 194 } 195 } 196 } catch (Exception e) { 197 log.e("exception while drawing", e); 198 } 199 } 200 201 @Override draw()202 public void draw() { 203 draw(false); 204 } 205 206 @Override draw(boolean isPartially)207 public void draw(boolean isPartially) { 208 drawCallback(this::doDraw, null, isPartially); 209 } 210 211 @Override invalidate()212 public void invalidate() { 213 super.invalidate(); 214 } 215 216 } 217 218 private DocView doc; 219 220 // additional key codes for Nook 221 public static final int NOOK_KEY_PREV_LEFT = 96; 222 public static final int NOOK_KEY_PREV_RIGHT = 98; 223 public static final int NOOK_KEY_NEXT_RIGHT = 97; 224 public static final int NOOK_KEY_SHIFT_UP = 101; 225 public static final int NOOK_KEY_SHIFT_DOWN = 100; 226 227 // nook 1 & 2 228 public static final int NOOK_12_KEY_NEXT_LEFT = 95; 229 230 // Nook touch buttons 231 public static final int KEYCODE_PAGE_BOTTOMLEFT = 0x5d; // fwd = 93 ( 232 // public static final int KEYCODE_PAGE_BOTTOMRIGHT = 158; // 0x5f; // fwd = 95 233 public static final int KEYCODE_PAGE_TOPLEFT = 0x5c; // back = 92 234 public static final int KEYCODE_PAGE_TOPRIGHT = 0x5e; // back = 94 235 236 public static final int SONY_DPAD_UP_SCANCODE = 105; 237 public static final int SONY_DPAD_DOWN_SCANCODE = 106; 238 public static final int SONY_DPAD_LEFT_SCANCODE = 125; 239 public static final int SONY_DPAD_RIGHT_SCANCODE = 126; 240 241 public static final int KEYCODE_ESCAPE = 111; // KeyEvent constant since API 11 242 243 // public static final int SONY_MENU_SCANCODE = 357; 244 // public static final int SONY_BACK_SCANCODE = 158; 245 // public static final int SONY_HOME_SCANCODE = 102; 246 247 public static final int PAGE_ANIMATION_NONE = 0; 248 public static final int PAGE_ANIMATION_PAPER = 1; 249 public static final int PAGE_ANIMATION_SLIDE = 2; 250 public static final int PAGE_ANIMATION_SLIDE2 = 3; 251 public static final int PAGE_ANIMATION_MAX = 3; 252 253 public static final int SEL_CMD_SELECT_FIRST_SENTENCE_ON_PAGE = 1; 254 public static final int SEL_CMD_NEXT_SENTENCE = 2; 255 public static final int SEL_CMD_PREV_SENTENCE = 3; 256 257 // Double tap selections within this radius are are assumed to be attempts to select a single point 258 public static final int DOUBLE_TAP_RADIUS = 60; 259 260 private final static int BRIGHTNESS_TYPE_COMMON = 0; 261 private final static int BRIGHTNESS_TYPE_WARM = 1; 262 263 private ViewMode viewMode = ViewMode.PAGES; 264 execute(Engine.EngineTask task)265 private void execute(Engine.EngineTask task) { 266 mEngine.execute(task); 267 } 268 post(Engine.EngineTask task)269 private void post(Engine.EngineTask task) { 270 mEngine.post(task); 271 } 272 273 private abstract class Task implements Engine.EngineTask { 274 done()275 public void done() { 276 // override to do something useful 277 } 278 fail(Exception e)279 public void fail(Exception e) { 280 // do nothing, just log exception 281 // override to do custom action 282 log.e("Task " + this.getClass().getSimpleName() + " is failed with exception " + e.getMessage(), e); 283 } 284 } 285 286 static class Sync<T> extends Object { 287 private volatile T result = null; 288 private volatile boolean completed = false; 289 set(T res)290 public void set(T res) { 291 log.d("sync.set() called from " + Thread.currentThread().getName()); 292 result = res; 293 completed = true; 294 synchronized (this) { 295 notify(); 296 } 297 log.d("sync.set() returned from notify " + Thread.currentThread().getName()); 298 } 299 get()300 public T get() { 301 log.d("sync.get() called from " + Thread.currentThread().getName()); 302 while (!completed) { 303 try { 304 log.d("sync.get() before wait " + Thread.currentThread().getName()); 305 synchronized (this) { 306 if (!completed) 307 wait(); 308 } 309 log.d("sync.get() after wait wait " + Thread.currentThread().getName()); 310 } catch (InterruptedException e) { 311 log.d("sync.get() exception", e); 312 // ignore 313 } catch (Exception e) { 314 log.d("sync.get() exception", e); 315 // ignore 316 } 317 } 318 log.d("sync.get() returning " + Thread.currentThread().getName()); 319 return result; 320 } 321 } 322 323 private final CoolReader mActivity; 324 private final Engine mEngine; 325 private final EinkScreen mEinkScreen; 326 327 private BookInfo mBookInfo; 328 329 private Properties mSettings = new Properties(); 330 getEngine()331 public Engine getEngine() { 332 return mEngine; 333 } 334 getActivity()335 public CoolReader getActivity() { 336 return mActivity; 337 } 338 339 private int lastResizeTaskId = 0; 340 isBookLoaded()341 public boolean isBookLoaded() { 342 return mOpened; 343 } 344 getOrientation()345 public int getOrientation() { 346 int angle = mSettings.getInt(PROP_APP_SCREEN_ORIENTATION, 0); 347 if (angle == 4) 348 angle = mActivity.getOrientationFromSensor(); 349 return angle; 350 } 351 overrideKey(int keyCode)352 private int overrideKey(int keyCode) { 353 return keyCode; 354 } 355 getTapZone(int x, int y, int dx, int dy)356 public int getTapZone(int x, int y, int dx, int dy) { 357 int x1 = dx / 3; 358 int x2 = dx * 2 / 3; 359 int y1 = dy / 3; 360 int y2 = dy * 2 / 3; 361 int zone = 0; 362 if (y < y1) { 363 if (x < x1) 364 zone = 1; 365 else if (x < x2) 366 zone = 2; 367 else 368 zone = 3; 369 } else if (y < y2) { 370 if (x < x1) 371 zone = 4; 372 else if (x < x2) 373 zone = 5; 374 else 375 zone = 6; 376 } else { 377 if (x < x1) 378 zone = 7; 379 else if (x < x2) 380 zone = 8; 381 else 382 zone = 9; 383 } 384 return zone; 385 } 386 findTapZoneAction(int zone, int tapActionType)387 public ReaderAction findTapZoneAction(int zone, int tapActionType) { 388 ReaderAction action = ReaderAction.NONE; 389 boolean isSecondaryAction = (secondaryTapActionType == tapActionType); 390 if (tapActionType == TAP_ACTION_TYPE_SHORT) { 391 action = ReaderAction.findForTap(zone, mSettings); 392 } else { 393 if (isSecondaryAction) 394 action = ReaderAction.findForLongTap(zone, mSettings); 395 else if (doubleTapSelectionEnabled || tapActionType == TAP_ACTION_TYPE_LONGPRESS) 396 action = ReaderAction.START_SELECTION; 397 } 398 return action; 399 } 400 getOpenedFileInfo()401 public FileInfo getOpenedFileInfo() { 402 if (isBookLoaded() && mBookInfo != null) 403 return mBookInfo.getFileInfo(); 404 return null; 405 } 406 407 public final int LONG_KEYPRESS_TIME = 900; 408 public final int AUTOREPEAT_KEYPRESS_TIME = 700; 409 public final int DOUBLE_CLICK_INTERVAL = 400; 410 private ReaderAction currentDoubleClickAction = null; 411 private ReaderAction currentSingleClickAction = null; 412 private long currentDoubleClickActionStart = 0; 413 private int currentDoubleClickActionKeyCode = 0; 414 // boolean VOLUME_KEYS_ZOOM = false; 415 416 //private boolean backKeyDownHere = false; 417 418 419 private long statStartTime; 420 private long statTimeElapsed; 421 startStats()422 public void startStats() { 423 if (statStartTime == 0) { 424 statStartTime = android.os.SystemClock.uptimeMillis(); 425 log.d("stats: started reading"); 426 } 427 } 428 stopStats()429 public void stopStats() { 430 if (statStartTime > 0) { 431 statTimeElapsed += android.os.SystemClock.uptimeMillis() - statStartTime; 432 statStartTime = 0; 433 log.d("stats: stopped reading"); 434 } 435 } 436 getTimeElapsed()437 public long getTimeElapsed() { 438 if (statStartTime > 0) 439 return statTimeElapsed + android.os.SystemClock.uptimeMillis() - statStartTime; 440 else 441 return statTimeElapsed++; 442 } 443 setTimeElapsed(long timeElapsed)444 public void setTimeElapsed(long timeElapsed) { 445 statTimeElapsed = timeElapsed; 446 } 447 onAppPause()448 public void onAppPause() { 449 stopTracking(); 450 if (currentAutoScrollAnimation != null) 451 stopAutoScroll(); 452 Bookmark bmk = getCurrentPositionBookmark(); 453 if (bmk != null) 454 savePositionBookmark(bmk); 455 log.i("calling bookView.onPause()"); 456 bookView.onPause(); 457 } 458 459 private long lastAppResumeTs = 0; 460 onAppResume()461 public void onAppResume() { 462 lastAppResumeTs = System.currentTimeMillis(); 463 log.i("calling bookView.onResume()"); 464 bookView.onResume(); 465 } 466 startTrackingKey(KeyEvent event)467 private boolean startTrackingKey(KeyEvent event) { 468 if (event.getRepeatCount() == 0) { 469 stopTracking(); 470 trackedKeyEvent = event; 471 return true; 472 } 473 return false; 474 } 475 stopTracking()476 private void stopTracking() { 477 trackedKeyEvent = null; 478 actionToRepeat = null; 479 repeatActionActive = false; 480 if (currentTapHandler != null) 481 currentTapHandler.cancel(); 482 } 483 isTracked(KeyEvent event)484 private boolean isTracked(KeyEvent event) { 485 if (trackedKeyEvent != null) { 486 int tkeKc = trackedKeyEvent.getKeyCode(); 487 int eKc = event.getKeyCode(); 488 // check if tracked key and current key are the same 489 if (tkeKc == eKc) { 490 long tkeDt = trackedKeyEvent.getDownTime(); 491 long eDt = event.getDownTime(); 492 // empirical value (could be changed or moved to constant) 493 long delta = 300l; 494 // time difference between tracked and current event 495 long diff = eDt - tkeDt; 496 // needed for correct function on HTC Desire for CENTER_KEY 497 if (delta > diff) 498 return true; 499 } else { 500 log.v("isTracked( trackedKeyEvent=" + trackedKeyEvent + ", event=" + event + " )"); 501 } 502 } 503 stopTracking(); 504 return false; 505 } 506 507 508 private KeyEvent trackedKeyEvent = null; 509 private ReaderAction actionToRepeat = null; 510 private boolean repeatActionActive = false; 511 private SparseArray<Long> keyDownTimestampMap = new SparseArray<Long>(); 512 translateKeyCode(int keyCode)513 private int translateKeyCode(int keyCode) { 514 if (DeviceInfo.REVERT_LANDSCAPE_VOLUME_KEYS && (mActivity.getScreenOrientation() & 1) != 0) { 515 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) 516 return KeyEvent.KEYCODE_VOLUME_UP; 517 if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) 518 return KeyEvent.KEYCODE_VOLUME_DOWN; 519 } 520 return keyCode; 521 } 522 523 private int nextUpdateId = 0; 524 updateSelection(int startX, int startY, int endX, int endY, final boolean isUpdateEnd)525 private void updateSelection(int startX, int startY, int endX, int endY, final boolean isUpdateEnd) { 526 final Selection sel = new Selection(); 527 final int myId = ++nextUpdateId; 528 sel.startX = startX; 529 sel.startY = startY; 530 sel.endX = endX; 531 sel.endY = endY; 532 mEngine.execute(new Task() { 533 @Override 534 public void work() throws Exception { 535 if (myId != nextUpdateId && !isUpdateEnd) 536 return; 537 doc.updateSelection(sel); 538 if (!sel.isEmpty()) { 539 invalidImages = true; 540 BitmapInfo bi = preparePageImage(0); 541 if (bi != null) { 542 bookView.draw(true); 543 } 544 } 545 } 546 547 @Override 548 public void done() { 549 if (isUpdateEnd) { 550 String text = sel.text; 551 if (text != null && text.length() > 0) { 552 onSelectionComplete(sel); 553 } else { 554 clearSelection(); 555 } 556 } 557 } 558 }); 559 } 560 isMultiSelection(Selection sel)561 public static boolean isMultiSelection(Selection sel) { 562 String str = sel.text; 563 if (str != null) { 564 for (int i = 0; i < str.length(); i++) { 565 if (Character.isWhitespace(str.charAt(i))) { 566 return true; 567 } 568 } 569 } 570 return false; 571 } 572 573 private int mSelectionAction = SELECTION_ACTION_TOOLBAR; 574 private int mMultiSelectionAction = SELECTION_ACTION_TOOLBAR; 575 onSelectionComplete(Selection sel)576 private void onSelectionComplete(Selection sel) { 577 int iSelectionAction; 578 iSelectionAction = isMultiSelection(sel) ? mMultiSelectionAction : mSelectionAction; 579 580 switch (iSelectionAction) { 581 case SELECTION_ACTION_TOOLBAR: 582 SelectionToolbarDlg.showDialog(mActivity, ReaderView.this, sel); 583 break; 584 case SELECTION_ACTION_COPY: 585 copyToClipboard(sel.text); 586 clearSelection(); 587 break; 588 case SELECTION_ACTION_DICTIONARY: 589 mActivity.findInDictionary(sel.text); 590 if (!getSettings().getBool(PROP_APP_SELECTION_PERSIST, false)) 591 clearSelection(); 592 break; 593 case SELECTION_ACTION_BOOKMARK: 594 clearSelection(); 595 showNewBookmarkDialog(sel); 596 break; 597 case SELECTION_ACTION_FIND: 598 clearSelection(); 599 showSearchDialog(sel.text); 600 break; 601 default: 602 clearSelection(); 603 break; 604 } 605 606 } 607 showNewBookmarkDialog(Selection sel)608 public void showNewBookmarkDialog(Selection sel) { 609 if (mBookInfo == null) 610 return; 611 Bookmark bmk = new Bookmark(); 612 bmk.setType(Bookmark.TYPE_COMMENT); 613 bmk.setPosText(sel.text); 614 bmk.setStartPos(sel.startPos); 615 bmk.setEndPos(sel.endPos); 616 bmk.setPercent(sel.percent); 617 bmk.setTitleText(sel.chapter); 618 BookmarkEditDialog dlg = new BookmarkEditDialog(mActivity, this, bmk, true); 619 dlg.show(); 620 } 621 sendQuotationInEmail(Selection sel)622 public void sendQuotationInEmail(Selection sel) { 623 StringBuilder buf = new StringBuilder(); 624 if (mBookInfo.getFileInfo().authors != null) 625 buf.append("|" + mBookInfo.getFileInfo().authors + "\n"); 626 if (mBookInfo.getFileInfo().title != null) 627 buf.append("|" + mBookInfo.getFileInfo().title + "\n"); 628 if (sel.chapter != null && sel.chapter.length() > 0) 629 buf.append("|" + sel.chapter + "\n"); 630 buf.append(sel.text + "\n"); 631 mActivity.sendBookFragment(mBookInfo, buf.toString()); 632 } 633 copyToClipboard(String text)634 public void copyToClipboard(String text) { 635 if (text != null && text.length() > 0) { 636 ClipboardManager cm = mActivity.getClipboardmanager(); 637 cm.setText(text); 638 log.i("Setting clipboard text: " + text); 639 mActivity.showToast("Selection text copied to clipboard"); 640 } 641 } 642 643 // private void cancelSelection() { 644 // // 645 // selectionInProgress = false; 646 // clearSelection(); 647 // } 648 649 private int isBacklightControlFlick = 1; 650 private int isWarmBacklightControlFlick = 2; 651 private boolean isTouchScreenEnabled = true; 652 // private boolean isManualScrollActive = false; 653 // private boolean isBrightnessControlActive = false; 654 // private int manualScrollStartPosX = -1; 655 // private int manualScrollStartPosY = -1; 656 // volatile private boolean touchEventIgnoreNextUp = false; 657 // volatile private int longTouchId = 0; 658 // volatile private long currentDoubleTapActionStart = 0; 659 // private boolean selectionInProgress = false; 660 // private int selectionStartX = 0; 661 // private int selectionStartY = 0; 662 // private int selectionEndX = 0; 663 // private int selectionEndY = 0; 664 private boolean doubleTapSelectionEnabled = false; 665 private int mGesturePageFlipsPerFullSwipe; 666 private boolean mIsPageMode; 667 private int secondaryTapActionType = TAP_ACTION_TYPE_LONGPRESS; 668 private boolean selectionModeActive = false; 669 toggleSelectionMode()670 public void toggleSelectionMode() { 671 selectionModeActive = !selectionModeActive; 672 mActivity.showToast(selectionModeActive ? R.string.action_toggle_selection_mode_on : R.string.action_toggle_selection_mode_off); 673 } 674 675 private ImageViewer currentImageViewer; 676 677 private class ImageViewer extends SimpleOnGestureListener { 678 private ImageInfo currentImage; 679 final GestureDetector detector; 680 int oldOrientation; 681 ImageViewer(ImageInfo image)682 public ImageViewer(ImageInfo image) { 683 lockOrientation(); 684 detector = new GestureDetector(this); 685 if (image.bufHeight / image.height >= 2 && image.bufWidth / image.width >= 2) { 686 image.scaledHeight *= 2; 687 image.scaledWidth *= 2; 688 } 689 centerIfLessThanScreen(image); 690 currentImage = image; 691 } 692 lockOrientation()693 private void lockOrientation() { 694 oldOrientation = mActivity.getScreenOrientation(); 695 if (oldOrientation == 4) 696 mActivity.setScreenOrientation(mActivity.getOrientationFromSensor()); 697 } 698 unlockOrientation()699 private void unlockOrientation() { 700 if (oldOrientation == 4) 701 mActivity.setScreenOrientation(oldOrientation); 702 } 703 centerIfLessThanScreen(ImageInfo image)704 private void centerIfLessThanScreen(ImageInfo image) { 705 if (image.scaledHeight < image.bufHeight) 706 image.y = (image.bufHeight - image.scaledHeight) / 2; 707 if (image.scaledWidth < image.bufWidth) 708 image.x = (image.bufWidth - image.scaledWidth) / 2; 709 } 710 fixScreenBounds(ImageInfo image)711 private void fixScreenBounds(ImageInfo image) { 712 if (image.scaledHeight > image.bufHeight) { 713 if (image.y < image.bufHeight - image.scaledHeight) 714 image.y = image.bufHeight - image.scaledHeight; 715 if (image.y > 0) 716 image.y = 0; 717 } 718 if (image.scaledWidth > image.bufWidth) { 719 if (image.x < image.bufWidth - image.scaledWidth) 720 image.x = image.bufWidth - image.scaledWidth; 721 if (image.x > 0) 722 image.x = 0; 723 } 724 } 725 updateImage(ImageInfo image)726 private void updateImage(ImageInfo image) { 727 centerIfLessThanScreen(image); 728 fixScreenBounds(image); 729 if (!currentImage.equals(image)) { 730 currentImage = image; 731 drawPage(); 732 } 733 } 734 zoomIn()735 public void zoomIn() { 736 ImageInfo image = new ImageInfo(currentImage); 737 if (image.scaledHeight >= image.height) { 738 int scale = image.scaledHeight / image.height; 739 if (scale < 4) 740 scale++; 741 image.scaledHeight = image.height * scale; 742 image.scaledWidth = image.width * scale; 743 } else { 744 int scale = image.height / image.scaledHeight; 745 if (scale > 1) 746 scale--; 747 image.scaledHeight = image.height / scale; 748 image.scaledWidth = image.width / scale; 749 } 750 updateImage(image); 751 } 752 zoomOut()753 public void zoomOut() { 754 ImageInfo image = new ImageInfo(currentImage); 755 if (image.scaledHeight > image.height) { 756 int scale = image.scaledHeight / image.height; 757 if (scale > 1) 758 scale--; 759 image.scaledHeight = image.height * scale; 760 image.scaledWidth = image.width * scale; 761 } else { 762 int scale = image.height / image.scaledHeight; 763 if (image.scaledHeight > image.bufHeight || image.scaledWidth > image.bufWidth) 764 scale++; 765 image.scaledHeight = image.height / scale; 766 image.scaledWidth = image.width / scale; 767 } 768 updateImage(image); 769 } 770 getStep()771 public int getStep() { 772 ImageInfo image = currentImage; 773 int max = image.bufHeight; 774 if (max < image.bufWidth) 775 max = image.bufWidth; 776 return max / 10; 777 } 778 moveBy(int dx, int dy)779 public void moveBy(int dx, int dy) { 780 ImageInfo image = new ImageInfo(currentImage); 781 image.x += dx; 782 image.y += dy; 783 updateImage(image); 784 } 785 onKeyDown(int keyCode, final KeyEvent event)786 public boolean onKeyDown(int keyCode, final KeyEvent event) { 787 if (keyCode == 0) 788 keyCode = event.getScanCode(); 789 switch (keyCode) { 790 case KeyEvent.KEYCODE_VOLUME_UP: 791 zoomIn(); 792 return true; 793 case KeyEvent.KEYCODE_VOLUME_DOWN: 794 zoomOut(); 795 return true; 796 case KeyEvent.KEYCODE_DPAD_CENTER: 797 case KeyEvent.KEYCODE_BACK: 798 case KeyEvent.KEYCODE_ENDCALL: 799 close(); 800 return true; 801 case KeyEvent.KEYCODE_DPAD_LEFT: 802 moveBy(getStep(), 0); 803 return true; 804 case KeyEvent.KEYCODE_DPAD_RIGHT: 805 moveBy(-getStep(), 0); 806 return true; 807 case KeyEvent.KEYCODE_DPAD_UP: 808 moveBy(0, getStep()); 809 return true; 810 case KeyEvent.KEYCODE_DPAD_DOWN: 811 moveBy(0, -getStep()); 812 return true; 813 } 814 return false; 815 } 816 onKeyUp(int keyCode, final KeyEvent event)817 public boolean onKeyUp(int keyCode, final KeyEvent event) { 818 if (keyCode == 0) 819 keyCode = event.getScanCode(); 820 switch (keyCode) { 821 case KeyEvent.KEYCODE_BACK: 822 case KeyEvent.KEYCODE_ENDCALL: 823 close(); 824 return true; 825 } 826 return false; 827 } 828 onTouchEvent(MotionEvent event)829 public boolean onTouchEvent(MotionEvent event) { 830 // int aindex = event.getActionIndex(); 831 // if (event.getAction() == MotionEvent.ACTION_POINTER_DOWN) { 832 // log.v("ACTION_POINTER_DOWN"); 833 // } 834 return detector.onTouchEvent(event); 835 } 836 837 838 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)839 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 840 float velocityY) { 841 log.v("onFling()"); 842 return true; 843 } 844 845 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)846 public boolean onScroll(MotionEvent e1, MotionEvent e2, 847 float distanceX, float distanceY) { 848 log.v("onScroll() " + distanceX + ", " + distanceY); 849 int dx = (int) distanceX; 850 int dy = (int) distanceY; 851 moveBy(-dx, -dy); 852 return true; 853 } 854 855 @Override onSingleTapConfirmed(MotionEvent e)856 public boolean onSingleTapConfirmed(MotionEvent e) { 857 log.v("onSingleTapConfirmed()"); 858 ImageInfo image = new ImageInfo(currentImage); 859 860 int x = (int) e.getX(); 861 int y = (int) e.getY(); 862 863 int zone = 0; 864 int zw = mActivity.getDensityDpi() / 2; 865 int w = image.bufWidth; 866 int h = image.bufHeight; 867 if (image.rotation == 0) { 868 if (x < zw && y > h - zw) 869 zone = 1; 870 if (x > w - zw && y > h - zw) 871 zone = 2; 872 } else { 873 if (x < zw && y < zw) 874 zone = 1; 875 if (x < zw && y > h - zw) 876 zone = 2; 877 } 878 if (zone != 0) { 879 if (zone == 1) 880 zoomIn(); 881 else 882 zoomOut(); 883 return true; 884 } 885 886 close(); 887 return super.onSingleTapConfirmed(e); 888 } 889 890 @Override onDown(MotionEvent e)891 public boolean onDown(MotionEvent e) { 892 return true; 893 } 894 close()895 public void close() { 896 if (currentImageViewer == null) 897 return; 898 currentImageViewer = null; 899 unlockOrientation(); 900 BackgroundThread.instance().postBackground(() -> doc.closeImage()); 901 drawPage(); 902 } 903 prepareImage()904 public BitmapInfo prepareImage() { 905 // called from background thread 906 ImageInfo img = currentImage; 907 img.bufWidth = internalDX; 908 img.bufHeight = internalDY; 909 if (mCurrentPageInfo != null) { 910 if (img.equals(mCurrentPageInfo.imageInfo)) 911 return mCurrentPageInfo; 912 mCurrentPageInfo.recycle(); 913 mCurrentPageInfo = null; 914 } 915 PositionProperties currpos = doc.getPositionProps(null, false); 916 BitmapInfo bi = new BitmapInfo(); 917 bi.imageInfo = new ImageInfo(img); 918 bi.bitmap = factory.get(internalDX, internalDY); 919 bi.position = currpos; 920 doc.drawImage(bi.bitmap, bi.imageInfo); 921 mCurrentPageInfo = bi; 922 return mCurrentPageInfo; 923 } 924 925 } 926 startImageViewer(ImageInfo image)927 private void startImageViewer(ImageInfo image) { 928 currentImageViewer = new ImageViewer(image); 929 drawPage(); 930 } 931 isImageViewMode()932 private boolean isImageViewMode() { 933 return currentImageViewer != null; 934 } 935 stopImageViewer()936 private void stopImageViewer() { 937 if (currentImageViewer != null) 938 currentImageViewer.close(); 939 } 940 941 private TapHandler currentTapHandler = null; 942 943 public class TapHandler { 944 945 private final static int STATE_INITIAL = 0; // no events yet 946 private final static int STATE_DOWN_1 = 1; // down first time 947 private final static int STATE_SELECTION = 3; // selection is started 948 private final static int STATE_FLIPPING = 4; // flipping is in progress 949 private final static int STATE_WAIT_FOR_DOUBLE_CLICK = 5; // flipping is in progress 950 private final static int STATE_DONE = 6; // done: no more tracking 951 private final static int STATE_BRIGHTNESS = 7; // brightness change in progress 952 private final static int STATE_FLIP_TRACKING = 8; // pages flip tracking in progress 953 954 private final static int EXPIRATION_TIME_MS = 180000; 955 956 int state = STATE_INITIAL; 957 int brightness_type = BRIGHTNESS_TYPE_COMMON; 958 959 int start_x = 0; 960 int start_y = 0; 961 int width = 0; 962 int height = 0; 963 ReaderAction shortTapAction = ReaderAction.NONE; 964 ReaderAction longTapAction = ReaderAction.NONE; 965 ReaderAction doubleTapAction = ReaderAction.NONE; 966 long firstDown; 967 968 /// handle unexpected event for state: stop tracking unexpectedEvent()969 private boolean unexpectedEvent() { 970 cancel(); 971 return true; // ignore 972 } 973 isInitialState()974 public boolean isInitialState() { 975 return state == STATE_INITIAL; 976 } 977 checkExpiration()978 public void checkExpiration() { 979 if (state != STATE_INITIAL && Utils.timeInterval(firstDown) > EXPIRATION_TIME_MS) 980 cancel(); 981 } 982 983 /// cancel current action and reset touch tracking state cancel()984 private boolean cancel() { 985 if (state == STATE_INITIAL) 986 return true; 987 switch (state) { 988 case STATE_DOWN_1: 989 case STATE_SELECTION: 990 clearSelection(); 991 break; 992 case STATE_FLIPPING: 993 stopAnimation(-1, -1); 994 break; 995 case STATE_WAIT_FOR_DOUBLE_CLICK: 996 case STATE_DONE: 997 case STATE_BRIGHTNESS: 998 case STATE_FLIP_TRACKING: 999 stopBrightnessControl(-1, -1, brightness_type); 1000 break; 1001 } 1002 state = STATE_DONE; 1003 unhiliteTapZone(); 1004 currentTapHandler = new TapHandler(); 1005 return true; 1006 } 1007 adjustStartValuesOnDrag(int swipeDistance, int distanceForFlip)1008 private void adjustStartValuesOnDrag(int swipeDistance, int distanceForFlip) { 1009 if (Math.abs(swipeDistance) < distanceForFlip) { 1010 return; // Nothing to do 1011 } 1012 int direction = swipeDistance > 0 ? 1 : -1; // Left-to-right or right-to-left swipe? 1013 int value = direction * distanceForFlip; 1014 while (Math.abs(swipeDistance) >= distanceForFlip) { 1015 if (mIsPageMode) { 1016 start_x += value; 1017 } else { 1018 start_y += value; 1019 } 1020 swipeDistance -= value; 1021 } 1022 } 1023 updatePageFlipTracking(final int x, final int y)1024 private void updatePageFlipTracking(final int x, final int y) { 1025 if (!mOpened) 1026 return; 1027 final int swipeDistance = mIsPageMode ? x - start_x : y - start_y; 1028 final int distanceForFlip = surface.getWidth() / mGesturePageFlipsPerFullSwipe; 1029 int pagesToFlip = swipeDistance / distanceForFlip; 1030 if (pagesToFlip == 0) { 1031 return; // Nothing to do 1032 } 1033 adjustStartValuesOnDrag(swipeDistance, distanceForFlip); 1034 ReaderAction action = pagesToFlip > 0 ? ReaderAction.PAGE_DOWN : ReaderAction.PAGE_UP; 1035 while (pagesToFlip != 0) { 1036 onAction(action); 1037 if (pagesToFlip > 0) { 1038 pagesToFlip--; 1039 } else { 1040 pagesToFlip++; 1041 } 1042 } 1043 } 1044 1045 /// perform action and reset touch tracking state performAction(final ReaderAction action, boolean checkForLinks)1046 private boolean performAction(final ReaderAction action, boolean checkForLinks) { 1047 log.d("performAction on touch: " + action); 1048 state = STATE_DONE; 1049 1050 currentTapHandler = new TapHandler(); 1051 1052 if (!checkForLinks) { 1053 onAction(action); 1054 return true; 1055 } 1056 1057 // check link before executing action 1058 mEngine.execute(new Task() { 1059 String link; 1060 ImageInfo image; 1061 Bookmark bookmark; 1062 1063 public void work() { 1064 image = new ImageInfo(); 1065 image.bufWidth = internalDX; 1066 image.bufHeight = internalDY; 1067 image.bufDpi = mActivity.getDensityDpi(); 1068 if (doc.checkImage(start_x, start_y, image)) { 1069 return; 1070 } 1071 image = null; 1072 link = doc.checkLink(start_x, start_y, mActivity.getPalmTipPixels() / 2); 1073 if (link != null) { 1074 if (link.startsWith("#")) { 1075 log.d("go to " + link); 1076 doc.goLink(link); 1077 drawPage(); 1078 } 1079 return; 1080 } 1081 bookmark = doc.checkBookmark(start_x, start_y); 1082 if (bookmark != null && bookmark.getType() == Bookmark.TYPE_POSITION) 1083 bookmark = null; 1084 } 1085 1086 public void done() { 1087 if (bookmark != null) 1088 bookmark = mBookInfo.findBookmark(bookmark); 1089 if (link == null && image == null && bookmark == null) { 1090 onAction(action); 1091 } else if (image != null) { 1092 startImageViewer(image); 1093 } else if (bookmark != null) { 1094 BookmarkEditDialog dlg = new BookmarkEditDialog(mActivity, ReaderView.this, bookmark, false); 1095 dlg.show(); 1096 } else if (!link.startsWith("#")) { 1097 log.d("external link " + link); 1098 if (link.startsWith("http://") || link.startsWith("https://")) { 1099 mActivity.openURL(link); 1100 } else { 1101 // absolute path to file 1102 FileInfo fi = new FileInfo(link); 1103 if (fi.exists()) { 1104 mActivity.loadDocument(fi, true); 1105 return; 1106 } 1107 File baseDir = null; 1108 if (mBookInfo != null && mBookInfo.getFileInfo() != null) { 1109 if (!mBookInfo.getFileInfo().isArchive) { 1110 // relatively to base directory 1111 File f = new File(mBookInfo.getFileInfo().getBasePath()); 1112 baseDir = f.getParentFile(); 1113 String url = link; 1114 while (baseDir != null && url != null && url.startsWith("../")) { 1115 baseDir = baseDir.getParentFile(); 1116 url = url.substring(3); 1117 } 1118 if (baseDir != null && url != null && url.length() > 0) { 1119 fi = new FileInfo(baseDir.getAbsolutePath() + "/" + url); 1120 if (fi.exists()) { 1121 mActivity.loadDocument(fi, true); 1122 return; 1123 } 1124 } 1125 } else { 1126 // from archive 1127 fi = new FileInfo(mBookInfo.getFileInfo().getArchiveName() + FileInfo.ARC_SEPARATOR + link); 1128 if (fi.exists()) { 1129 mActivity.loadDocument(fi, true); 1130 return; 1131 } 1132 } 1133 } 1134 mActivity.showToast("Cannot open link " + link); 1135 } 1136 } 1137 } 1138 }); 1139 return true; 1140 } 1141 startSelection()1142 private boolean startSelection() { 1143 state = STATE_SELECTION; 1144 // check link before executing action 1145 mEngine.execute(new Task() { 1146 ImageInfo image; 1147 Bookmark bookmark; 1148 1149 public void work() { 1150 image = new ImageInfo(); 1151 image.bufWidth = internalDX; 1152 image.bufHeight = internalDY; 1153 image.bufDpi = mActivity.getDensityDpi(); 1154 if (!doc.checkImage(start_x, start_y, image)) 1155 image = null; 1156 bookmark = doc.checkBookmark(start_x, start_y); 1157 if (bookmark != null && bookmark.getType() == Bookmark.TYPE_POSITION) 1158 bookmark = null; 1159 } 1160 1161 public void done() { 1162 if (bookmark != null) 1163 bookmark = mBookInfo.findBookmark(bookmark); 1164 if (image != null) { 1165 cancel(); 1166 startImageViewer(image); 1167 } else if (bookmark != null) { 1168 cancel(); 1169 BookmarkEditDialog dlg = new BookmarkEditDialog(mActivity, ReaderView.this, bookmark, false); 1170 dlg.show(); 1171 } else { 1172 updateSelection(start_x, start_y, start_x, start_y, false); 1173 } 1174 } 1175 }); 1176 return true; 1177 } 1178 trackDoubleTap()1179 private boolean trackDoubleTap() { 1180 state = STATE_WAIT_FOR_DOUBLE_CLICK; 1181 BackgroundThread.instance().postGUI(() -> { 1182 if (currentTapHandler == TapHandler.this && state == STATE_WAIT_FOR_DOUBLE_CLICK) 1183 performAction(shortTapAction, false); 1184 }, DOUBLE_CLICK_INTERVAL); 1185 return true; 1186 } 1187 trackLongTap()1188 private boolean trackLongTap() { 1189 BackgroundThread.instance().postGUI(() -> { 1190 if (currentTapHandler == TapHandler.this && state == STATE_DOWN_1) { 1191 if (longTapAction == ReaderAction.START_SELECTION) 1192 startSelection(); 1193 else 1194 performAction(longTapAction, true); 1195 } 1196 }, LONG_KEYPRESS_TIME); 1197 return true; 1198 } 1199 onTouchEvent(MotionEvent event)1200 public boolean onTouchEvent(MotionEvent event) { 1201 int x = (int) event.getX(); 1202 int y = (int) event.getY(); 1203 if ((DeviceInfo.getSDKLevel() >= 19) && mActivity.isFullscreen() && (event.getAction() == MotionEvent.ACTION_DOWN)) { 1204 if ((y < 30) || (y > (getSurface().getHeight() - 30))) 1205 return unexpectedEvent(); 1206 } 1207 1208 if (state == STATE_INITIAL && event.getAction() != MotionEvent.ACTION_DOWN) 1209 return unexpectedEvent(); // ignore unexpected event 1210 1211 // Uncomment to disable user interaction during cloud sync 1212 //if (isCloudSyncProgressActive()) 1213 // return unexpectedEvent(); 1214 1215 if (event.getAction() == MotionEvent.ACTION_UP) { 1216 long duration = Utils.timeInterval(firstDown); 1217 switch (state) { 1218 case STATE_DOWN_1: 1219 if (hiliteTapZoneOnTap) { 1220 hiliteTapZone(true, x, y, width, height); 1221 scheduleUnhilite(LONG_KEYPRESS_TIME); 1222 } 1223 if (duration > LONG_KEYPRESS_TIME) { 1224 if (longTapAction == ReaderAction.START_SELECTION) 1225 return startSelection(); 1226 return performAction(longTapAction, true); 1227 } 1228 if (doubleTapAction.isNone()) 1229 return performAction(shortTapAction, false); 1230 // start possible double tap tracking 1231 return trackDoubleTap(); 1232 case STATE_FLIPPING: 1233 stopAnimation(x, y); 1234 state = STATE_DONE; 1235 return cancel(); 1236 case STATE_BRIGHTNESS: 1237 stopBrightnessControl(x, y, brightness_type); 1238 state = STATE_DONE; 1239 return cancel(); 1240 case STATE_SELECTION: 1241 // If the second tap is within a radius of the first tap point, assume the user is trying to double tap on the same point 1242 if (start_x - x <= DOUBLE_TAP_RADIUS && x - start_x <= DOUBLE_TAP_RADIUS && y - start_y <= DOUBLE_TAP_RADIUS && start_y - y <= DOUBLE_TAP_RADIUS) 1243 updateSelection(start_x, start_y, start_x, start_y, true); 1244 else 1245 updateSelection(start_x, start_y, x, y, true); 1246 selectionModeActive = false; 1247 state = STATE_DONE; 1248 return cancel(); 1249 case STATE_FLIP_TRACKING: 1250 updatePageFlipTracking(x, y); 1251 state = STATE_DONE; 1252 return cancel(); 1253 } 1254 } else if (event.getAction() == MotionEvent.ACTION_DOWN) { 1255 switch (state) { 1256 case STATE_INITIAL: 1257 start_x = x; 1258 start_y = y; 1259 width = surface.getWidth(); 1260 height = surface.getHeight(); 1261 int zone = getTapZone(x, y, width, height); 1262 shortTapAction = findTapZoneAction(zone, TAP_ACTION_TYPE_SHORT); 1263 longTapAction = findTapZoneAction(zone, TAP_ACTION_TYPE_LONGPRESS); 1264 doubleTapAction = findTapZoneAction(zone, TAP_ACTION_TYPE_DOUBLE); 1265 firstDown = Utils.timeStamp(); 1266 if (selectionModeActive) { 1267 startSelection(); 1268 } else { 1269 state = STATE_DOWN_1; 1270 trackLongTap(); 1271 } 1272 return true; 1273 case STATE_DOWN_1: 1274 case STATE_BRIGHTNESS: 1275 case STATE_FLIPPING: 1276 case STATE_SELECTION: 1277 case STATE_FLIP_TRACKING: 1278 return unexpectedEvent(); 1279 case STATE_WAIT_FOR_DOUBLE_CLICK: 1280 if (doubleTapAction == ReaderAction.START_SELECTION) 1281 return startSelection(); 1282 return performAction(doubleTapAction, true); 1283 } 1284 } else if (event.getAction() == MotionEvent.ACTION_MOVE) { 1285 int dx = x - start_x; 1286 int dy = y - start_y; 1287 int adx = dx > 0 ? dx : -dx; 1288 int ady = dy > 0 ? dy : -dy; 1289 int distance = adx + ady; 1290 int dragThreshold = mActivity.getPalmTipPixels(); 1291 switch (state) { 1292 case STATE_DOWN_1: 1293 if (distance < dragThreshold) 1294 return true; 1295 if ((!DeviceInfo.EINK_SCREEN || DeviceInfo.EINK_HAVE_FRONTLIGHT) && isBacklightControlFlick != BACKLIGHT_CONTROL_FLICK_NONE && ady > adx) { 1296 // backlight control enabled 1297 if (start_x < dragThreshold * 170 / 100 && isBacklightControlFlick == 1 1298 || start_x > width - dragThreshold * 170 / 100 && isBacklightControlFlick == 2) { 1299 // brightness 1300 state = STATE_BRIGHTNESS; 1301 brightness_type = BRIGHTNESS_TYPE_COMMON; 1302 startBrightnessControl(start_x, start_y, brightness_type); 1303 return true; 1304 } 1305 } 1306 if (DeviceInfo.EINK_HAVE_NATURAL_BACKLIGHT && isWarmBacklightControlFlick != BACKLIGHT_CONTROL_FLICK_NONE && ady > adx) { 1307 // warm backlight control enabled 1308 if (start_x < dragThreshold * 170 / 100 && isWarmBacklightControlFlick == 1 1309 || start_x > width - dragThreshold * 170 / 100 && isWarmBacklightControlFlick == 2) { 1310 // warm backlight brightness 1311 state = STATE_BRIGHTNESS; 1312 brightness_type = BRIGHTNESS_TYPE_WARM; 1313 startBrightnessControl(start_x, start_y, brightness_type); 1314 return true; 1315 } 1316 } 1317 int dir = mIsPageMode ? x - start_x : y - start_y; 1318 if (mGesturePageFlipsPerFullSwipe == 1) { 1319 if (pageFlipAnimationSpeedMs == 0 || DeviceInfo.EINK_SCREEN) { 1320 // no animation 1321 return performAction(dir < 0 ? ReaderAction.PAGE_DOWN : ReaderAction.PAGE_UP, false); 1322 } 1323 startAnimation(start_x, start_y, width, height, x, y); 1324 updateAnimation(x, y); 1325 state = STATE_FLIPPING; 1326 } 1327 if (mGesturePageFlipsPerFullSwipe > 1) { 1328 state = STATE_FLIP_TRACKING; 1329 updatePageFlipTracking(start_x, start_y); 1330 } 1331 return true; 1332 case STATE_FLIPPING: 1333 updateAnimation(x, y); 1334 return true; 1335 case STATE_BRIGHTNESS: 1336 updateBrightnessControl(x, y, brightness_type); 1337 return true; 1338 case STATE_FLIP_TRACKING: 1339 updatePageFlipTracking(x, y); 1340 return true; 1341 case STATE_WAIT_FOR_DOUBLE_CLICK: 1342 return true; 1343 case STATE_SELECTION: 1344 updateSelection(start_x, start_y, x, y, false); 1345 break; 1346 } 1347 1348 } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1349 return unexpectedEvent(); 1350 } 1351 return true; 1352 } 1353 } 1354 1355 showTOC()1356 public void showTOC() { 1357 BackgroundThread.ensureGUI(); 1358 final ReaderView view = this; 1359 mEngine.post(new Task() { 1360 TOCItem toc; 1361 PositionProperties pos; 1362 1363 public void work() { 1364 BackgroundThread.ensureBackground(); 1365 toc = doc.getTOC(); 1366 pos = doc.getPositionProps(null, false); 1367 } 1368 1369 public void done() { 1370 BackgroundThread.ensureGUI(); 1371 if (toc != null && pos != null) { 1372 TOCDlg dlg = new TOCDlg(mActivity, view, toc, pos.pageNumber); 1373 dlg.show(); 1374 } else { 1375 mActivity.showToast("No Table of Contents found"); 1376 } 1377 } 1378 }); 1379 } 1380 showSearchDialog(String initialText)1381 public void showSearchDialog(String initialText) { 1382 if (initialText != null && initialText.length() > 40) 1383 initialText = initialText.substring(0, 40); 1384 BackgroundThread.ensureGUI(); 1385 SearchDlg dlg = new SearchDlg(mActivity, this, initialText); 1386 dlg.show(); 1387 } 1388 findText(final String pattern, final boolean reverse, final boolean caseInsensitive)1389 public void findText(final String pattern, final boolean reverse, final boolean caseInsensitive) { 1390 BackgroundThread.ensureGUI(); 1391 final ReaderView view = this; 1392 mEngine.execute(new Task() { 1393 public void work() throws Exception { 1394 BackgroundThread.ensureBackground(); 1395 boolean res = doc.findText(pattern, 1, reverse ? 1 : 0, caseInsensitive ? 1 : 0); 1396 if (!res) 1397 res = doc.findText(pattern, -1, reverse ? 1 : 0, caseInsensitive ? 1 : 0); 1398 if (!res) { 1399 doc.clearSelection(); 1400 throw new Exception("pattern not found"); 1401 } 1402 } 1403 1404 public void done() { 1405 BackgroundThread.ensureGUI(); 1406 drawPage(); 1407 FindNextDlg.showDialog(mActivity, view, pattern, caseInsensitive); 1408 } 1409 1410 public void fail(Exception e) { 1411 BackgroundThread.ensureGUI(); 1412 mActivity.showToast("Pattern not found"); 1413 } 1414 1415 }); 1416 } 1417 findNext(final String pattern, final boolean reverse, final boolean caseInsensitive)1418 public void findNext(final String pattern, final boolean reverse, final boolean caseInsensitive) { 1419 BackgroundThread.ensureGUI(); 1420 mEngine.execute(new Task() { 1421 public void work() throws Exception { 1422 BackgroundThread.ensureBackground(); 1423 boolean res = doc.findText(pattern, 1, reverse ? 1 : 0, caseInsensitive ? 1 : 0); 1424 if (!res) 1425 res = doc.findText(pattern, -1, reverse ? 1 : 0, caseInsensitive ? 1 : 0); 1426 if (!res) { 1427 doc.clearSelection(); 1428 throw new Exception("pattern not found"); 1429 } 1430 } 1431 1432 public void done() { 1433 BackgroundThread.ensureGUI(); 1434 // drawPage(); 1435 drawPage(true); 1436 } 1437 }); 1438 } 1439 1440 private boolean flgHighlightBookmarks = false; 1441 clearSelection()1442 public void clearSelection() { 1443 BackgroundThread.ensureGUI(); 1444 if (mBookInfo == null || !isBookLoaded()) 1445 return; 1446 mEngine.post(new Task() { 1447 public void work() throws Exception { 1448 doc.clearSelection(); 1449 invalidImages = true; 1450 } 1451 1452 public void done() { 1453 if (surface.isShown()) 1454 drawPage(true); 1455 } 1456 }); 1457 } 1458 highlightBookmarks()1459 public void highlightBookmarks() { 1460 BackgroundThread.ensureGUI(); 1461 if (mBookInfo == null || !isBookLoaded()) 1462 return; 1463 int count = mBookInfo.getBookmarkCount(); 1464 final Bookmark[] list = (count > 0 && flgHighlightBookmarks) ? new Bookmark[count] : null; 1465 for (int i = 0; i < count && flgHighlightBookmarks; i++) 1466 list[i] = mBookInfo.getBookmark(i); 1467 mEngine.post(new Task() { 1468 public void work() throws Exception { 1469 doc.hilightBookmarks(list); 1470 invalidImages = true; 1471 } 1472 1473 public void done() { 1474 if (surface.isShown()) 1475 drawPage(true); 1476 } 1477 }); 1478 } 1479 goToBookmark(Bookmark bm)1480 public void goToBookmark(Bookmark bm) { 1481 BackgroundThread.ensureGUI(); 1482 final String pos = bm.getStartPos(); 1483 mEngine.execute(new Task() { 1484 public void work() { 1485 BackgroundThread.ensureBackground(); 1486 doc.goToPosition(pos, true); 1487 } 1488 1489 public void done() { 1490 BackgroundThread.ensureGUI(); 1491 drawPage(); 1492 } 1493 }); 1494 } 1495 goToBookmark(final int shortcut)1496 public boolean goToBookmark(final int shortcut) { 1497 BackgroundThread.ensureGUI(); 1498 if (mBookInfo != null) { 1499 Bookmark bm = mBookInfo.findShortcutBookmark(shortcut); 1500 if (bm == null) { 1501 addBookmark(shortcut); 1502 return true; 1503 } else { 1504 // go to bookmark 1505 goToBookmark(bm); 1506 return false; 1507 } 1508 } 1509 return false; 1510 } 1511 removeBookmark(final Bookmark bookmark)1512 public Bookmark removeBookmark(final Bookmark bookmark) { 1513 Bookmark removed = mBookInfo.removeBookmark(bookmark); 1514 if (removed != null) { 1515 if (removed.getId() != null) { 1516 mActivity.getDB().deleteBookmark(removed); 1517 } 1518 highlightBookmarks(); 1519 } 1520 return removed; 1521 } 1522 updateBookmark(final Bookmark bookmark)1523 public Bookmark updateBookmark(final Bookmark bookmark) { 1524 Bookmark bm = mBookInfo.updateBookmark(bookmark); 1525 if (bm != null) { 1526 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 1527 highlightBookmarks(); 1528 } 1529 return bm; 1530 } 1531 addBookmark(final Bookmark bookmark)1532 public void addBookmark(final Bookmark bookmark) { 1533 mBookInfo.addBookmark(bookmark); 1534 highlightBookmarks(); 1535 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 1536 } 1537 addBookmark(final int shortcut)1538 public void addBookmark(final int shortcut) { 1539 BackgroundThread.ensureGUI(); 1540 // set bookmark instead 1541 mEngine.execute(new Task() { 1542 Bookmark bm; 1543 1544 public void work() { 1545 BackgroundThread.ensureBackground(); 1546 if (mBookInfo != null) { 1547 bm = doc.getCurrentPageBookmark(); 1548 bm.setShortcut(shortcut); 1549 } 1550 } 1551 1552 public void done() { 1553 if (mBookInfo != null && bm != null) { 1554 if (shortcut == 0) 1555 mBookInfo.addBookmark(bm); 1556 else 1557 mBookInfo.setShortcutBookmark(shortcut, bm); 1558 mActivity.getDB().saveBookInfo(mBookInfo); 1559 String s; 1560 if (shortcut == 0) 1561 s = mActivity.getString(R.string.toast_position_bookmark_is_set); 1562 else { 1563 s = mActivity.getString(R.string.toast_shortcut_bookmark_is_set); 1564 s.replace("$1", String.valueOf(shortcut)); 1565 } 1566 highlightBookmarks(); 1567 mActivity.showToast(s); 1568 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 1569 } 1570 } 1571 }); 1572 } 1573 onMenuItem(final int itemId)1574 public boolean onMenuItem(final int itemId) { 1575 BackgroundThread.ensureGUI(); 1576 ReaderAction action = ReaderAction.findByMenuId(itemId); 1577 if (action.isNone()) 1578 return false; 1579 onAction(action); 1580 return true; 1581 } 1582 onAction(final ReaderAction action)1583 public void onAction(final ReaderAction action) { 1584 onAction(action, null); 1585 } 1586 onAction(final ReaderAction action, final Runnable onFinishHandler)1587 public void onAction(final ReaderAction action, final Runnable onFinishHandler) { 1588 BackgroundThread.ensureGUI(); 1589 if (action.cmd != ReaderCommand.DCMD_NONE) 1590 onCommand(action.cmd, action.param, onFinishHandler); 1591 } 1592 toggleDayNightMode()1593 public void toggleDayNightMode() { 1594 Properties settings = getSettings(); 1595 OptionsDialog.toggleDayNightMode(settings); 1596 //setSettings(settings, mActivity.settings()); 1597 mActivity.setSettings(settings, 60000, true); 1598 invalidImages = true; 1599 } 1600 isNightMode()1601 public boolean isNightMode() { 1602 return mSettings.getBool(PROP_NIGHT_MODE, false); 1603 } 1604 getSetting(String name)1605 public String getSetting(String name) { 1606 return mSettings.getProperty(name); 1607 } 1608 setSetting(String name, String value, boolean invalidateImages, boolean save, boolean apply)1609 public void setSetting(String name, String value, boolean invalidateImages, boolean save, boolean apply) { 1610 mActivity.setSetting(name, value, apply); 1611 invalidImages = true; 1612 } 1613 setSetting(String name, String value)1614 public void setSetting(String name, String value) { 1615 setSetting(name, value, true, false, true); 1616 } 1617 setViewModeNonPermanent(ViewMode mode)1618 public void setViewModeNonPermanent(ViewMode mode) { 1619 if (mode != viewMode) { 1620 if (mode == ViewMode.SCROLL) { 1621 doc.doCommand(ReaderCommand.DCMD_TOGGLE_PAGE_SCROLL_VIEW.nativeId, 0); 1622 viewMode = mode; 1623 mIsPageMode = false; 1624 } else { 1625 doc.doCommand(ReaderCommand.DCMD_TOGGLE_PAGE_SCROLL_VIEW.nativeId, 0); 1626 viewMode = mode; 1627 mIsPageMode = true; 1628 } 1629 } 1630 } 1631 saveSetting(String name, String value)1632 public void saveSetting(String name, String value) { 1633 setSetting(name, value, true, true, true); 1634 } 1635 toggleScreenOrientation()1636 public void toggleScreenOrientation() { 1637 int orientation = mActivity.getScreenOrientation(); 1638 orientation = (orientation == 0) ? 1 : 0; 1639 saveSetting(PROP_APP_SCREEN_ORIENTATION, String.valueOf(orientation)); 1640 mActivity.setScreenOrientation(orientation); 1641 } 1642 toggleFullscreen()1643 public void toggleFullscreen() { 1644 boolean newBool = !mActivity.isFullscreen(); 1645 String newValue = newBool ? "1" : "0"; 1646 saveSetting(PROP_APP_FULLSCREEN, newValue); 1647 mActivity.setFullscreen(newBool); 1648 } 1649 showReadingPositionPopup()1650 public void showReadingPositionPopup() { 1651 if (mBookInfo == null) 1652 return; 1653 final StringBuilder buf = new StringBuilder(); 1654 // if (mActivity.isFullscreen()) { 1655 buf.append(Utils.formatTime(mActivity, System.currentTimeMillis()) + " "); 1656 if (mBatteryState >= 0) 1657 buf.append(" [" + mBatteryState + "%]\n"); 1658 // } 1659 execute(new Task() { 1660 Bookmark bm; 1661 1662 @Override 1663 public void work() { 1664 bm = doc.getCurrentPageBookmark(); 1665 if (bm != null) { 1666 PositionProperties prop = doc.getPositionProps(bm.getStartPos(), true); 1667 if (prop.pageMode != 0) { 1668 buf.append("" + (prop.pageNumber + 1) + " / " + prop.pageCount + " "); 1669 } 1670 int percent = (int) (10000 * (long) prop.y / prop.fullHeight); 1671 buf.append("" + (percent / 100) + "." + (percent % 100) + "%"); 1672 1673 // Show chapter details if book has more than one chapter 1674 TOCItem toc = doc.getTOC(); 1675 if (toc != null && toc.getChildCount() > 1) { 1676 TOCItem chapter = toc.getChapterAtPage(prop.pageNumber); 1677 1678 String chapterName = chapter.getName(); 1679 if (chapterName != null && chapterName.length() > 30) 1680 chapterName = chapterName.substring(0, 30) + "..."; 1681 1682 TOCItem nextChapter = chapter.getNextChapter(); 1683 int iChapterEnd = (nextChapter != null) ? nextChapter.getPage() : prop.pageCount; 1684 1685 String chapterPos = null; 1686 if (prop.pageMode != 0) { 1687 int iChapterStart = chapter.getPage(); 1688 int iChapterLen = iChapterEnd - iChapterStart; 1689 int iChapterPage = prop.pageNumber - iChapterStart + 1; 1690 1691 chapterPos = " (" + iChapterPage + " / " + iChapterLen + ")"; 1692 } 1693 1694 if (chapterName != null && chapterName.length() > 0) 1695 buf.append("\n" + chapterName); 1696 if (chapterPos != null && chapterPos.length() > 0) 1697 buf.append(chapterPos); 1698 } 1699 } 1700 } 1701 1702 public void done() { 1703 mActivity.showToast(buf.toString()); 1704 } 1705 }); 1706 } 1707 toggleTitlebar()1708 public void toggleTitlebar() { 1709 boolean newBool = "1".equals(getSetting(PROP_STATUS_LINE)); 1710 String newValue = !newBool ? "1" : "0"; 1711 mActivity.setSetting(PROP_STATUS_LINE, newValue, true); 1712 } 1713 toggleDocumentStyles()1714 public void toggleDocumentStyles() { 1715 if (mOpened && mBookInfo != null) { 1716 log.d("toggleDocumentStyles()"); 1717 boolean disableInternalStyles = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_USE_DOCUMENT_STYLES_FLAG); 1718 disableInternalStyles = !disableInternalStyles; 1719 mBookInfo.getFileInfo().setFlag(FileInfo.DONT_USE_DOCUMENT_STYLES_FLAG, disableInternalStyles); 1720 doEngineCommand(ReaderCommand.DCMD_SET_INTERNAL_STYLES, disableInternalStyles ? 0 : 1); 1721 doEngineCommand(ReaderCommand.DCMD_REQUEST_RENDER, 1); 1722 mActivity.getDB().saveBookInfo(mBookInfo); 1723 } 1724 } 1725 toggleEmbeddedFonts()1726 public void toggleEmbeddedFonts() { 1727 if (mOpened && mBookInfo != null) { 1728 log.d("toggleEmbeddedFonts()"); 1729 boolean enableInternalFonts = mBookInfo.getFileInfo().getFlag(FileInfo.USE_DOCUMENT_FONTS_FLAG); 1730 enableInternalFonts = !enableInternalFonts; 1731 mBookInfo.getFileInfo().setFlag(FileInfo.USE_DOCUMENT_FONTS_FLAG, enableInternalFonts); 1732 doEngineCommand(ReaderCommand.DCMD_SET_DOC_FONTS, enableInternalFonts ? 1 : 0); 1733 doEngineCommand(ReaderCommand.DCMD_REQUEST_RENDER, 1); 1734 mActivity.getDB().saveBookInfo(mBookInfo); 1735 } 1736 } 1737 isTextAutoformatEnabled()1738 public boolean isTextAutoformatEnabled() { 1739 if (mOpened && mBookInfo != null) { 1740 boolean disableTextReflow = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_REFLOW_TXT_FILES_FLAG); 1741 return !disableTextReflow; 1742 } 1743 return true; 1744 } 1745 isTextFormat()1746 public boolean isTextFormat() { 1747 if (mOpened && mBookInfo != null) { 1748 DocumentFormat fmt = mBookInfo.getFileInfo().format; 1749 return fmt == DocumentFormat.TXT || fmt == DocumentFormat.HTML || fmt == DocumentFormat.PDB; 1750 } 1751 return false; 1752 } 1753 isFormatWithEmbeddedFonts()1754 public boolean isFormatWithEmbeddedFonts() { 1755 if (mOpened && mBookInfo != null) { 1756 DocumentFormat fmt = mBookInfo.getFileInfo().format; 1757 return fmt == DocumentFormat.EPUB; 1758 } 1759 return false; 1760 } 1761 isFormatWithEmbeddedStyles()1762 public boolean isFormatWithEmbeddedStyles() { 1763 if (mOpened && mBookInfo != null) { 1764 DocumentFormat fmt = mBookInfo.getFileInfo().format; 1765 return fmt == DocumentFormat.EPUB || fmt == DocumentFormat.HTML || fmt == DocumentFormat.CHM || fmt == DocumentFormat.FB2 || fmt == DocumentFormat.FB3; 1766 } 1767 return false; 1768 } 1769 isHtmlFormat()1770 public boolean isHtmlFormat() { 1771 if (mOpened && mBookInfo != null) { 1772 DocumentFormat fmt = mBookInfo.getFileInfo().format; 1773 return fmt == DocumentFormat.EPUB || fmt == DocumentFormat.HTML || fmt == DocumentFormat.PDB || fmt == DocumentFormat.CHM; 1774 } 1775 return false; 1776 } 1777 getDOMVersion()1778 public int getDOMVersion() { 1779 if (mOpened && mBookInfo != null) { 1780 return mBookInfo.getFileInfo().domVersion; 1781 } 1782 return Engine.DOM_VERSION_CURRENT; 1783 } 1784 setDOMVersion(int version)1785 public void setDOMVersion(int version) { 1786 if (null != mBookInfo) { 1787 mBookInfo.getFileInfo().domVersion = version; 1788 doEngineCommand(ReaderCommand.DCMD_SET_REQUESTED_DOM_VERSION, version); 1789 mActivity.getDB().saveBookInfo(mBookInfo); 1790 if (mOpened) 1791 reloadDocument(); 1792 } 1793 } 1794 getBlockRenderingFlags()1795 public int getBlockRenderingFlags() { 1796 if (mOpened && mBookInfo != null) { 1797 return mBookInfo.getFileInfo().blockRenderingFlags; 1798 } 1799 return 0; 1800 } 1801 setBlockRenderingFlags(int flags)1802 public void setBlockRenderingFlags(int flags) { 1803 if (null != mBookInfo) { 1804 mBookInfo.getFileInfo().blockRenderingFlags = flags; 1805 doEngineCommand(ReaderCommand.DCMD_SET_RENDER_BLOCK_RENDERING_FLAGS, flags); 1806 mActivity.getDB().saveBookInfo(mBookInfo); 1807 if (mOpened) 1808 reloadDocument(); 1809 } 1810 } 1811 toggleTextFormat()1812 public void toggleTextFormat() { 1813 if (mOpened && mBookInfo != null) { 1814 log.d("toggleDocumentStyles()"); 1815 if (!isTextFormat()) 1816 return; 1817 boolean disableTextReflow = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_REFLOW_TXT_FILES_FLAG); 1818 disableTextReflow = !disableTextReflow; 1819 mBookInfo.getFileInfo().setFlag(FileInfo.DONT_REFLOW_TXT_FILES_FLAG, disableTextReflow); 1820 mActivity.getDB().saveBookInfo(mBookInfo); 1821 reloadDocument(); 1822 } 1823 } 1824 getDocumentStylesEnabled()1825 public boolean getDocumentStylesEnabled() { 1826 if (mOpened && mBookInfo != null) { 1827 boolean flg = !mBookInfo.getFileInfo().getFlag(FileInfo.DONT_USE_DOCUMENT_STYLES_FLAG); 1828 return flg; 1829 } 1830 return true; 1831 } 1832 getDocumentFontsEnabled()1833 public boolean getDocumentFontsEnabled() { 1834 if (mOpened && mBookInfo != null) { 1835 boolean flg = mBookInfo.getFileInfo().getFlag(FileInfo.USE_DOCUMENT_FONTS_FLAG); 1836 return flg; 1837 } 1838 return true; 1839 } 1840 1841 static private SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault()); 1842 showBookInfo()1843 public void showBookInfo() { 1844 final ArrayList<String> items = new ArrayList<String>(); 1845 items.add("section=section.system"); 1846 items.add("system.version=Cool Reader " + mActivity.getVersion()); 1847 items.add("system.battery=" + mBatteryState + "%"); 1848 items.add("system.time=" + Utils.formatTime(mActivity, System.currentTimeMillis())); 1849 final BookInfo bi = mBookInfo; 1850 if (bi != null) { 1851 FileInfo fi = bi.getFileInfo(); 1852 items.add("section=section.file"); 1853 String fname = new File(fi.pathname).getName(); 1854 items.add("file.name=" + fname); 1855 if (new File(fi.pathname).getParent() != null) 1856 items.add("file.path=" + new File(fi.pathname).getParent()); 1857 items.add("file.size=" + fi.size); 1858 if (fi.arcname != null) { 1859 items.add("file.arcname=" + new File(fi.arcname).getName()); 1860 if (new File(fi.arcname).getParent() != null) 1861 items.add("file.arcpath=" + new File(fi.arcname).getParent()); 1862 items.add("file.arcsize=" + fi.arcsize); 1863 } 1864 items.add("file.format=" + fi.format.name()); 1865 } 1866 execute(new Task() { 1867 Bookmark bm; 1868 1869 @Override 1870 public void work() { 1871 bm = doc.getCurrentPageBookmark(); 1872 if (bm != null) { 1873 PositionProperties prop = doc.getPositionProps(bm.getStartPos(), true); 1874 items.add("section=section.position"); 1875 if (prop.pageMode != 0) { 1876 items.add("position.page=" + (prop.pageNumber + 1) + " / " + prop.pageCount); 1877 } 1878 int percent = (int) (10000 * (long) prop.y / prop.fullHeight); 1879 items.add("position.percent=" + (percent / 100) + "." + (percent % 100) + "%"); 1880 String chapter = bm.getTitleText(); 1881 if (chapter != null && chapter.length() > 100) 1882 chapter = chapter.substring(0, 100) + "..."; 1883 items.add("position.chapter=" + chapter); 1884 } 1885 } 1886 1887 public void done() { 1888 FileInfo fi = bi.getFileInfo(); 1889 items.add("section=section.book"); 1890 if (fi.authors != null || fi.title != null || fi.series != null) { 1891 items.add("book.authors=" + fi.authors); 1892 items.add("book.title=" + fi.title); 1893 if (fi.series != null) { 1894 String s = fi.series; 1895 if (fi.seriesNumber > 0) 1896 s = s + " #" + fi.seriesNumber; 1897 items.add("book.series=" + s); 1898 } 1899 } 1900 if (fi.language != null) { 1901 items.add("book.language=" + fi.language); 1902 } 1903 if (fi.format == DocumentFormat.FB2) { 1904 if (fi.genres != null && fi.genres.length() > 0) { 1905 items.add("book.genres=" + fi.genres); 1906 } 1907 } 1908 BookInfoDialog dlg = new BookInfoDialog(mActivity, items); 1909 dlg.show(); 1910 } 1911 }); 1912 } 1913 1914 private int autoScrollSpeed = 1500; // chars / minute 1915 private int autoScrollNotificationId = 0; 1916 private AutoScrollAnimation currentAutoScrollAnimation = null; 1917 isAutoScrollActive()1918 private boolean isAutoScrollActive() { 1919 return currentAutoScrollAnimation != null; 1920 } 1921 stopAutoScroll()1922 private void stopAutoScroll() { 1923 if (!isAutoScrollActive()) 1924 return; 1925 log.d("stopAutoScroll()"); 1926 //notifyAutoscroll("Autoscroll is stopped"); 1927 currentAutoScrollAnimation.stop(); 1928 } 1929 1930 public static final int AUTOSCROLL_START_ANIMATION_PERCENT = 5; 1931 startAutoScroll()1932 private void startAutoScroll() { 1933 if (isAutoScrollActive()) 1934 return; 1935 log.d("startAutoScroll()"); 1936 currentAutoScrollAnimation = new AutoScrollAnimation(AUTOSCROLL_START_ANIMATION_PERCENT * 100); 1937 nextHiliteId++; 1938 hiliteRect = null; 1939 } 1940 toggleAutoScroll()1941 private void toggleAutoScroll() { 1942 if (isAutoScrollActive()) 1943 stopAutoScroll(); 1944 else 1945 startAutoScroll(); 1946 } 1947 1948 private final static boolean AUTOSCROLL_SPEED_NOTIFICATION_ENABLED = false; 1949 notifyAutoscroll(final String msg)1950 private void notifyAutoscroll(final String msg) { 1951 if (DeviceInfo.EINK_SCREEN) 1952 return; // disable toast for eink 1953 if (AUTOSCROLL_SPEED_NOTIFICATION_ENABLED) { 1954 final int myId = ++autoScrollNotificationId; 1955 BackgroundThread.instance().postGUI(() -> { 1956 if (myId == autoScrollNotificationId) 1957 mActivity.showToast(msg); 1958 }, 1000); 1959 } 1960 } 1961 notifyAutoscrollSpeed()1962 private void notifyAutoscrollSpeed() { 1963 final String msg = mActivity.getString(R.string.lbl_autoscroll_speed).replace("$1", String.valueOf(autoScrollSpeed)); 1964 notifyAutoscroll(msg); 1965 } 1966 changeAutoScrollSpeed(int delta)1967 private void changeAutoScrollSpeed(int delta) { 1968 if (autoScrollSpeed < 300) 1969 delta *= 10; 1970 else if (autoScrollSpeed < 500) 1971 delta *= 20; 1972 else if (autoScrollSpeed < 1000) 1973 delta *= 40; 1974 else if (autoScrollSpeed < 2000) 1975 delta *= 80; 1976 else if (autoScrollSpeed < 5000) 1977 delta *= 200; 1978 else 1979 delta *= 300; 1980 autoScrollSpeed += delta; 1981 if (autoScrollSpeed < 200) 1982 autoScrollSpeed = 200; 1983 if (autoScrollSpeed > 10000) 1984 autoScrollSpeed = 10000; 1985 setSetting(PROP_APP_VIEW_AUTOSCROLL_SPEED, String.valueOf(autoScrollSpeed), false, true, false); 1986 notifyAutoscrollSpeed(); 1987 } 1988 1989 class AutoScrollAnimation { 1990 1991 boolean isScrollView; 1992 BitmapInfo image1; 1993 BitmapInfo image2; 1994 PositionProperties currPos; 1995 int progress; 1996 int pageCount; 1997 int charCount; 1998 int timerInterval; 1999 long pageTurnStart; 2000 int nextPos; 2001 2002 Paint[] shadePaints; 2003 Paint[] hilitePaints; 2004 2005 final int startAnimationProgress; 2006 2007 public static final int MAX_PROGRESS = 10000; 2008 public final static int ANIMATION_INTERVAL_NORMAL = 30; 2009 public final static int ANIMATION_INTERVAL_EINK = 5000; 2010 AutoScrollAnimation(final int startProgress)2011 public AutoScrollAnimation(final int startProgress) { 2012 progress = startProgress; 2013 startAnimationProgress = AUTOSCROLL_START_ANIMATION_PERCENT * 100; 2014 currentAutoScrollAnimation = this; 2015 2016 final int numPaints = 32; 2017 shadePaints = new Paint[numPaints]; 2018 hilitePaints = new Paint[numPaints]; 2019 for (int i = 0; i < numPaints; i++) { 2020 shadePaints[i] = new Paint(); 2021 hilitePaints[i] = new Paint(); 2022 hilitePaints[i].setStyle(Paint.Style.FILL); 2023 shadePaints[i].setStyle(Paint.Style.FILL); 2024 if (mActivity.isNightMode()) { 2025 shadePaints[i].setColor(Color.argb((i + 1) * 128 / numPaints, 0, 0, 0)); 2026 hilitePaints[i].setColor(Color.argb((i + 1) * 128 / numPaints, 128, 128, 128)); 2027 } else { 2028 shadePaints[i].setColor(Color.argb((i + 1) * 128 / numPaints, 0, 0, 0)); 2029 hilitePaints[i].setColor(Color.argb((i + 1) * 128 / numPaints, 255, 255, 255)); 2030 } 2031 } 2032 2033 BackgroundThread.instance().postBackground(() -> { 2034 if (initPageTurn(startProgress)) { 2035 log.d("AutoScrollAnimation: starting autoscroll timer"); 2036 timerInterval = DeviceInfo.EINK_SCREEN ? ANIMATION_INTERVAL_EINK : ANIMATION_INTERVAL_NORMAL; 2037 startTimer(timerInterval); 2038 } else { 2039 currentAutoScrollAnimation = null; 2040 } 2041 }); 2042 } 2043 calcProgressPercent()2044 private int calcProgressPercent() { 2045 long duration = Utils.timeInterval(pageTurnStart); 2046 long estimatedFullDuration = 60000 * charCount / autoScrollSpeed; 2047 int percent = (int) (10000 * duration / estimatedFullDuration); 2048 // if (duration > estimatedFullDuration - timerInterval / 3) 2049 // percent = 10000; 2050 if (percent > 10000) 2051 percent = 10000; 2052 if (percent < 0) 2053 percent = 0; 2054 return percent; 2055 } 2056 onTimer()2057 private boolean onTimer() { 2058 int newProgress = calcProgressPercent(); 2059 alog.v("onTimer(progress = " + newProgress + ")"); 2060 mActivity.onUserActivity(); 2061 progress = newProgress; 2062 if (progress == 0 || progress >= startAnimationProgress) { 2063 if (image1 != null && image2 != null) { 2064 if (image1.isReleased() || image2.isReleased()) { 2065 log.d("Images lost! Recreating images..."); 2066 initPageTurn(progress); 2067 } 2068 draw(); 2069 } 2070 } 2071 if (progress >= 10000) { 2072 if (!donePageTurn(true)) { 2073 stop(); 2074 return false; 2075 } 2076 initPageTurn(0); 2077 } 2078 return true; 2079 } 2080 2081 class AutoscrollTimerTask implements Runnable { 2082 final long interval; 2083 AutoscrollTimerTask(long interval)2084 public AutoscrollTimerTask(long interval) { 2085 this.interval = interval; 2086 mActivity.onUserActivity(); 2087 BackgroundThread.instance().postGUI(this, interval); 2088 } 2089 2090 @Override run()2091 public void run() { 2092 if (currentAutoScrollAnimation != AutoScrollAnimation.this) { 2093 log.v("timer is cancelled - GUI"); 2094 return; 2095 } 2096 BackgroundThread.instance().postBackground(() -> { 2097 if (currentAutoScrollAnimation != AutoScrollAnimation.this) { 2098 log.v("timer is cancelled - BackgroundThread"); 2099 return; 2100 } 2101 if (onTimer()) 2102 BackgroundThread.instance().postGUI(AutoscrollTimerTask.this, interval); 2103 else 2104 log.v("timer is cancelled - onTimer returned false"); 2105 }); 2106 } 2107 } 2108 startTimer(final int interval)2109 private void startTimer(final int interval) { 2110 new AutoscrollTimerTask(interval); 2111 } 2112 initPageTurn(int startProgress)2113 private boolean initPageTurn(int startProgress) { 2114 cancelGc(); 2115 log.v("initPageTurn(startProgress = " + startProgress + ")"); 2116 pageTurnStart = Utils.timeStamp(); 2117 progress = startProgress; 2118 currPos = doc.getPositionProps(null, true); 2119 charCount = currPos.charCount; 2120 pageCount = currPos.pageMode; 2121 if (charCount < 150) 2122 charCount = 150; 2123 isScrollView = currPos.pageMode == 0; 2124 log.v("initPageTurn(charCount = " + charCount + ")"); 2125 if (isScrollView) { 2126 image1 = preparePageImage(0); 2127 if (image1 == null) { 2128 log.v("ScrollViewAnimation -- not started: image is null"); 2129 return false; 2130 } 2131 int pos0 = image1.position.y; 2132 int pos1 = pos0 + image1.position.pageHeight * 9 / 10; 2133 if (pos1 > image1.position.fullHeight - image1.position.pageHeight) 2134 pos1 = image1.position.fullHeight - image1.position.pageHeight; 2135 if (pos1 < 0) 2136 pos1 = 0; 2137 nextPos = pos1; 2138 image2 = preparePageImage(pos1 - pos0); 2139 if (image2 == null) { 2140 log.v("ScrollViewAnimation -- not started: image is null"); 2141 return false; 2142 } 2143 } else { 2144 int page1 = currPos.pageNumber; 2145 int page2 = currPos.pageNumber + 1; 2146 if (page2 < 0 || page2 >= currPos.pageCount) { 2147 currentAnimation = null; 2148 return false; 2149 } 2150 image1 = preparePageImage(0); 2151 image2 = preparePageImage(1); 2152 if (page1 == page2) { 2153 log.v("PageViewAnimation -- cannot start animation: not moved"); 2154 return false; 2155 } 2156 if (image1 == null || image2 == null) { 2157 log.v("PageViewAnimation -- cannot start animation: page image is null"); 2158 return false; 2159 } 2160 2161 } 2162 long duration = android.os.SystemClock.uptimeMillis() - pageTurnStart; 2163 log.v("AutoScrollAnimation -- page turn initialized in " + duration + " millis"); 2164 currentAutoScrollAnimation = this; 2165 draw(); 2166 return true; 2167 } 2168 2169 donePageTurn(boolean turnPage)2170 private boolean donePageTurn(boolean turnPage) { 2171 log.v("donePageTurn()"); 2172 if (turnPage) { 2173 if (isScrollView) 2174 doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, nextPos); 2175 else 2176 doc.doCommand(ReaderCommand.DCMD_PAGEDOWN.nativeId, 1); 2177 } 2178 progress = 0; 2179 //draw(); 2180 return currPos.canMoveToNextPage(); 2181 } 2182 draw()2183 public void draw() { 2184 draw(true); 2185 } 2186 draw(boolean isPartially)2187 public void draw(boolean isPartially) { 2188 // long startTs = android.os.SystemClock.uptimeMillis(); 2189 drawCallback(this::draw, null, isPartially); 2190 } 2191 stop()2192 public void stop() { 2193 currentAutoScrollAnimation = null; 2194 BackgroundThread.instance().executeBackground(() -> { 2195 donePageTurn(wantPageTurn()); 2196 //redraw(); 2197 drawPage(null, false); 2198 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 2199 }); 2200 scheduleGc(); 2201 } 2202 wantPageTurn()2203 private boolean wantPageTurn() { 2204 return (progress > (startAnimationProgress + MAX_PROGRESS) / 2); 2205 } 2206 drawGradient(Canvas canvas, Rect rc, Paint[] paints, int startIndex, int endIndex)2207 private void drawGradient(Canvas canvas, Rect rc, Paint[] paints, int startIndex, int endIndex) { 2208 //log.v("drawShadow"); 2209 int n = (startIndex < endIndex) ? endIndex - startIndex + 1 : startIndex - endIndex + 1; 2210 int dir = (startIndex < endIndex) ? 1 : -1; 2211 int dx = rc.bottom - rc.top; 2212 Rect rect = new Rect(rc); 2213 for (int i = 0; i < n; i++) { 2214 int index = startIndex + i * dir; 2215 int x1 = rc.top + dx * i / n; 2216 int x2 = rc.top + dx * (i + 1) / n; 2217 if (x1 < 0) 2218 x1 = 0; 2219 if (x2 > canvas.getHeight()) 2220 x2 = canvas.getHeight(); 2221 rect.top = x1; 2222 rect.bottom = x2; 2223 if (x2 > x1) { 2224 //log.v("drawShadow : " + x1 + ", " + x2 + ", " + index); 2225 canvas.drawRect(rect, paints[index]); 2226 } 2227 } 2228 } 2229 drawShadow(Canvas canvas, Rect rc)2230 private void drawShadow(Canvas canvas, Rect rc) { 2231 drawGradient(canvas, rc, shadePaints, shadePaints.length * 3 / 4, 0); 2232 } 2233 drawPageProgress(Canvas canvas, int scrollPercent, Rect dst, Rect src)2234 void drawPageProgress(Canvas canvas, int scrollPercent, Rect dst, Rect src) { 2235 int shadowHeight = 32; 2236 int h = dst.height(); 2237 int div = (h + shadowHeight) * scrollPercent / 10000 - shadowHeight; 2238 //log.v("drawPageProgress() div = " + div + ", percent = " + scrollPercent); 2239 int d = Math.max(div, 0); 2240 if (d > 0) { 2241 Rect src1 = new Rect(src.left, src.top, src.right, src.top + d); 2242 Rect dst1 = new Rect(dst.left, dst.top, dst.right, dst.top + d); 2243 drawDimmedBitmap(canvas, image2.bitmap, src1, dst1); 2244 } 2245 if (d < h) { 2246 Rect src2 = new Rect(src.left, src.top + d, src.right, src.bottom); 2247 Rect dst2 = new Rect(dst.left, dst.top + d, dst.right, dst.bottom); 2248 drawDimmedBitmap(canvas, image1.bitmap, src2, dst2); 2249 } 2250 if (scrollPercent > 0 && scrollPercent < 10000) { 2251 Rect shadowRect = new Rect(src.left, src.top + div, src.right, src.top + div + shadowHeight); 2252 drawShadow(canvas, shadowRect); 2253 } 2254 } 2255 draw(Canvas canvas)2256 public void draw(Canvas canvas) { 2257 if (currentAutoScrollAnimation != this) 2258 return; 2259 alog.v("AutoScrollAnimation.draw(" + progress + ")"); 2260 if (progress != 0 && progress < startAnimationProgress) 2261 return; // don't draw page w/o started animation 2262 int scrollPercent = 10000 * (progress - startAnimationProgress) / (MAX_PROGRESS - startAnimationProgress); 2263 if (scrollPercent < 0) 2264 scrollPercent = 0; 2265 int w = image1.bitmap.getWidth(); 2266 int h = image1.bitmap.getHeight(); 2267 if (isScrollView) { 2268 // scroll 2269 drawPageProgress(canvas, scrollPercent, new Rect(0, 0, w, h), new Rect(0, 0, w, h)); 2270 } else { 2271 if (image1.isReleased() || image2.isReleased()) 2272 return; 2273 if (pageCount == 2) { 2274 if (scrollPercent < 5000) { 2275 // < 50% 2276 scrollPercent = scrollPercent * 2; 2277 drawPageProgress(canvas, scrollPercent, new Rect(0, 0, w / 2, h), new Rect(0, 0, w / 2, h)); 2278 drawPageProgress(canvas, 0, new Rect(w / 2, 0, w, h), new Rect(w / 2, 0, w, h)); 2279 } else { 2280 // >=50% 2281 scrollPercent = (scrollPercent - 5000) * 2; 2282 drawPageProgress(canvas, 10000, new Rect(0, 0, w / 2, h), new Rect(0, 0, w / 2, h)); 2283 drawPageProgress(canvas, scrollPercent, new Rect(w / 2, 0, w, h), new Rect(w / 2, 0, w, h)); 2284 } 2285 } else { 2286 drawPageProgress(canvas, scrollPercent, new Rect(0, 0, w, h), new Rect(0, 0, w, h)); 2287 } 2288 } 2289 } 2290 } 2291 onCommand(final ReaderCommand cmd, final int param)2292 public void onCommand(final ReaderCommand cmd, final int param) { 2293 onCommand(cmd, param, null); 2294 } 2295 navigateByHistory(final ReaderCommand cmd)2296 private void navigateByHistory(final ReaderCommand cmd) { 2297 BackgroundThread.instance().postBackground(() -> { 2298 final boolean res = doc.doCommand(cmd.nativeId, 0); 2299 BackgroundThread.instance().postGUI(() -> { 2300 if (res) { 2301 // successful 2302 drawPage(); 2303 } else { 2304 // cannot navigate - no data on stack 2305 if (cmd == ReaderCommand.DCMD_LINK_BACK) { 2306 // TODO: exit from activity in some cases? 2307 if (mActivity.isPreviousFrameHome()) 2308 mActivity.showRootWindow(); 2309 else 2310 mActivity.showBrowser(!mActivity.isBrowserCreated() ? getOpenedFileInfo() : null); 2311 } 2312 } 2313 }); 2314 }); 2315 } 2316 onCommand(final ReaderCommand cmd, final int param, final Runnable onFinishHandler)2317 public void onCommand(final ReaderCommand cmd, final int param, final Runnable onFinishHandler) { 2318 BackgroundThread.ensureGUI(); 2319 log.i("On command " + cmd + (param != 0 ? " (" + param + ")" : " ")); 2320 switch (cmd) { 2321 case DCMD_FILE_BROWSER_ROOT: 2322 mActivity.showRootWindow(); 2323 break; 2324 case DCMD_ABOUT: 2325 mActivity.showAboutDialog(); 2326 break; 2327 case DCMD_SWITCH_PROFILE: 2328 showSwitchProfileDialog(); 2329 break; 2330 case DCMD_TOGGLE_AUTOSCROLL: 2331 toggleAutoScroll(); 2332 break; 2333 case DCMD_AUTOSCROLL_SPEED_INCREASE: 2334 changeAutoScrollSpeed(1); 2335 break; 2336 case DCMD_AUTOSCROLL_SPEED_DECREASE: 2337 changeAutoScrollSpeed(-1); 2338 break; 2339 case DCMD_SHOW_DICTIONARY: 2340 mActivity.showDictionary(); 2341 break; 2342 case DCMD_OPEN_PREVIOUS_BOOK: 2343 mActivity.loadPreviousDocument(() -> { 2344 // do nothing 2345 }); 2346 break; 2347 case DCMD_BOOK_INFO: 2348 if (isBookLoaded()) 2349 showBookInfo(); 2350 break; 2351 case DCMD_USER_MANUAL: 2352 showManual(); 2353 break; 2354 case DCMD_TTS_PLAY: { 2355 log.i("DCMD_TTS_PLAY: initializing TTS"); 2356 if (!mActivity.initTTS(tts -> { 2357 log.i("TTS created: opening TTS toolbar"); 2358 ttsToolbar = TTSToolbarDlg.showDialog(mActivity, ReaderView.this, tts); 2359 ttsToolbar.setOnCloseListener(() -> ttsToolbar = null); 2360 ttsToolbar.setAppSettings(mSettings, null); 2361 })) { 2362 log.e("Cannot initialize TTS"); 2363 } 2364 } 2365 break; 2366 case DCMD_TOGGLE_DOCUMENT_STYLES: 2367 if (isBookLoaded()) 2368 toggleDocumentStyles(); 2369 break; 2370 case DCMD_SHOW_HOME_SCREEN: 2371 mActivity.showHomeScreen(); 2372 break; 2373 case DCMD_TOGGLE_ORIENTATION: 2374 toggleScreenOrientation(); 2375 break; 2376 case DCMD_TOGGLE_FULLSCREEN: 2377 toggleFullscreen(); 2378 break; 2379 case DCMD_TOGGLE_TITLEBAR: 2380 toggleTitlebar(); 2381 break; 2382 case DCMD_SHOW_POSITION_INFO_POPUP: 2383 if (isBookLoaded()) 2384 showReadingPositionPopup(); 2385 break; 2386 case DCMD_TOGGLE_SELECTION_MODE: 2387 if (isBookLoaded()) 2388 toggleSelectionMode(); 2389 break; 2390 case DCMD_TOGGLE_TOUCH_SCREEN_LOCK: 2391 isTouchScreenEnabled = !isTouchScreenEnabled; 2392 if (isTouchScreenEnabled) 2393 mActivity.showToast(R.string.action_touch_screen_enabled_toast); 2394 else 2395 mActivity.showToast(R.string.action_touch_screen_disabled_toast); 2396 break; 2397 case DCMD_LINK_BACK: 2398 case DCMD_LINK_FORWARD: 2399 navigateByHistory(cmd); 2400 break; 2401 case DCMD_ZOOM_OUT: 2402 doEngineCommand(ReaderCommand.DCMD_ZOOM_OUT, param); 2403 syncViewSettings(getSettings(), true, true); 2404 break; 2405 case DCMD_ZOOM_IN: 2406 doEngineCommand(ReaderCommand.DCMD_ZOOM_IN, param); 2407 syncViewSettings(getSettings(), true, true); 2408 break; 2409 case DCMD_FONT_NEXT: 2410 switchFontFace(1); 2411 break; 2412 case DCMD_FONT_PREVIOUS: 2413 switchFontFace(-1); 2414 break; 2415 case DCMD_MOVE_BY_CHAPTER: 2416 if (isBookLoaded()) 2417 doEngineCommand(cmd, param, onFinishHandler); 2418 drawPage(); 2419 break; 2420 case DCMD_PAGEDOWN: 2421 if (isBookLoaded()) { 2422 if (param == 1 && !DeviceInfo.EINK_SCREEN) 2423 animatePageFlip(1, onFinishHandler); 2424 else 2425 doEngineCommand(cmd, param, onFinishHandler); 2426 } 2427 break; 2428 case DCMD_PAGEUP: 2429 if (isBookLoaded()) { 2430 if (param == 1 && !DeviceInfo.EINK_SCREEN) 2431 animatePageFlip(-1, onFinishHandler); 2432 else 2433 doEngineCommand(cmd, param, onFinishHandler); 2434 } 2435 break; 2436 case DCMD_BEGIN: 2437 case DCMD_END: 2438 if (isBookLoaded()) 2439 doEngineCommand(cmd, param); 2440 break; 2441 case DCMD_RECENT_BOOKS_LIST: 2442 mActivity.showRecentBooks(); 2443 break; 2444 case DCMD_SEARCH: 2445 if (isBookLoaded()) 2446 showSearchDialog(null); 2447 break; 2448 case DCMD_EXIT: 2449 mActivity.finish(); 2450 break; 2451 case DCMD_BOOKMARKS: 2452 if (isBookLoaded()) 2453 mActivity.showBookmarksDialog(); 2454 break; 2455 case DCMD_GO_PERCENT_DIALOG: 2456 if (isBookLoaded()) 2457 showGoToPercentDialog(); 2458 break; 2459 case DCMD_GO_PAGE_DIALOG: 2460 if (isBookLoaded()) 2461 showGoToPageDialog(); 2462 break; 2463 case DCMD_TOC_DIALOG: 2464 if (isBookLoaded()) 2465 showTOC(); 2466 break; 2467 case DCMD_FILE_BROWSER: 2468 mActivity.showBrowser(!mActivity.isBrowserCreated() ? getOpenedFileInfo() : null); 2469 break; 2470 case DCMD_CURRENT_BOOK_DIRECTORY: 2471 mActivity.showBrowser(getOpenedFileInfo()); 2472 break; 2473 case DCMD_OPTIONS_DIALOG: 2474 mActivity.showOptionsDialog(OptionsDialog.Mode.READER); 2475 break; 2476 case DCMD_READER_MENU: 2477 mActivity.showReaderMenu(); 2478 break; 2479 case DCMD_TOGGLE_DAY_NIGHT_MODE: 2480 toggleDayNightMode(); 2481 break; 2482 case DCMD_TOGGLE_DICT_ONCE: 2483 log.i("Next dictionary will be the 2nd for one time"); 2484 mActivity.showToast("Next dictionary will be the 2nd for one time"); 2485 mActivity.mDictionaries.setiDic2IsActive(2); 2486 break; 2487 case DCMD_TOGGLE_DICT: 2488 if (mActivity.mDictionaries.isiDic2IsActive() > 0) { 2489 mActivity.mDictionaries.setiDic2IsActive(0); 2490 } else { 2491 mActivity.mDictionaries.setiDic2IsActive(1); 2492 } 2493 log.i("Switched to dictionary: " + Integer.toString(mActivity.mDictionaries.isiDic2IsActive() + 1)); 2494 mActivity.showToast("Switched to dictionary: " + Integer.toString(mActivity.mDictionaries.isiDic2IsActive() + 1)); 2495 break; 2496 case DCMD_BACKLIGHT_SET_DEFAULT: 2497 setSetting(PROP_APP_SCREEN_BACKLIGHT, "-1"); // system default backlight level 2498 break; 2499 case DCMD_SHOW_SYSTEM_BACKLIGHT_DIALOG: 2500 if (DeviceInfo.EINK_HAVE_FRONTLIGHT) { 2501 if (DeviceInfo.EINK_ONYX) { 2502 mActivity.sendBroadcast(new Intent("action.show.brightness.dialog")); 2503 } else { 2504 // TODO: other eink devices with frontlight 2505 } 2506 } 2507 break; 2508 case DCMD_GOOGLEDRIVE_SYNC: 2509 if (0 == param) { // sync to 2510 mActivity.forceSyncToGoogleDrive(); 2511 } else if (1 == param) { // sync from 2512 mActivity.forceSyncFromGoogleDrive(); 2513 } 2514 break; 2515 case DCMD_SAVE_LOGCAT: 2516 mActivity.createLogcatFile(); 2517 break; 2518 default: 2519 // do nothing 2520 break; 2521 } 2522 } 2523 2524 boolean firstShowBrowserCall = true; 2525 2526 2527 private TTSToolbarDlg ttsToolbar; 2528 stopTTS()2529 public void stopTTS() { 2530 if (ttsToolbar != null) 2531 ttsToolbar.pause(); 2532 } 2533 isTTSActive()2534 public boolean isTTSActive() { 2535 return ttsToolbar != null; 2536 } 2537 getTTSToolbar()2538 public TTSToolbarDlg getTTSToolbar() { 2539 return ttsToolbar; 2540 } 2541 doEngineCommand(final ReaderCommand cmd, final int param)2542 public void doEngineCommand(final ReaderCommand cmd, final int param) { 2543 doEngineCommand(cmd, param, null); 2544 } 2545 doEngineCommand(final ReaderCommand cmd, final int param, final Runnable doneHandler)2546 public void doEngineCommand(final ReaderCommand cmd, final int param, final Runnable doneHandler) { 2547 BackgroundThread.ensureGUI(); 2548 log.d("doCommand(" + cmd + ", " + param + ")"); 2549 post(new Task() { 2550 boolean res; 2551 boolean isMoveCommand; 2552 2553 public void work() { 2554 BackgroundThread.ensureBackground(); 2555 res = doc.doCommand(cmd.nativeId, param); 2556 switch (cmd) { 2557 case DCMD_BEGIN: 2558 case DCMD_LINEUP: 2559 case DCMD_PAGEUP: 2560 case DCMD_PAGEDOWN: 2561 case DCMD_LINEDOWN: 2562 case DCMD_LINK_FORWARD: 2563 case DCMD_LINK_BACK: 2564 case DCMD_LINK_NEXT: 2565 case DCMD_LINK_PREV: 2566 case DCMD_LINK_GO: 2567 case DCMD_END: 2568 case DCMD_GO_POS: 2569 case DCMD_GO_PAGE: 2570 case DCMD_MOVE_BY_CHAPTER: 2571 case DCMD_GO_SCROLL_POS: 2572 case DCMD_LINK_FIRST: 2573 case DCMD_SCROLL_BY: 2574 isMoveCommand = true; 2575 break; 2576 default: 2577 // do nothing 2578 break; 2579 } 2580 if (isMoveCommand && isBookLoaded()) 2581 updateCurrentPositionStatus(); 2582 } 2583 2584 public void done() { 2585 if (res) { 2586 invalidImages = true; 2587 drawPage(doneHandler, false); 2588 } 2589 if (isMoveCommand && isBookLoaded()) 2590 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 2591 } 2592 }); 2593 } 2594 2595 // update book and position info in status bar updateCurrentPositionStatus()2596 private void updateCurrentPositionStatus() { 2597 if (mBookInfo == null) 2598 return; 2599 // in background thread 2600 final FileInfo fileInfo = mBookInfo.getFileInfo(); 2601 if (fileInfo == null) 2602 return; 2603 final Bookmark bmk = doc != null ? doc.getCurrentPageBookmark() : null; 2604 final PositionProperties props = bmk != null ? doc.getPositionProps(bmk.getStartPos(), false) : null; 2605 if (props != null) BackgroundThread.instance().postGUI(() -> { 2606 mActivity.updateCurrentPositionStatus(fileInfo, bmk, props); 2607 2608 String fname = mBookInfo.getFileInfo().getBasePath(); 2609 if (fname != null && fname.length() > 0) 2610 setBookPositionForExternalShell(fname, props.pageNumber, props.pageCount); 2611 }); 2612 } 2613 doCommandFromBackgroundThread(final ReaderCommand cmd, final int param)2614 public void doCommandFromBackgroundThread(final ReaderCommand cmd, final int param) { 2615 log.d("doCommandFromBackgroundThread(" + cmd + ", " + param + ")"); 2616 BackgroundThread.ensureBackground(); 2617 boolean res = doc.doCommand(cmd.nativeId, param); 2618 if (res) { 2619 BackgroundThread.instance().executeGUI(this::drawPage); 2620 } 2621 } 2622 2623 volatile private boolean mInitialized = false; 2624 volatile private boolean mOpened = false; 2625 2626 //private File historyFile; 2627 updateLoadedBookInfo()2628 private void updateLoadedBookInfo() { 2629 BackgroundThread.ensureBackground(); 2630 // get title, authors, genres, etc. 2631 doc.updateBookInfo(mBookInfo); 2632 updateCurrentPositionStatus(); 2633 // check whether current book properties updated on another devices 2634 // TODO: fix and reenable 2635 //syncUpdater.syncExternalChanges(mBookInfo); 2636 } 2637 applySettings(Properties props)2638 private void applySettings(Properties props) { 2639 props = new Properties(props); // make a copy 2640 props.remove(PROP_TXT_OPTION_PREFORMATTED); 2641 props.remove(PROP_EMBEDDED_STYLES); 2642 props.remove(PROP_EMBEDDED_FONTS); 2643 props.remove(PROP_REQUESTED_DOM_VERSION); 2644 props.remove(PROP_RENDER_BLOCK_RENDERING_FLAGS); 2645 BackgroundThread.ensureBackground(); 2646 log.v("applySettings()"); 2647 boolean isFullScreen = props.getBool(PROP_APP_FULLSCREEN, false); 2648 props.setBool(PROP_SHOW_BATTERY, isFullScreen); 2649 props.setBool(PROP_SHOW_TIME, isFullScreen); 2650 String backgroundImageId = props.getProperty(PROP_PAGE_BACKGROUND_IMAGE); 2651 int backgroundColor = props.getColor(PROP_BACKGROUND_COLOR, 0xFFFFFF); 2652 setBackgroundTexture(backgroundImageId, backgroundColor); 2653 int statusLocation = props.getInt(PROP_STATUS_LOCATION, VIEWER_STATUS_PAGE_HEADER); 2654 int statusLine = 0; 2655 switch (statusLocation) { 2656 case VIEWER_STATUS_PAGE_HEADER: 2657 statusLine = 1; 2658 break; 2659 case VIEWER_STATUS_PAGE_FOOTER: 2660 statusLine = 2; 2661 break; 2662 } 2663 props.setInt(PROP_STATUS_LINE, statusLine); 2664 2665 if (!inDisabledFullRefresh()) { 2666 // If this function is called when new settings loaded from the cloud are applied, 2667 // we must prohibit changing the e-ink screen refresh mode, as this will lead to 2668 // a periodic full screen refresh when drawing the next phase of the progress bar. 2669 int updModeCode = props.getInt(PROP_APP_SCREEN_UPDATE_MODE, EinkScreen.EinkUpdateMode.Clear.code); 2670 int updInterval = props.getInt(PROP_APP_SCREEN_UPDATE_INTERVAL, 10); 2671 mActivity.setScreenUpdateMode(EinkScreen.EinkUpdateMode.byCode(updModeCode), surface); 2672 mActivity.setScreenUpdateInterval(updInterval, surface); 2673 } 2674 2675 if (null != mBookInfo) { 2676 FileInfo fileInfo = mBookInfo.getFileInfo(); 2677 final String bookLanguage = fileInfo.getLanguage(); 2678 final String fontFace = props.getProperty(PROP_FONT_FACE); 2679 String fcLangCode = null; 2680 if (null != bookLanguage && bookLanguage.length() > 0) { 2681 fcLangCode = Engine.findCompatibleFcLangCode(bookLanguage); 2682 if (props.getBool(PROP_TEXTLANG_EMBEDDED_LANGS_ENABLED, false)) 2683 props.setProperty(PROP_TEXTLANG_MAIN_LANG, bookLanguage); 2684 } 2685 if (null != fcLangCode && fcLangCode.length() > 0) { 2686 boolean res = Engine.checkFontLanguageCompatibility(fontFace, fcLangCode); 2687 log.d("Checking font \"" + fontFace + "\" for compatibility with language \"" + bookLanguage + "\" fcLangCode=" + fcLangCode + ": res=" + res); 2688 if (!res) { 2689 BackgroundThread.instance().executeGUI(() -> mActivity.showToast(R.string.font_not_compat_with_language, fontFace, bookLanguage)); 2690 } 2691 } else { 2692 if (null != bookLanguage) 2693 log.d("Can't find compatible language code in embedded FontConfig catalog: language=\"" + bookLanguage + "\" bookInfo=" + fileInfo); 2694 } 2695 } 2696 doc.applySettings(props); 2697 //syncViewSettings(props, save, saveDelayed); 2698 drawPage(); 2699 } 2700 eq(Object obj1, Object obj2)2701 public static boolean eq(Object obj1, Object obj2) { 2702 if (obj1 == null && obj2 == null) 2703 return true; 2704 if (obj1 == null || obj2 == null) 2705 return false; 2706 return obj1.equals(obj2); 2707 } 2708 saveSettings(Properties settings)2709 public void saveSettings(Properties settings) { 2710 mActivity.setSettings(settings, 0, false); 2711 } 2712 2713 /** 2714 * Read JNI view settings, update and save if changed 2715 */ syncViewSettings(final Properties currSettings, final boolean save, final boolean saveDelayed)2716 private void syncViewSettings(final Properties currSettings, final boolean save, final boolean saveDelayed) { 2717 post(new Task() { 2718 Properties props; 2719 2720 public void work() { 2721 BackgroundThread.ensureBackground(); 2722 java.util.Properties internalProps = doc.getSettings(); 2723 props = new Properties(internalProps); 2724 } 2725 2726 public void done() { 2727 Properties changedSettings = props.diff(currSettings); 2728 for (Map.Entry<Object, Object> entry : changedSettings.entrySet()) { 2729 currSettings.setProperty((String) entry.getKey(), (String) entry.getValue()); 2730 } 2731 mSettings = currSettings; 2732 if (save) { 2733 mActivity.setSettings(mSettings, saveDelayed ? 5000 : 0, false); 2734 } else { 2735 mActivity.setSettings(mSettings, -1, false); 2736 } 2737 } 2738 }); 2739 } 2740 getSettings()2741 public Properties getSettings() { 2742 return new Properties(mSettings); 2743 } 2744 stringToInt(String value, int defValue)2745 static public int stringToInt(String value, int defValue) { 2746 if (value == null) 2747 return defValue; 2748 try { 2749 return Integer.valueOf(value); 2750 } catch (NumberFormatException e) { 2751 return defValue; 2752 } 2753 } 2754 getManualFileName()2755 private String getManualFileName() { 2756 Scanner s = Services.getScanner(); 2757 if (s != null) { 2758 FileInfo fi = s.getDownloadDirectory(); 2759 if (fi != null) { 2760 File bookDir = new File(fi.getPathName()); 2761 return HelpFileGenerator.getHelpFileName(bookDir, mActivity.getCurrentLanguage()).getAbsolutePath(); 2762 } 2763 } 2764 log.e("cannot get manual file name!"); 2765 return null; 2766 } 2767 generateManual()2768 private File generateManual() { 2769 HelpFileGenerator generator = new HelpFileGenerator(mActivity, mEngine, getSettings(), mActivity.getCurrentLanguage()); 2770 FileInfo downloadDir = Services.getScanner().getDownloadDirectory(); 2771 File bookDir; 2772 if (downloadDir != null) 2773 bookDir = new File(Services.getScanner().getDownloadDirectory().getPathName()); 2774 else { 2775 log.e("cannot download directory file name!"); 2776 bookDir = new File("/tmp/"); 2777 } 2778 int settingsHash = generator.getSettingsHash(); 2779 String helpFileContentId = mActivity.getCurrentLanguage() + settingsHash + "v" + mActivity.getVersion(); 2780 String lastHelpFileContentId = mActivity.getLastGeneratedHelpFileSignature(); 2781 File manual = generator.getHelpFileName(bookDir); 2782 if (!manual.exists() || lastHelpFileContentId == null || !lastHelpFileContentId.equals(helpFileContentId)) { 2783 log.d("Generating help file " + manual.getAbsolutePath()); 2784 mActivity.setLastGeneratedHelpFileSignature(helpFileContentId); 2785 manual = generator.generateHelpFile(bookDir); 2786 } 2787 return manual; 2788 } 2789 2790 /** 2791 * Generate help file (if necessary) and show it. 2792 * 2793 * @return true if opened successfully 2794 */ showManual()2795 public boolean showManual() { 2796 return loadDocument(getManualFileName(), null, () -> mActivity.showToast("Error while opening manual")); 2797 } 2798 2799 private boolean hiliteTapZoneOnTap = false; 2800 private boolean enableVolumeKeys = true; 2801 static private final int DEF_PAGE_FLIP_MS = 300; 2802 applyAppSetting(String key, String value)2803 public void applyAppSetting(String key, String value) { 2804 boolean flg = "1".equals(value); 2805 if (key.equals(PROP_APP_TAP_ZONE_HILIGHT)) { 2806 hiliteTapZoneOnTap = flg; 2807 } else if (key.equals(PROP_APP_DOUBLE_TAP_SELECTION)) { 2808 doubleTapSelectionEnabled = flg; 2809 } else if (key.equals(PROP_APP_GESTURE_PAGE_FLIPPING)) { 2810 mGesturePageFlipsPerFullSwipe = Integer.valueOf(value); 2811 } else if (key.equals(PROP_PAGE_VIEW_MODE)) { 2812 mIsPageMode = flg; 2813 } else if (key.equals(PROP_APP_SECONDARY_TAP_ACTION_TYPE)) { 2814 secondaryTapActionType = flg ? TAP_ACTION_TYPE_DOUBLE : TAP_ACTION_TYPE_LONGPRESS; 2815 } else if (key.equals(PROP_APP_FLICK_BACKLIGHT_CONTROL)) { 2816 isBacklightControlFlick = "1".equals(value) ? 1 : ("2".equals(value) ? 2 : 0); 2817 } else if (key.equals(PROP_APP_FLICK_WARMLIGHT_CONTROL)) { 2818 isWarmBacklightControlFlick = "1".equals(value) ? 1 : ("2".equals(value) ? 2 : 0); 2819 } else if (PROP_APP_HIGHLIGHT_BOOKMARKS.equals(key)) { 2820 flgHighlightBookmarks = !"0".equals(value); 2821 clearSelection(); 2822 } else if (PROP_APP_VIEW_AUTOSCROLL_SPEED.equals(key)) { 2823 autoScrollSpeed = Utils.parseInt(value, 1500, 200, 10000); 2824 } else if (PROP_PAGE_ANIMATION.equals(key)) { 2825 pageFlipAnimationMode = Utils.parseInt(value, PAGE_ANIMATION_SLIDE2, PAGE_ANIMATION_NONE, PAGE_ANIMATION_MAX); 2826 pageFlipAnimationSpeedMs = pageFlipAnimationMode != PAGE_ANIMATION_NONE ? DEF_PAGE_FLIP_MS : 0; 2827 } else if (PROP_CONTROLS_ENABLE_VOLUME_KEYS.equals(key)) { 2828 enableVolumeKeys = flg; 2829 } else if (PROP_APP_SELECTION_ACTION.equals(key)) { 2830 mSelectionAction = Utils.parseInt(value, SELECTION_ACTION_TOOLBAR); 2831 } else if (PROP_APP_MULTI_SELECTION_ACTION.equals(key)) { 2832 mMultiSelectionAction = Utils.parseInt(value, SELECTION_ACTION_TOOLBAR); 2833 } else { 2834 //mActivity.applyAppSetting(key, value); 2835 } 2836 // 2837 } 2838 setAppSettings(Properties newSettings, Properties oldSettings)2839 public void setAppSettings(Properties newSettings, Properties oldSettings) { 2840 log.v("setAppSettings()"); //|| keyCode == KeyEvent.KEYCODE_DPAD_LEFT 2841 BackgroundThread.ensureGUI(); 2842 if (oldSettings == null) 2843 oldSettings = mSettings; 2844 Properties changedSettings = newSettings.diff(oldSettings); 2845 for (Map.Entry<Object, Object> entry : changedSettings.entrySet()) { 2846 String key = (String) entry.getKey(); 2847 String value = (String) entry.getValue(); 2848 applyAppSetting(key, value); 2849 if (PROP_APP_FULLSCREEN.equals(key)) { 2850 boolean flg = mSettings.getBool(PROP_APP_FULLSCREEN, false); 2851 newSettings.setBool(PROP_SHOW_BATTERY, flg); 2852 newSettings.setBool(PROP_SHOW_TIME, flg); 2853 } else if (PROP_PAGE_VIEW_MODE.equals(key)) { 2854 boolean flg = "1".equals(value); 2855 viewMode = flg ? ViewMode.PAGES : ViewMode.SCROLL; 2856 } else if (PROP_APP_SCREEN_ORIENTATION.equals(key) 2857 || PROP_PAGE_ANIMATION.equals(key) 2858 || PROP_PAGE_VIEW_MODE.equals(key) 2859 || PROP_CONTROLS_ENABLE_VOLUME_KEYS.equals(key) 2860 || PROP_APP_SHOW_COVERPAGES.equals(key) 2861 || PROP_APP_COVERPAGE_SIZE.equals(key) 2862 || PROP_APP_SCREEN_BACKLIGHT.equals(key) 2863 || PROP_APP_SCREEN_WARM_BACKLIGHT.equals(key) 2864 || PROP_APP_BOOK_PROPERTY_SCAN_ENABLED.equals(key) 2865 || PROP_APP_SCREEN_BACKLIGHT_LOCK.equals(key) 2866 || PROP_APP_TAP_ZONE_HILIGHT.equals(key) 2867 || PROP_APP_DICTIONARY.equals(key) 2868 || PROP_APP_DOUBLE_TAP_SELECTION.equals(key) 2869 || PROP_APP_FLICK_BACKLIGHT_CONTROL.equals(key) 2870 || PROP_APP_FLICK_WARMLIGHT_CONTROL.equals(key) 2871 || PROP_APP_FILE_BROWSER_HIDE_EMPTY_FOLDERS.equals(key) 2872 || PROP_APP_FILE_BROWSER_HIDE_EMPTY_GENRES.equals(key) 2873 || PROP_APP_SELECTION_ACTION.equals(key) 2874 || PROP_APP_FILE_BROWSER_SIMPLE_MODE.equals(key) 2875 || PROP_APP_GESTURE_PAGE_FLIPPING.equals(key) 2876 || PROP_APP_HIGHLIGHT_BOOKMARKS.equals(key) 2877 || PROP_HIGHLIGHT_SELECTION_COLOR.equals(key) 2878 || PROP_HIGHLIGHT_BOOKMARK_COLOR_COMMENT.equals(key) 2879 || PROP_HIGHLIGHT_BOOKMARK_COLOR_CORRECTION.equals(key) 2880 // TODO: redesign all this mess! 2881 ) { 2882 newSettings.setProperty(key, value); 2883 } 2884 } 2885 } 2886 getViewMode()2887 public ViewMode getViewMode() { 2888 return viewMode; 2889 } 2890 2891 /** 2892 * Change settings. 2893 * 2894 * @param newSettings are new settings 2895 */ updateSettings(Properties newSettings)2896 public void updateSettings(Properties newSettings) { 2897 log.v("updateSettings() " + newSettings.toString()); 2898 log.v("oldNightMode=" + mSettings.getProperty(PROP_NIGHT_MODE) + " newNightMode=" + newSettings.getProperty(PROP_NIGHT_MODE)); 2899 BackgroundThread.ensureGUI(); 2900 final Properties currSettings = new Properties(mSettings); 2901 if (null != ttsToolbar) { 2902 // ignore all non TTS options if TTS is active... 2903 ttsToolbar.setAppSettings(newSettings, currSettings); 2904 Properties changedSettings = newSettings.diff(currSettings); 2905 currSettings.setAll(changedSettings); 2906 mSettings = currSettings; 2907 } else { 2908 setAppSettings(newSettings, currSettings); 2909 Properties changedSettings = newSettings.diff(currSettings); 2910 currSettings.setAll(changedSettings); 2911 mSettings = currSettings; 2912 BackgroundThread.instance().postBackground(() -> applySettings(currSettings)); 2913 } 2914 } 2915 setBackgroundTexture(String textureId, int color)2916 private void setBackgroundTexture(String textureId, int color) { 2917 BackgroundTextureInfo[] textures = mEngine.getAvailableTextures(); 2918 for (BackgroundTextureInfo item : textures) { 2919 if (item.id.equals(textureId)) { 2920 setBackgroundTexture(item, color); 2921 return; 2922 } 2923 } 2924 setBackgroundTexture(Engine.NO_TEXTURE, color); 2925 } 2926 setBackgroundTexture(BackgroundTextureInfo texture, int color)2927 private void setBackgroundTexture(BackgroundTextureInfo texture, int color) { 2928 log.v("setBackgroundTexture(" + texture + ", " + color + ")"); 2929 if (!currentBackgroundTexture.equals(texture) || currentBackgroundColor != color) { 2930 log.d("setBackgroundTexture( " + texture + " )"); 2931 currentBackgroundColor = color; 2932 currentBackgroundTexture = texture; 2933 byte[] data = mEngine.getImageData(currentBackgroundTexture); 2934 doc.setPageBackgroundTexture(data, texture.tiled ? 1 : 0); 2935 currentBackgroundTextureTiled = texture.tiled; 2936 if (data != null && data.length > 0) { 2937 if (currentBackgroundTextureBitmap != null) 2938 currentBackgroundTextureBitmap.recycle(); 2939 try { 2940 currentBackgroundTextureBitmap = android.graphics.BitmapFactory.decodeByteArray(data, 0, data.length); 2941 } catch (Exception e) { 2942 log.e("Exception while decoding image data", e); 2943 currentBackgroundTextureBitmap = null; 2944 } 2945 } else { 2946 currentBackgroundTextureBitmap = null; 2947 } 2948 } 2949 } 2950 2951 BackgroundTextureInfo currentBackgroundTexture = Engine.NO_TEXTURE; 2952 Bitmap currentBackgroundTextureBitmap = null; 2953 boolean currentBackgroundTextureTiled = false; 2954 int currentBackgroundColor = 0; 2955 2956 class CreateViewTask extends Task { 2957 Properties props = new Properties(); 2958 CreateViewTask(Properties props)2959 public CreateViewTask(Properties props) { 2960 this.props = props; 2961 Properties oldSettings = new Properties(); // may be changed by setAppSettings 2962 setAppSettings(props, oldSettings); 2963 props.setAll(oldSettings); 2964 mSettings = props; 2965 } 2966 work()2967 public void work() throws Exception { 2968 BackgroundThread.ensureBackground(); 2969 log.d("CreateViewTask - in background thread"); 2970 // BackgroundTextureInfo[] textures = mEngine.getAvailableTextures(); 2971 // byte[] data = mEngine.getImageData(textures[3]); 2972 byte[] data = mEngine.getImageData(currentBackgroundTexture); 2973 doc.setPageBackgroundTexture(data, currentBackgroundTexture.tiled ? 1 : 0); 2974 2975 //File historyDir = activity.getDir("settings", Context.MODE_PRIVATE); 2976 //File historyDir = new File(Environment.getExternalStorageDirectory(), ".cr3"); 2977 //historyDir.mkdirs(); 2978 //File historyFile = new File(historyDir, "cr3hist.ini"); 2979 2980 //File historyFile = new File(activity.getDir("settings", Context.MODE_PRIVATE), "cr3hist.ini"); 2981 //if ( historyFile.exists() ) { 2982 //log.d("Reading history from file " + historyFile.getAbsolutePath()); 2983 //readHistoryInternal(historyFile.getAbsolutePath()); 2984 //} 2985 String css = mEngine.loadResourceUtf8(R.raw.fb2); 2986 if (css != null && css.length() > 0) 2987 doc.setStylesheet(css); 2988 applySettings(props); 2989 mInitialized = true; 2990 log.i("CreateViewTask - finished"); 2991 } 2992 done()2993 public void done() { 2994 log.d("InitializationFinishedEvent"); 2995 //BackgroundThread.ensureGUI(); 2996 //setSettings(props, new Properties()); 2997 } 2998 fail(Exception e)2999 public void fail(Exception e) { 3000 log.e("CoolReader engine initialization failed. Exiting.", e); 3001 mEngine.fatalError("Failed to init CoolReader engine"); 3002 } 3003 } 3004 closeIfOpened(final FileInfo fileInfo)3005 public void closeIfOpened(final FileInfo fileInfo) { 3006 if (this.mBookInfo != null && this.mBookInfo.getFileInfo().pathname.equals(fileInfo.pathname) && mOpened) { 3007 close(); 3008 } 3009 } 3010 reloadDocument()3011 public boolean reloadDocument() { 3012 if (this.mBookInfo != null && this.mBookInfo.getFileInfo() != null) { 3013 save(); // save current position 3014 post(new LoadDocumentTask(this.mBookInfo, null, null, null)); 3015 return true; 3016 } 3017 return false; 3018 } 3019 loadDocument(final FileInfo fileInfo, final Runnable doneHandler, final Runnable errorHandler)3020 public boolean loadDocument(final FileInfo fileInfo, final Runnable doneHandler, final Runnable errorHandler) { 3021 log.v("loadDocument(" + fileInfo.getPathName() + ")"); 3022 if (this.mBookInfo != null && this.mBookInfo.getFileInfo().pathname.equals(fileInfo.pathname) && mOpened) { 3023 log.d("trying to load already opened document"); 3024 mActivity.showReader(); 3025 if (null != doneHandler) 3026 doneHandler.run(); 3027 drawPage(); 3028 return false; 3029 } 3030 Services.getHistory().getOrCreateBookInfo(mActivity.getDB(), fileInfo, bookInfo -> { 3031 log.v("posting LoadDocument task to background thread"); 3032 BackgroundThread.instance().postBackground(() -> { 3033 log.v("posting LoadDocument task to GUI thread"); 3034 BackgroundThread.instance().postGUI(() -> { 3035 log.v("synced posting LoadDocument task to GUI thread"); 3036 post(new LoadDocumentTask(bookInfo, null, doneHandler, errorHandler)); 3037 }); 3038 }); 3039 }); 3040 return true; 3041 } 3042 loadDocumentFromStream(final InputStream inputStream, final FileInfo fileInfo, final Runnable doneHandler, final Runnable errorHandler)3043 public boolean loadDocumentFromStream(final InputStream inputStream, final FileInfo fileInfo, final Runnable doneHandler, final Runnable errorHandler) { 3044 log.v("loadDocument(" + fileInfo.getPathName() + ")"); 3045 if (this.mBookInfo != null && this.mBookInfo.getFileInfo().pathname.equals(fileInfo.pathname) && mOpened) { 3046 log.d("trying to load already opened document"); 3047 mActivity.showReader(); 3048 if (null != doneHandler) 3049 doneHandler.run(); 3050 drawPage(); 3051 return false; 3052 } 3053 Services.getHistory().getOrCreateBookInfo(mActivity.getDB(), fileInfo, bookInfo -> { 3054 log.v("posting LoadDocument task to background thread"); 3055 BackgroundThread.instance().postBackground(() -> { 3056 log.v("posting LoadDocument task to GUI thread"); 3057 BackgroundThread.instance().postGUI(() -> { 3058 log.v("synced posting LoadDocument task to GUI thread"); 3059 post(new LoadDocumentTask(bookInfo, inputStream, doneHandler, errorHandler)); 3060 }); 3061 }); 3062 }); 3063 return true; 3064 } 3065 loadDocument(String fileName, final Runnable doneHandler, final Runnable errorHandler)3066 public boolean loadDocument(String fileName, final Runnable doneHandler, final Runnable errorHandler) { 3067 BackgroundThread.ensureGUI(); 3068 save(); 3069 log.i("loadDocument(" + fileName + ")"); 3070 if (fileName == null) { 3071 log.v("loadDocument() : no filename specified"); 3072 if (errorHandler != null) 3073 errorHandler.run(); 3074 return false; 3075 } 3076 if ("@manual".equals(fileName)) { 3077 fileName = getManualFileName(); 3078 log.i("Manual document: " + fileName); 3079 } 3080 String normalized = mEngine.getPathCorrector().normalize(fileName); 3081 if (normalized == null) { 3082 log.e("Trying to load book from non-standard path " + fileName); 3083 mActivity.showToast("Trying to load book from non-standard path " + fileName); 3084 hideProgress(); 3085 if (errorHandler != null) 3086 errorHandler.run(); 3087 return false; 3088 } else if (!normalized.equals(fileName)) { 3089 log.w("Filename normalized to " + normalized); 3090 fileName = normalized; 3091 } 3092 if (fileName.equals(getManualFileName())) { 3093 // ensure manual file is up to date 3094 if (generateManual() == null) { 3095 log.v("loadDocument() : no filename specified"); 3096 if (errorHandler != null) 3097 errorHandler.run(); 3098 return false; 3099 } 3100 } 3101 BookInfo book = Services.getHistory().getBookInfo(fileName); 3102 if (book != null) 3103 log.v("loadDocument() : found book in history : " + book); 3104 FileInfo fi = null; 3105 if (book == null) { 3106 log.v("loadDocument() : book not found in history, looking for location directory"); 3107 FileInfo dir = Services.getScanner().findParent(new FileInfo(fileName), Services.getScanner().getRoot()); 3108 if (dir != null) { 3109 log.v("loadDocument() : document location found : " + dir); 3110 fi = dir.findItemByPathName(fileName); 3111 log.v("loadDocument() : item inside location : " + fi); 3112 } 3113 if (fi == null) { 3114 log.v("loadDocument() : no file item " + fileName + " found inside " + dir); 3115 if (errorHandler != null) 3116 errorHandler.run(); 3117 return false; 3118 } 3119 if (fi.isDirectory) { 3120 log.v("loadDocument() : is a directory, opening browser"); 3121 mActivity.showBrowser(fi); 3122 return true; 3123 } 3124 } else { 3125 fi = book.getFileInfo(); 3126 log.v("loadDocument() : item from history : " + fi); 3127 } 3128 return loadDocument(fi, doneHandler, errorHandler); 3129 } 3130 loadDocumentFromStream(InputStream inputStream, String contentPath, final Runnable doneHandler, final Runnable errorHandler)3131 public boolean loadDocumentFromStream(InputStream inputStream, String contentPath, final Runnable doneHandler, final Runnable errorHandler) { 3132 BackgroundThread.ensureGUI(); 3133 save(); 3134 log.i("loadDocument(" + contentPath + ")"); 3135 if (contentPath == null || inputStream == null) { 3136 log.v("loadDocument() : no filename or stream specified"); 3137 if (errorHandler != null) 3138 errorHandler.run(); 3139 return false; 3140 } 3141 BookInfo book = Services.getHistory().getBookInfo(contentPath); 3142 if (book != null) 3143 log.v("loadDocument() : found book in history : " + book); 3144 FileInfo fi = null; 3145 if (book == null) { 3146 log.v("loadDocument() : book not found in history, building FileInfo by Uri..."); 3147 fi = new FileInfo(contentPath); 3148 } else { 3149 fi = book.getFileInfo(); 3150 log.v("loadDocument() : item from history : " + fi); 3151 } 3152 return loadDocumentFromStream(inputStream, fi, doneHandler, errorHandler); 3153 } 3154 getBookInfo()3155 public BookInfo getBookInfo() { 3156 BackgroundThread.ensureGUI(); 3157 return mBookInfo; 3158 } 3159 3160 private int mBatteryState = 100; 3161 setBatteryState(int state)3162 public void setBatteryState(int state) { 3163 if (state != mBatteryState) { 3164 log.i("Battery state changed: " + state); 3165 mBatteryState = state; 3166 if (!DeviceInfo.EINK_SCREEN && !isAutoScrollActive()) { 3167 drawPage(); 3168 } 3169 } 3170 } 3171 getBatteryState()3172 public int getBatteryState() { 3173 return mBatteryState; 3174 } 3175 3176 private static final VMRuntimeHack runtime = new VMRuntimeHack(); 3177 3178 private static class BitmapFactory { 3179 public static final int MAX_FREE_LIST_SIZE = 2; 3180 ArrayList<Bitmap> freeList = new ArrayList<Bitmap>(); 3181 ArrayList<Bitmap> usedList = new ArrayList<Bitmap>(); 3182 get(int dx, int dy)3183 public synchronized Bitmap get(int dx, int dy) { 3184 for (int i = 0; i < freeList.size(); i++) { 3185 Bitmap bmp = freeList.get(i); 3186 if (bmp.getWidth() == dx && bmp.getHeight() == dy) { 3187 // found bitmap of proper size 3188 freeList.remove(i); 3189 usedList.add(bmp); 3190 //log.d("BitmapFactory: reused free bitmap, used list = " + usedList.size() + ", free list=" + freeList.size()); 3191 return bmp; 3192 } 3193 } 3194 for (int i = freeList.size() - 1; i >= 0; i--) { 3195 Bitmap bmp = freeList.remove(i); 3196 runtime.trackAlloc(bmp.getWidth() * bmp.getHeight() * 2); 3197 //log.d("Recycling free bitmap "+bmp.getWidth()+"x"+bmp.getHeight()); 3198 //bmp.recycle(); //20110109 3199 } 3200 Bitmap bmp = Bitmap.createBitmap(dx, dy, DeviceInfo.BUFFER_COLOR_FORMAT); 3201 runtime.trackFree(dx * dy * 2); 3202 //bmp.setDensity(0); 3203 usedList.add(bmp); 3204 //log.d("Created new bitmap "+dx+"x"+dy+". New bitmap list size = " + usedList.size()); 3205 return bmp; 3206 } 3207 compact()3208 public synchronized void compact() { 3209 while (freeList.size() > 0) { 3210 //freeList.get(0).recycle();//20110109 3211 Bitmap bmp = freeList.remove(0); 3212 runtime.trackAlloc(bmp.getWidth() * bmp.getHeight() * 2); 3213 } 3214 } 3215 release(Bitmap bmp)3216 public synchronized void release(Bitmap bmp) { 3217 for (int i = 0; i < usedList.size(); i++) { 3218 if (usedList.get(i) == bmp) { 3219 freeList.add(bmp); 3220 usedList.remove(i); 3221 while (freeList.size() > MAX_FREE_LIST_SIZE) { 3222 //freeList.get(0).recycle(); //20110109 3223 Bitmap b = freeList.remove(0); 3224 runtime.trackAlloc(b.getWidth() * b.getHeight() * 2); 3225 //b.recycle(); 3226 } 3227 log.d("BitmapFactory: bitmap released, used size = " + usedList.size() + ", free size=" + freeList.size()); 3228 return; 3229 } 3230 } 3231 // unknown bitmap, just recycle 3232 //bmp.recycle();//20110109 3233 } 3234 } 3235 3236 ; 3237 BitmapFactory factory = new BitmapFactory(); 3238 3239 class BitmapInfo { 3240 Bitmap bitmap; 3241 PositionProperties position; 3242 ImageInfo imageInfo; 3243 recycle()3244 void recycle() { 3245 factory.release(bitmap); 3246 bitmap = null; 3247 position = null; 3248 imageInfo = null; 3249 } 3250 isReleased()3251 boolean isReleased() { 3252 return bitmap == null; 3253 } 3254 3255 @Override toString()3256 public String toString() { 3257 return "BitmapInfo [position=" + position + "]"; 3258 } 3259 3260 } 3261 3262 private BitmapInfo mCurrentPageInfo; 3263 private BitmapInfo mNextPageInfo; 3264 3265 /** 3266 * Prepare and cache page image. 3267 * Cache is represented by two slots: mCurrentPageInfo and mNextPageInfo. 3268 * If page already exists in cache, returns it (if current page requested, 3269 * ensures that it became stored as mCurrentPageInfo; if another page requested, 3270 * no mCurrentPageInfo/mNextPageInfo reordering made). 3271 * 3272 * @param offset is kind of page: 0==current, -1=previous, 1=next page 3273 * @return page image and properties, null if requested page is unavailable (e.g. requested next/prev page is out of document range) 3274 */ preparePageImage(int offset)3275 private BitmapInfo preparePageImage(int offset) { 3276 BackgroundThread.ensureBackground(); 3277 log.v("preparePageImage( " + offset + ")"); 3278 //if (offset == 0) { 3279 // // DEBUG stack trace 3280 // try { 3281 // if (currentAutoScrollAnimation!=null) 3282 // log.v("preparePageImage from autoscroll"); 3283 // throw new Exception("stack trace"); 3284 // } catch (Exception e) { 3285 // Log.d("cr3", "stack trace", e); 3286 // } 3287 //} 3288 if (invalidImages) { 3289 if (mCurrentPageInfo != null) 3290 mCurrentPageInfo.recycle(); 3291 mCurrentPageInfo = null; 3292 if (mNextPageInfo != null) 3293 mNextPageInfo.recycle(); 3294 mNextPageInfo = null; 3295 invalidImages = false; 3296 } 3297 3298 if (internalDX == 0 || internalDY == 0) { 3299 if (requestedWidth > 0 && requestedHeight > 0) { 3300 internalDX = requestedWidth; 3301 internalDY = requestedHeight; 3302 doc.resize(internalDX, internalDY); 3303 } else { 3304 internalDX = surface.getWidth(); 3305 internalDY = surface.getHeight(); 3306 doc.resize(internalDX, internalDY); 3307 } 3308 // internalDX=200; 3309 // internalDY=300; 3310 // doc.resize(internalDX, internalDY); 3311 // BackgroundThread.instance().postGUI(new Runnable() { 3312 // @Override 3313 // public void run() { 3314 // log.d("invalidating view due to resize"); 3315 // //ReaderView.this.invalidate(); 3316 // drawPage(null, false); 3317 // //redraw(); 3318 // } 3319 // }); 3320 } 3321 3322 if (currentImageViewer != null) 3323 return currentImageViewer.prepareImage(); 3324 3325 PositionProperties currpos = doc.getPositionProps(null, false); 3326 if (null == currpos) 3327 return null; 3328 3329 boolean isPageView = currpos.pageMode != 0; 3330 3331 BitmapInfo currposBitmap = null; 3332 if (mCurrentPageInfo != null && mCurrentPageInfo.position != null && mCurrentPageInfo.position.equals(currpos) && mCurrentPageInfo.imageInfo == null) 3333 currposBitmap = mCurrentPageInfo; 3334 else if (mNextPageInfo != null && mNextPageInfo.position != null && mNextPageInfo.position.equals(currpos) && mNextPageInfo.imageInfo == null) 3335 currposBitmap = mNextPageInfo; 3336 if (offset == 0) { 3337 // Current page requested 3338 if (currposBitmap != null) { 3339 if (mNextPageInfo == currposBitmap) { 3340 // reorder pages 3341 BitmapInfo tmp = mNextPageInfo; 3342 mNextPageInfo = mCurrentPageInfo; 3343 mCurrentPageInfo = tmp; 3344 } 3345 // found ready page image 3346 return mCurrentPageInfo; 3347 } 3348 if (mCurrentPageInfo != null) { 3349 mCurrentPageInfo.recycle(); 3350 mCurrentPageInfo = null; 3351 } 3352 BitmapInfo bi = new BitmapInfo(); 3353 bi.position = currpos; 3354 bi.bitmap = factory.get(internalDX > 0 ? internalDX : requestedWidth, 3355 internalDY > 0 ? internalDY : requestedHeight); 3356 doc.setBatteryState(mBatteryState); 3357 doc.getPageImage(bi.bitmap); 3358 mCurrentPageInfo = bi; 3359 //log.v("Prepared new current page image " + mCurrentPageInfo); 3360 return mCurrentPageInfo; 3361 } 3362 if (isPageView) { 3363 // PAGES: one of next or prev pages requested, offset is specified as param 3364 int cmd1 = offset > 0 ? ReaderCommand.DCMD_PAGEDOWN.nativeId : ReaderCommand.DCMD_PAGEUP.nativeId; 3365 int cmd2 = offset > 0 ? ReaderCommand.DCMD_PAGEUP.nativeId : ReaderCommand.DCMD_PAGEDOWN.nativeId; 3366 if (offset < 0) 3367 offset = -offset; 3368 if (doc.doCommand(cmd1, offset)) { 3369 // can move to next page 3370 PositionProperties nextpos = doc.getPositionProps(null, false); 3371 BitmapInfo nextposBitmap = null; 3372 if (mCurrentPageInfo != null && mCurrentPageInfo.position != null && mCurrentPageInfo.position.equals(nextpos)) 3373 nextposBitmap = mCurrentPageInfo; 3374 else if (mNextPageInfo != null && mNextPageInfo.position != null && mNextPageInfo.position.equals(nextpos)) 3375 nextposBitmap = mNextPageInfo; 3376 if (nextposBitmap == null) { 3377 // existing image not found in cache, overriding mNextPageInfo 3378 if (mNextPageInfo != null) 3379 mNextPageInfo.recycle(); 3380 mNextPageInfo = null; 3381 BitmapInfo bi = new BitmapInfo(); 3382 bi.position = nextpos; 3383 bi.bitmap = factory.get(internalDX, internalDY); 3384 doc.setBatteryState(mBatteryState); 3385 doc.getPageImage(bi.bitmap); 3386 mNextPageInfo = bi; 3387 nextposBitmap = bi; 3388 //log.v("Prepared new current page image " + mNextPageInfo); 3389 } 3390 // return back to previous page 3391 doc.doCommand(cmd2, offset); 3392 return nextposBitmap; 3393 } else { 3394 // cannot move to page: out of document range 3395 return null; 3396 } 3397 } else { 3398 // SCROLL next or prev page requested, with pixel offset specified 3399 int y = currpos.y + offset; 3400 if (doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, y)) { 3401 PositionProperties nextpos = doc.getPositionProps(null, false); 3402 BitmapInfo nextposBitmap = null; 3403 if (mCurrentPageInfo != null && mCurrentPageInfo.position != null && mCurrentPageInfo.position.equals(nextpos)) 3404 nextposBitmap = mCurrentPageInfo; 3405 else if (mNextPageInfo != null && mNextPageInfo.position != null && mNextPageInfo.position.equals(nextpos)) 3406 nextposBitmap = mNextPageInfo; 3407 if (nextposBitmap == null) { 3408 // existing image not found in cache, overriding mNextPageInfo 3409 if (mNextPageInfo != null) 3410 mNextPageInfo.recycle(); 3411 mNextPageInfo = null; 3412 BitmapInfo bi = new BitmapInfo(); 3413 bi.position = nextpos; 3414 bi.bitmap = factory.get(internalDX, internalDY); 3415 doc.setBatteryState(mBatteryState); 3416 doc.getPageImage(bi.bitmap); 3417 mNextPageInfo = bi; 3418 nextposBitmap = bi; 3419 } 3420 // return back to prev position 3421 doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, currpos.y); 3422 return nextposBitmap; 3423 } else { 3424 return null; 3425 } 3426 } 3427 3428 } 3429 3430 private int lastDrawTaskId = 0; 3431 3432 private class DrawPageTask extends Task { 3433 final int id; 3434 BitmapInfo bi; 3435 Runnable doneHandler; 3436 boolean isPartially; 3437 DrawPageTask(Runnable doneHandler, boolean isPartially)3438 DrawPageTask(Runnable doneHandler, boolean isPartially) { 3439 // // DEBUG stack trace 3440 // try { 3441 // throw new Exception("DrawPageTask() stack trace"); 3442 // } catch (Exception e) { 3443 // Log.d("cr3", "stack trace", e); 3444 // } 3445 this.id = ++lastDrawTaskId; 3446 this.doneHandler = doneHandler; 3447 this.isPartially = isPartially; 3448 cancelGc(); 3449 } 3450 work()3451 public void work() { 3452 BackgroundThread.ensureBackground(); 3453 if (this.id != lastDrawTaskId) { 3454 log.d("skipping duplicate drawPage request"); 3455 return; 3456 } 3457 nextHiliteId++; 3458 if (currentAnimation != null) { 3459 log.d("skipping drawPage request while scroll animation is in progress"); 3460 return; 3461 } 3462 log.e("DrawPageTask.work(" + internalDX + "," + internalDY + ")"); 3463 bi = preparePageImage(0); 3464 if (bi != null) { 3465 bookView.draw(isPartially); 3466 } 3467 } 3468 3469 @Override done()3470 public void done() { 3471 BackgroundThread.ensureGUI(); 3472 // log.d("drawPage : bitmap is ready, invalidating view to draw new bitmap"); 3473 // if ( bi!=null ) { 3474 // setBitmap( bi.bitmap ); 3475 // invalidate(); 3476 // } 3477 // if (mOpened) 3478 //hideProgress(); 3479 if (doneHandler != null) 3480 doneHandler.run(); 3481 scheduleGc(); 3482 } 3483 3484 @Override fail(Exception e)3485 public void fail(Exception e) { 3486 hideProgress(); 3487 } 3488 } 3489 3490 ; 3491 3492 static class ReaderSurfaceView extends SurfaceView { ReaderSurfaceView(Context context)3493 public ReaderSurfaceView(Context context) { 3494 super(context); 3495 } 3496 } 3497 3498 // private boolean mIsOnFront = false; 3499 private int requestedWidth = 0; 3500 private int requestedHeight = 0; 3501 // public void setOnFront(boolean front) { 3502 // if (mIsOnFront == front) 3503 // return; 3504 // mIsOnFront = front; 3505 // log.d("setOnFront(" + front + ")"); 3506 // if (mIsOnFront) { 3507 // checkSize(); 3508 // } else { 3509 // // save position immediately 3510 // scheduleSaveCurrentPositionBookmark(0); 3511 // } 3512 // } 3513 requestResize(int width, int height)3514 private void requestResize(int width, int height) { 3515 requestedWidth = width; 3516 requestedHeight = height; 3517 if (requestedWidth <= 0) 3518 requestedWidth = 80; 3519 if (requestedHeight <= 0) 3520 requestedHeight = 80; 3521 checkSize(); 3522 } 3523 checkSize()3524 private void checkSize() { 3525 boolean changed = (requestedWidth != internalDX) || (requestedHeight != internalDY); 3526 if (!changed) 3527 return; 3528 if (getActivity().isDialogActive()) { 3529 log.d("checkSize() : dialog is active, skipping resize"); 3530 return; 3531 } 3532 // if (mIsOnFront || !mOpened) { 3533 log.d("checkSize() : calling resize"); 3534 resize(); 3535 // } else { 3536 // log.d("Skipping resize request"); 3537 // } 3538 } 3539 resize()3540 private void resize() { 3541 final int thisId = ++lastResizeTaskId; 3542 // if ( w<h && mActivity.isLandscape() ) { 3543 // log.i("ignoring size change to portrait since landscape is set"); 3544 // return; 3545 // } 3546 // if ( mActivity.isPaused() ) { 3547 // log.i("ignoring size change since activity is paused"); 3548 // return; 3549 // } 3550 // update size with delay: chance to avoid extra unnecessary resizing 3551 3552 Runnable task = () -> { 3553 if (thisId != lastResizeTaskId) { 3554 log.d("skipping duplicate resize request in GUI thread"); 3555 return; 3556 } 3557 post(new Task() { 3558 public void work() { 3559 BackgroundThread.ensureBackground(); 3560 if (thisId != lastResizeTaskId) { 3561 log.d("skipping duplicate resize request"); 3562 return; 3563 } 3564 internalDX = requestedWidth; 3565 internalDY = requestedHeight; 3566 log.d("ResizeTask: resizeInternal(" + internalDX + "," + internalDY + ")"); 3567 doc.resize(internalDX, internalDY); 3568 // if ( mOpened ) { 3569 // log.d("ResizeTask: done, drawing page"); 3570 // drawPage(); 3571 // } 3572 } 3573 3574 public void done() { 3575 clearImageCache(); 3576 drawPage(null, false); 3577 //redraw(); 3578 } 3579 }); 3580 }; 3581 3582 long timeSinceLastResume = System.currentTimeMillis() - lastAppResumeTs; 3583 int delay = 300; 3584 3585 if (timeSinceLastResume < 1000) 3586 delay = 1000; 3587 3588 if (mOpened) { 3589 log.d("scheduling delayed resize task id=" + thisId + " for " + delay + " ms"); 3590 BackgroundThread.instance().postGUI(task, delay); 3591 } else { 3592 log.d("executing resize without delay"); 3593 task.run(); 3594 } 3595 } 3596 3597 int hackMemorySize = 0; 3598 3599 // SurfaceView callbacks 3600 @Override surfaceChanged(SurfaceHolder holder, int format, final int width, final int height)3601 public void surfaceChanged(SurfaceHolder holder, int format, final int width, 3602 final int height) { 3603 log.i("surfaceChanged(" + width + ", " + height + ")"); 3604 3605 if (hackMemorySize <= 0) { 3606 hackMemorySize = width * height * 2; 3607 runtime.trackFree(hackMemorySize); 3608 } 3609 3610 3611 surface.invalidate(); 3612 //if (!isProgressActive()) 3613 bookView.draw(); 3614 //requestResize(width, height); 3615 //draw(); 3616 } 3617 3618 boolean mSurfaceCreated = false; 3619 3620 @Override surfaceCreated(SurfaceHolder holder)3621 public void surfaceCreated(SurfaceHolder holder) { 3622 log.i("surfaceCreated()"); 3623 mSurfaceCreated = true; 3624 //draw(); 3625 } 3626 3627 @Override surfaceDestroyed(SurfaceHolder holder)3628 public void surfaceDestroyed(SurfaceHolder holder) { 3629 log.i("surfaceDestroyed()"); 3630 mSurfaceCreated = false; 3631 if (hackMemorySize > 0) { 3632 runtime.trackAlloc(hackMemorySize); 3633 hackMemorySize = 0; 3634 } 3635 } 3636 3637 enum AnimationType { 3638 SCROLL, // for scroll mode 3639 PAGE_SHIFT, // for simple page shift 3640 } 3641 3642 3643 private ViewAnimationControl currentAnimation = null; 3644 3645 private int pageFlipAnimationSpeedMs = DEF_PAGE_FLIP_MS; // if 0 : no animation 3646 private int pageFlipAnimationMode = PAGE_ANIMATION_SLIDE2; //PAGE_ANIMATION_PAPER; // if 0 : no animation 3647 3648 // private void animatePageFlip( final int dir ) { 3649 // animatePageFlip(dir, null); 3650 // } animatePageFlip(final int dir, final Runnable onFinishHandler)3651 private void animatePageFlip(final int dir, final Runnable onFinishHandler) { 3652 if (!mOpened) 3653 return; 3654 BackgroundThread.instance().executeBackground(() -> { 3655 BackgroundThread.ensureBackground(); 3656 if (currentAnimation == null) { 3657 PositionProperties currPos = doc.getPositionProps(null, false); 3658 if (currPos == null) 3659 return; 3660 if (mCurrentPageInfo == null) 3661 return; 3662 int w = currPos.pageWidth; 3663 int h = currPos.pageHeight; 3664 int dir2 = dir; 3665 // if ( currPos.pageMode==2 ) 3666 // if ( dir2==1 ) 3667 // dir2 = 2; 3668 // else if ( dir2==-1 ) 3669 // dir2 = -2; 3670 int speed = pageFlipAnimationSpeedMs; 3671 if (onFinishHandler != null) 3672 speed = pageFlipAnimationSpeedMs / 2; 3673 if (currPos.pageMode != 0) { 3674 int fromX = dir2 > 0 ? w : 0; 3675 int toX = dir2 > 0 ? 0 : w; 3676 new PageViewAnimation(fromX, w, dir2); 3677 if (currentAnimation != null) { 3678 nextHiliteId++; 3679 hiliteRect = null; 3680 currentAnimation.update(toX, h / 2); 3681 currentAnimation.move(speed, true); 3682 currentAnimation.stop(-1, -1); 3683 if (onFinishHandler != null) 3684 BackgroundThread.instance().executeGUI(onFinishHandler); 3685 } 3686 } else { 3687 //new ScrollViewAnimation(startY, maxY); 3688 int fromY = dir > 0 ? h * 7 / 8 : 0; 3689 int toY = dir > 0 ? 0 : h * 7 / 8; 3690 new ScrollViewAnimation(fromY, h); 3691 if (currentAnimation != null) { 3692 nextHiliteId++; 3693 hiliteRect = null; 3694 currentAnimation.update(w / 2, toY); 3695 currentAnimation.move(speed, true); 3696 currentAnimation.stop(-1, -1); 3697 if (onFinishHandler != null) 3698 BackgroundThread.instance().executeGUI(onFinishHandler); 3699 } 3700 } 3701 } 3702 }); 3703 } 3704 tapZoneBounds(int startX, int startY, int maxX, int maxY)3705 static private Rect tapZoneBounds(int startX, int startY, int maxX, int maxY) { 3706 if (startX < 0) 3707 startX = 0; 3708 if (startY < 0) 3709 startY = 0; 3710 if (startX > maxX) 3711 startX = maxX; 3712 if (startY > maxY) 3713 startY = maxY; 3714 int dx = (maxX + 2) / 3; 3715 int dy = (maxY + 2) / 3; 3716 int x0 = startX / dx * dx; 3717 int y0 = startY / dy * dy; 3718 return new Rect(x0, y0, x0 + dx, y0 + dy); 3719 } 3720 3721 volatile private int nextHiliteId = 0; 3722 private final static int HILITE_RECT_ALPHA = 32; 3723 private Rect hiliteRect = null; 3724 unhiliteTapZone()3725 private void unhiliteTapZone() { 3726 hiliteTapZone(false, 0, 0, surface.getWidth(), surface.getHeight()); 3727 } 3728 hiliteTapZone(final boolean hilite, final int startX, final int startY, final int maxX, final int maxY)3729 private void hiliteTapZone(final boolean hilite, final int startX, final int startY, final int maxX, final int maxY) { 3730 alog.d("highliteTapZone(" + startX + ", " + startY + ")"); 3731 final int myHiliteId = ++nextHiliteId; 3732 int txcolor = mSettings.getColor(PROP_FONT_COLOR, Color.BLACK); 3733 final int color = (txcolor & 0xFFFFFF) | (HILITE_RECT_ALPHA << 24); 3734 BackgroundThread.instance().executeBackground(() -> { 3735 if (myHiliteId != nextHiliteId || (!hilite && hiliteRect == null)) 3736 return; 3737 3738 if (currentAutoScrollAnimation != null) { 3739 hiliteRect = null; 3740 return; 3741 } 3742 3743 BackgroundThread.ensureBackground(); 3744 final BitmapInfo pageImage = preparePageImage(0); 3745 if (pageImage != null && pageImage.bitmap != null && pageImage.position != null) { 3746 //PositionProperties currPos = pageImage.position; 3747 final Rect rc = hilite ? tapZoneBounds(startX, startY, maxX, maxY) : hiliteRect; 3748 if (hilite) 3749 hiliteRect = rc; 3750 else 3751 hiliteRect = null; 3752 if (rc != null) 3753 drawCallback(canvas -> { 3754 if (mInitialized && mCurrentPageInfo != null) { 3755 log.d("onDraw() -- drawing page image"); 3756 drawDimmedBitmap(canvas, mCurrentPageInfo.bitmap, rc, rc); 3757 if (hilite) { 3758 Paint p = new Paint(); 3759 p.setColor(color); 3760 // if ( true ) { 3761 canvas.drawRect(new Rect(rc.left, rc.top, rc.right - 2, rc.top + 2), p); 3762 canvas.drawRect(new Rect(rc.left, rc.top + 2, rc.left + 2, rc.bottom - 2), p); 3763 canvas.drawRect(new Rect(rc.right - 2 - 2, rc.top + 2, rc.right - 2, rc.bottom - 2), p); 3764 canvas.drawRect(new Rect(rc.left + 2, rc.bottom - 2 - 2, rc.right - 2 - 2, rc.bottom - 2), p); 3765 // } else { 3766 // canvas.drawRect(rc, p); 3767 // } 3768 } 3769 } 3770 }, rc, false); 3771 } 3772 }); 3773 } 3774 scheduleUnhilite(int delay)3775 private void scheduleUnhilite(int delay) { 3776 final int myHiliteId = nextHiliteId; 3777 BackgroundThread.instance().postGUI(() -> { 3778 if (myHiliteId == nextHiliteId && hiliteRect != null) 3779 unhiliteTapZone(); 3780 }, delay); 3781 } 3782 3783 int currentBrightnessValueIndex = -1; 3784 int currentBrightnessValue = -1; 3785 int currentBrightnessPrevYPos = -1; 3786 startBrightnessControl(final int startX, final int startY, int type)3787 private void startBrightnessControl(final int startX, final int startY, int type) { 3788 switch (type) { 3789 case BRIGHTNESS_TYPE_COMMON: 3790 currentBrightnessValue = mActivity.getScreenBacklightLevel(); 3791 if (!DeviceInfo.EINK_SCREEN) { 3792 currentBrightnessValueIndex = OptionsDialog.findBacklightSettingIndex(currentBrightnessValue); 3793 if (0 == currentBrightnessValueIndex) { // system backlight level 3794 // A trick that allows you to reduce the brightness of the backlight 3795 // if the brightness is set to the same as in the system. 3796 currentBrightnessValue = 50; 3797 currentBrightnessValueIndex = OptionsDialog.findBacklightSettingIndex(currentBrightnessValue); 3798 } 3799 } 3800 else if (DeviceInfo.EINK_HAVE_FRONTLIGHT) 3801 currentBrightnessValueIndex = Utils.findNearestIndex(mEinkScreen.getFrontLightLevels(mActivity), currentBrightnessValue); 3802 break; 3803 case BRIGHTNESS_TYPE_WARM: 3804 currentBrightnessValue = mActivity.getWarmBacklightLevel(); 3805 if (DeviceInfo.EINK_HAVE_NATURAL_BACKLIGHT) 3806 currentBrightnessValueIndex = Utils.findNearestIndex(mEinkScreen.getWarmLightLevels(mActivity), currentBrightnessValue); 3807 break; 3808 default: 3809 return; 3810 } 3811 currentBrightnessPrevYPos = startY; 3812 updateBrightnessControl(startX, startY, type); 3813 } 3814 updateBrightnessControl(final int x, final int y, int type)3815 private void updateBrightnessControl(final int x, final int y, int type) { 3816 List<Integer> levelList = null; 3817 int count = 0; 3818 switch (type) { 3819 case BRIGHTNESS_TYPE_COMMON: 3820 if (!DeviceInfo.EINK_SCREEN) 3821 count = OptionsDialog.mBacklightLevels.length; 3822 else if (null != mEinkScreen) { 3823 levelList = mEinkScreen.getFrontLightLevels(mActivity); 3824 if (null != levelList) 3825 count = levelList.size(); 3826 else 3827 return; 3828 } 3829 break; 3830 case BRIGHTNESS_TYPE_WARM: 3831 if (null != mEinkScreen) { 3832 levelList = mEinkScreen.getWarmLightLevels(mActivity); 3833 if (null != levelList) 3834 count = levelList.size(); 3835 else 3836 return; 3837 } 3838 break; 3839 default: 3840 return; 3841 } 3842 if (0 == count) 3843 return; 3844 int diff = count*(currentBrightnessPrevYPos - y)/surface.getHeight(); 3845 int index = currentBrightnessValueIndex + diff; 3846 if (index < 0) 3847 index = 0; 3848 else if (index >= count) 3849 index = count - 1; 3850 if (!DeviceInfo.EINK_SCREEN) { 3851 if (index == 0) { 3852 // ignore system brightness level on non eink devices 3853 currentBrightnessPrevYPos = y; 3854 return; 3855 } 3856 } 3857 if (index != currentBrightnessValueIndex) { 3858 currentBrightnessValueIndex = index; 3859 if (!DeviceInfo.EINK_SCREEN) 3860 currentBrightnessValue = OptionsDialog.mBacklightLevels[currentBrightnessValueIndex]; 3861 else { 3862 // Here levelList already != null 3863 currentBrightnessValue = levelList.get(currentBrightnessValueIndex); 3864 } 3865 switch (type) { 3866 case BRIGHTNESS_TYPE_COMMON: 3867 mActivity.setScreenBacklightLevel(currentBrightnessValue); 3868 break; 3869 case BRIGHTNESS_TYPE_WARM: 3870 mActivity.setScreenWarmBacklightLevel(currentBrightnessValue); 3871 break; 3872 } 3873 currentBrightnessPrevYPos = y; 3874 } 3875 } 3876 stopBrightnessControl(final int x, final int y, int type)3877 private void stopBrightnessControl(final int x, final int y, int type) { 3878 if (currentBrightnessValueIndex >= 0) { 3879 if (x >= 0 && y >= 0) { 3880 updateBrightnessControl(x, y, type); 3881 } 3882 switch (type) { 3883 case BRIGHTNESS_TYPE_COMMON: 3884 mSettings.setInt(PROP_APP_SCREEN_BACKLIGHT, currentBrightnessValue); 3885 break; 3886 case BRIGHTNESS_TYPE_WARM: 3887 mSettings.setInt(PROP_APP_SCREEN_WARM_BACKLIGHT, currentBrightnessValue); 3888 break; 3889 default: 3890 return; 3891 } 3892 if (showBrightnessFlickToast) { 3893 OptionsDialog.mBacklightLevelsTitles[0] = mActivity.getString(R.string.options_app_backlight_screen_default); 3894 String s = OptionsDialog.mBacklightLevelsTitles[currentBrightnessValueIndex]; 3895 mActivity.showToast(s); 3896 } 3897 if (!DeviceInfo.EINK_SCREEN) 3898 saveSettings(mSettings); 3899 currentBrightnessValue = -1; 3900 currentBrightnessValueIndex = -1; 3901 currentBrightnessPrevYPos = -1; 3902 } 3903 } 3904 3905 private static final boolean showBrightnessFlickToast = false; 3906 3907 startAnimation(final int startX, final int startY, final int maxX, final int maxY, final int newX, final int newY)3908 private void startAnimation(final int startX, final int startY, final int maxX, final int maxY, final int newX, final int newY) { 3909 if (!mOpened) 3910 return; 3911 alog.d("startAnimation(" + startX + ", " + startY + ")"); 3912 BackgroundThread.instance().executeBackground(() -> { 3913 BackgroundThread.ensureBackground(); 3914 PositionProperties currPos = doc.getPositionProps(null, false); 3915 if (currPos != null && currPos.pageMode != 0) { 3916 //int dir = startX > maxX/2 ? currPos.pageMode : -currPos.pageMode; 3917 //int dir = startX > maxX/2 ? 1 : -1; 3918 int dir = newX - startX < 0 ? 1 : -1; 3919 int sx = startX; 3920 // if ( dir<0 ) 3921 // sx = 0; 3922 new PageViewAnimation(sx, maxX, dir); 3923 } else { 3924 new ScrollViewAnimation(startY, maxY); 3925 } 3926 if (currentAnimation != null) { 3927 nextHiliteId++; 3928 hiliteRect = null; 3929 } 3930 }); 3931 } 3932 3933 private volatile int updateSerialNumber = 0; 3934 3935 private class AnimationUpdate { 3936 private int x; 3937 private int y; 3938 3939 //ViewAnimationControl myAnimation; set(int x, int y)3940 public void set(int x, int y) { 3941 this.x = x; 3942 this.y = y; 3943 } 3944 AnimationUpdate(int x, int y)3945 public AnimationUpdate(int x, int y) { 3946 this.x = x; 3947 this.y = y; 3948 //this.myAnimation = currentAnimation; 3949 scheduleUpdate(); 3950 } 3951 scheduleUpdate()3952 private void scheduleUpdate() { 3953 BackgroundThread.instance().postBackground(() -> { 3954 alog.d("updating(" + x + ", " + y + ")"); 3955 boolean animate = false; 3956 synchronized (AnimationUpdate.class) { 3957 3958 if (currentAnimation != null && currentAnimationUpdate == AnimationUpdate.this) { 3959 currentAnimationUpdate = null; 3960 currentAnimation.update(x, y); 3961 animate = true; 3962 } 3963 } 3964 if (animate) 3965 currentAnimation.animate(); 3966 }); 3967 } 3968 3969 } 3970 3971 private AnimationUpdate currentAnimationUpdate; 3972 updateAnimation(final int x, final int y)3973 private void updateAnimation(final int x, final int y) { 3974 if (!mOpened) 3975 return; 3976 alog.d("updateAnimation(" + x + ", " + y + ")"); 3977 synchronized (AnimationUpdate.class) { 3978 if (currentAnimationUpdate != null) 3979 currentAnimationUpdate.set(x, y); 3980 else 3981 currentAnimationUpdate = new AnimationUpdate(x, y); 3982 } 3983 try { 3984 // give a chance to background thread to process event faster 3985 Thread.sleep(0); 3986 } catch (InterruptedException e) { 3987 // ignore 3988 } 3989 } 3990 stopAnimation(final int x, final int y)3991 private void stopAnimation(final int x, final int y) { 3992 if (!mOpened) 3993 return; 3994 alog.d("stopAnimation(" + x + ", " + y + ")"); 3995 BackgroundThread.instance().executeBackground(() -> { 3996 if (currentAnimation != null) { 3997 currentAnimation.stop(x, y); 3998 } 3999 }); 4000 } 4001 4002 DelayedExecutor animationScheduler = DelayedExecutor.createBackground("animation"); 4003 scheduleAnimation()4004 private void scheduleAnimation() { 4005 if (!mOpened) 4006 return; 4007 animationScheduler.post(() -> { 4008 if (currentAnimation != null) { 4009 currentAnimation.animate(); 4010 } 4011 }); 4012 } 4013 4014 interface ViewAnimationControl { update(int x, int y)4015 public void update(int x, int y); 4016 stop(int x, int y)4017 public void stop(int x, int y); 4018 animate()4019 public void animate(); 4020 move(int duration, boolean accelerated)4021 public void move(int duration, boolean accelerated); 4022 isStarted()4023 public boolean isStarted(); 4024 draw(Canvas canvas)4025 abstract void draw(Canvas canvas); 4026 } 4027 4028 // private Object surfaceLock = new Object(); 4029 4030 private static final int[] accelerationShape = new int[]{ 4031 0, 6, 24, 54, 95, 146, 206, 273, 345, 421, 500, 578, 654, 726, 793, 853, 904, 945, 975, 993, 1000 4032 }; 4033 accelerate(int x0, int x1, int x)4034 static public int accelerate(int x0, int x1, int x) { 4035 if (x < x0) 4036 x = x0; 4037 if (x > x1) 4038 x = x1; 4039 int intervals = accelerationShape.length - 1; 4040 int pos = x1 > x0 ? 100 * intervals * (x - x0) / (x1 - x0) : x1; 4041 int interval = pos / 100; 4042 int part = pos % 100; 4043 if (interval < 0) 4044 interval = 0; 4045 else if (interval > intervals) 4046 interval = intervals; 4047 int y = interval == intervals ? 100000 : accelerationShape[interval] * 100 + (accelerationShape[interval + 1] - accelerationShape[interval]) * part; 4048 return x0 + (x1 - x0) * y / 100000; 4049 } 4050 4051 private interface DrawCanvasCallback { drawTo(Canvas c)4052 void drawTo(Canvas c); 4053 } 4054 drawCallback(DrawCanvasCallback callback, Rect rc, boolean isPartially)4055 private void drawCallback(DrawCanvasCallback callback, Rect rc, boolean isPartially) { 4056 if (!mSurfaceCreated) 4057 return; 4058 //synchronized(surfaceLock) { } 4059 //log.v("draw() - in thread " + Thread.currentThread().getName()); 4060 final SurfaceHolder holder = surface.getHolder(); 4061 //log.v("before synchronized(surfaceLock)"); 4062 if (holder != null) 4063 //synchronized(surfaceLock) 4064 { 4065 Canvas canvas = null; 4066 long startTs = android.os.SystemClock.uptimeMillis(); 4067 try { 4068 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 4069 canvas = holder.lockHardwareCanvas(); 4070 else 4071 canvas = holder.lockCanvas(rc); 4072 //log.v("before draw(canvas)"); 4073 if (canvas != null) { 4074 if (DeviceInfo.EINK_SCREEN) { 4075 // pre draw update 4076 //BackgroundThread.instance().executeGUI(() -> EinkScreen.PrepareController(surface, isPartially)); 4077 mEinkScreen.prepareController(surface, isPartially); 4078 } 4079 callback.drawTo(canvas); 4080 } 4081 } finally { 4082 //log.v("exiting finally"); 4083 if (canvas != null && surface.getHolder() != null) { 4084 //log.v("before unlockCanvasAndPost"); 4085 if (canvas != null && holder != null) { 4086 holder.unlockCanvasAndPost(canvas); 4087 //if ( rc==null ) { 4088 long endTs = android.os.SystemClock.uptimeMillis(); 4089 updateAnimationDurationStats(endTs - startTs); 4090 //} 4091 if (DeviceInfo.EINK_SCREEN) { 4092 // post draw update 4093 mEinkScreen.updateController(surface, isPartially); 4094 } 4095 } 4096 //log.v("after unlockCanvasAndPost"); 4097 } 4098 } 4099 } 4100 //log.v("exiting draw()"); 4101 } 4102 4103 abstract class ViewAnimationBase implements ViewAnimationControl { 4104 //long startTimeStamp; 4105 boolean started; 4106 isStarted()4107 public boolean isStarted() { 4108 return started; 4109 } 4110 ViewAnimationBase()4111 ViewAnimationBase() { 4112 //startTimeStamp = android.os.SystemClock.uptimeMillis(); 4113 cancelGc(); 4114 } 4115 close()4116 public void close() { 4117 animationScheduler.cancel(); 4118 currentAnimation = null; 4119 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 4120 lastSavedBookmark = null; 4121 updateCurrentPositionStatus(); 4122 4123 scheduleGc(); 4124 } 4125 draw()4126 public void draw() { 4127 draw(false); 4128 } 4129 draw(boolean isPartially)4130 public void draw(boolean isPartially) { 4131 // long startTs = android.os.SystemClock.uptimeMillis(); 4132 drawCallback(this::draw, null, isPartially); 4133 } 4134 } 4135 4136 //private static final int PAGE_ANIMATION_DURATION = 3000; 4137 class ScrollViewAnimation extends ViewAnimationBase { 4138 int startY; 4139 int maxY; 4140 int pageHeight; 4141 int fullHeight; 4142 int pointerStartPos; 4143 int pointerDestPos; 4144 int pointerCurrPos; 4145 BitmapInfo image1; 4146 BitmapInfo image2; 4147 ScrollViewAnimation(int startY, int maxY)4148 ScrollViewAnimation(int startY, int maxY) { 4149 super(); 4150 this.startY = startY; 4151 this.maxY = maxY; 4152 long start = android.os.SystemClock.uptimeMillis(); 4153 log.v("ScrollViewAnimation -- creating: drawing two pages to buffer"); 4154 PositionProperties currPos = doc.getPositionProps(null, false); 4155 int pos = currPos.y; 4156 int pos0 = pos - (maxY - startY); 4157 if (pos0 < 0) 4158 pos0 = 0; 4159 pointerStartPos = pos; 4160 pointerCurrPos = pos; 4161 pointerDestPos = startY; 4162 pageHeight = currPos.pageHeight; 4163 fullHeight = currPos.fullHeight; 4164 doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, pos0); 4165 image1 = preparePageImage(0); 4166 if (image1 == null) { 4167 log.v("ScrollViewAnimation -- not started: image is null"); 4168 return; 4169 } 4170 image2 = preparePageImage(image1.position.pageHeight); 4171 doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, pos); 4172 if (image2 == null) { 4173 log.v("ScrollViewAnimation -- not started: image is null"); 4174 return; 4175 } 4176 long duration = android.os.SystemClock.uptimeMillis() - start; 4177 log.v("ScrollViewAnimation -- created in " + duration + " millis"); 4178 currentAnimation = this; 4179 } 4180 4181 @Override stop(int x, int y)4182 public void stop(int x, int y) { 4183 if (currentAnimation == null) 4184 return; 4185 //if ( started ) { 4186 if (y != -1) { 4187 int delta = startY - y; 4188 pointerCurrPos = pointerStartPos + delta; 4189 } 4190 if (pointerCurrPos < 0) 4191 pointerCurrPos = 0; 4192 if (pointerCurrPos > fullHeight - pageHeight) 4193 pointerCurrPos = fullHeight - pageHeight; 4194 pointerDestPos = pointerCurrPos; 4195 draw(); 4196 doc.doCommand(ReaderCommand.DCMD_GO_POS.nativeId, pointerDestPos); 4197 //} 4198 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 4199 close(); 4200 } 4201 4202 @Override move(int duration, boolean accelerated)4203 public void move(int duration, boolean accelerated) { 4204 if (duration > 0 && pageFlipAnimationSpeedMs != 0) { 4205 int steps = (int) (duration / getAvgAnimationDrawDuration()) + 2; 4206 int x0 = pointerCurrPos; 4207 int x1 = pointerDestPos; 4208 if ((x0 - x1) < 10 && (x0 - x1) > -10) 4209 steps = 2; 4210 for (int i = 1; i < steps; i++) { 4211 int x = x0 + (x1 - x0) * i / steps; 4212 pointerCurrPos = accelerated ? accelerate(x0, x1, x) : x; 4213 if (pointerCurrPos < 0) 4214 pointerCurrPos = 0; 4215 if (pointerCurrPos > fullHeight - pageHeight) 4216 pointerCurrPos = fullHeight - pageHeight; 4217 draw(); 4218 } 4219 } 4220 pointerCurrPos = pointerDestPos; 4221 draw(); 4222 } 4223 4224 @Override update(int x, int y)4225 public void update(int x, int y) { 4226 int delta = startY - y; 4227 pointerDestPos = pointerStartPos + delta; 4228 if (pointerDestPos < 0) 4229 pointerDestPos = 0; 4230 if (pointerDestPos > fullHeight - pageHeight) 4231 pointerDestPos = fullHeight - pageHeight; 4232 } 4233 animate()4234 public void animate() { 4235 //log.d("animate() is called"); 4236 if (pointerDestPos != pointerCurrPos) { 4237 if (!started) 4238 started = true; 4239 if (pageFlipAnimationSpeedMs == 0) 4240 pointerCurrPos = pointerDestPos; 4241 else { 4242 int delta = pointerCurrPos - pointerDestPos; 4243 if (delta < 0) 4244 delta = -delta; 4245 long avgDraw = getAvgAnimationDrawDuration(); 4246 //int maxStep = (int)(maxY * PAGE_ANIMATION_DURATION / avgDraw); 4247 int maxStep = pageFlipAnimationSpeedMs > 0 ? (int) (maxY * 1000 / avgDraw / pageFlipAnimationSpeedMs) : maxY; 4248 int step; 4249 if (delta > maxStep * 2) 4250 step = maxStep; 4251 else 4252 step = (delta + 3) / 4; 4253 //int step = delta<3 ? 1 : (delta<5 ? 2 : (delta<10 ? 3 : (delta<15 ? 6 : (delta<25 ? 10 : (delta<50 ? 15 : 30))))); 4254 if (pointerCurrPos < pointerDestPos) 4255 pointerCurrPos += step; 4256 else 4257 pointerCurrPos -= step; 4258 log.d("animate(" + pointerCurrPos + " => " + pointerDestPos + " step=" + step + ")"); 4259 } 4260 //pointerCurrPos = pointerDestPos; 4261 draw(); 4262 if (pointerDestPos != pointerCurrPos) 4263 scheduleAnimation(); 4264 } 4265 } 4266 draw(Canvas canvas)4267 public void draw(Canvas canvas) { 4268 // BitmapInfo image1 = mCurrentPageInfo; 4269 // BitmapInfo image2 = mNextPageInfo; 4270 if (image1 == null || image1.isReleased() || image2 == null || image2.isReleased()) 4271 return; 4272 int h = image1.position.pageHeight; 4273 int rowsFromImg1 = image1.position.y + h - pointerCurrPos; 4274 int rowsFromImg2 = h - rowsFromImg1; 4275 Rect src1 = new Rect(0, h - rowsFromImg1, mCurrentPageInfo.bitmap.getWidth(), h); 4276 Rect dst1 = new Rect(0, 0, mCurrentPageInfo.bitmap.getWidth(), rowsFromImg1); 4277 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4278 if (image2 != null) { 4279 Rect src2 = new Rect(0, 0, mCurrentPageInfo.bitmap.getWidth(), rowsFromImg2); 4280 Rect dst2 = new Rect(0, rowsFromImg1, mCurrentPageInfo.bitmap.getWidth(), h); 4281 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4282 } 4283 //log.v("anim.drawScroll( pos=" + pointerCurrPos + ", " + src1 + "=>" + dst1 + ", " + src2 + "=>" + dst2 + " )"); 4284 } 4285 } 4286 4287 private final static int SIN_TABLE_SIZE = 1024; 4288 private final static int SIN_TABLE_SCALE = 0x10000; 4289 private final static int PI_DIV_2 = (int) (Math.PI / 2 * SIN_TABLE_SCALE); 4290 /// sin table, for 0..PI/2 4291 private static int[] SIN_TABLE = new int[SIN_TABLE_SIZE + 1]; 4292 private static int[] ASIN_TABLE = new int[SIN_TABLE_SIZE + 1]; 4293 // mapping of 0..1 shift to angle 4294 private static int[] SRC_TABLE = new int[SIN_TABLE_SIZE + 1]; 4295 // mapping of 0..1 shift to sin(angle) 4296 private static int[] DST_TABLE = new int[SIN_TABLE_SIZE + 1]; 4297 4298 // for dx=0..1 find such alpha (0..pi/2) that alpha - sin(alpha) = dx shiftfn(double dx)4299 private static double shiftfn(double dx) { 4300 double a = 0; 4301 double b = Math.PI / 2; 4302 double c = 0; 4303 for (int i = 0; i < 15; i++) { 4304 c = (a + b) / 2; 4305 double cq = c - Math.sin(c); 4306 if (cq < dx) 4307 a = c; 4308 else 4309 b = c; 4310 } 4311 return c; 4312 } 4313 4314 static { 4315 for (int i = 0; i <= SIN_TABLE_SIZE; i++) { 4316 double angle = Math.PI / 2 * i / SIN_TABLE_SIZE; 4317 int s = (int) Math.round(Math.sin(angle) * SIN_TABLE_SCALE); 4318 SIN_TABLE[i] = s; 4319 double x = (double) i / SIN_TABLE_SIZE; 4320 s = (int) Math.round(Math.asin(x) * SIN_TABLE_SCALE); 4321 ASIN_TABLE[i] = s; 4322 4323 double dx = i * (Math.PI / 2 - 1.0) / SIN_TABLE_SIZE; 4324 angle = shiftfn(dx); 4325 SRC_TABLE[i] = (int) Math.round(angle * SIN_TABLE_SCALE); 4326 DST_TABLE[i] = (int) Math.round(Math.sin(angle) * SIN_TABLE_SCALE); 4327 } 4328 } 4329 4330 class PageViewAnimation extends ViewAnimationBase { 4331 int startX; 4332 int maxX; 4333 int page1; 4334 int page2; 4335 int direction; 4336 int currShift; 4337 int destShift; 4338 int pageCount; 4339 Paint divPaint; 4340 Paint[] shadePaints; 4341 Paint[] hilitePaints; 4342 private final boolean naturalPageFlip; 4343 private final boolean flipTwoPages; 4344 4345 BitmapInfo image1; 4346 BitmapInfo image2; 4347 PageViewAnimation(int startX, int maxX, int direction)4348 PageViewAnimation(int startX, int maxX, int direction) { 4349 super(); 4350 this.startX = startX; 4351 this.maxX = maxX; 4352 this.direction = direction; 4353 this.currShift = 0; 4354 this.destShift = 0; 4355 this.naturalPageFlip = (pageFlipAnimationMode == PAGE_ANIMATION_PAPER); 4356 this.flipTwoPages = (pageFlipAnimationMode == PAGE_ANIMATION_SLIDE2); 4357 4358 long start = android.os.SystemClock.uptimeMillis(); 4359 log.v("PageViewAnimation -- creating: drawing two pages to buffer"); 4360 4361 PositionProperties currPos = mCurrentPageInfo == null ? null : mCurrentPageInfo.position; 4362 if (currPos == null) 4363 currPos = doc.getPositionProps(null, false); 4364 page1 = currPos.pageNumber; 4365 page2 = currPos.pageNumber + direction; 4366 if (page2 < 0 || page2 >= currPos.pageCount) { 4367 currentAnimation = null; 4368 return; 4369 } 4370 this.pageCount = currPos.pageMode; 4371 image1 = preparePageImage(0); 4372 image2 = preparePageImage(direction); 4373 if (image1 == null || image2 == null) { 4374 log.v("PageViewAnimation -- cannot start animation: page image is null"); 4375 return; 4376 } 4377 if (page1 == page2) { 4378 log.v("PageViewAnimation -- cannot start animation: not moved"); 4379 return; 4380 } 4381 page2 = image2.position.pageNumber; 4382 currentAnimation = this; 4383 divPaint = new Paint(); 4384 divPaint.setStyle(Paint.Style.FILL); 4385 divPaint.setColor(mActivity.isNightMode() ? Color.argb(96, 64, 64, 64) : Color.argb(128, 128, 128, 128)); 4386 final int numPaints = 16; 4387 shadePaints = new Paint[numPaints]; 4388 hilitePaints = new Paint[numPaints]; 4389 for (int i = 0; i < numPaints; i++) { 4390 shadePaints[i] = new Paint(); 4391 hilitePaints[i] = new Paint(); 4392 hilitePaints[i].setStyle(Paint.Style.FILL); 4393 shadePaints[i].setStyle(Paint.Style.FILL); 4394 if (mActivity.isNightMode()) { 4395 shadePaints[i].setColor(Color.argb((i + 1) * 96 / numPaints, 0, 0, 0)); 4396 hilitePaints[i].setColor(Color.argb((i + 1) * 96 / numPaints, 64, 64, 64)); 4397 } else { 4398 shadePaints[i].setColor(Color.argb((i + 1) * 96 / numPaints, 0, 0, 0)); 4399 hilitePaints[i].setColor(Color.argb((i + 1) * 96 / numPaints, 255, 255, 255)); 4400 } 4401 } 4402 4403 4404 long duration = android.os.SystemClock.uptimeMillis() - start; 4405 log.d("PageViewAnimation -- created in " + duration + " millis"); 4406 } 4407 drawGradient(Canvas canvas, Rect rc, Paint[] paints, int startIndex, int endIndex)4408 private void drawGradient(Canvas canvas, Rect rc, Paint[] paints, int startIndex, int endIndex) { 4409 int n = (startIndex < endIndex) ? endIndex - startIndex + 1 : startIndex - endIndex + 1; 4410 int dir = (startIndex < endIndex) ? 1 : -1; 4411 int dx = rc.right - rc.left; 4412 Rect rect = new Rect(rc); 4413 for (int i = 0; i < n; i++) { 4414 int index = startIndex + i * dir; 4415 int x1 = rc.left + dx * i / n; 4416 int x2 = rc.left + dx * (i + 1) / n; 4417 if (x2 > rc.right) 4418 x2 = rc.right; 4419 rect.left = x1; 4420 rect.right = x2; 4421 if (x2 > x1) { 4422 canvas.drawRect(rect, paints[index]); 4423 } 4424 } 4425 } 4426 drawShadow(Canvas canvas, Rect rc)4427 private void drawShadow(Canvas canvas, Rect rc) { 4428 drawGradient(canvas, rc, shadePaints, shadePaints.length / 2, shadePaints.length / 10); 4429 } 4430 4431 private final static int DISTORT_PART_PERCENT = 30; 4432 drawDistorted(Canvas canvas, Bitmap bmp, Rect src, Rect dst, int dir)4433 private void drawDistorted(Canvas canvas, Bitmap bmp, Rect src, Rect dst, int dir) { 4434 int srcdx = src.width(); 4435 int dstdx = dst.width(); 4436 int dx = srcdx - dstdx; 4437 int maxdistortdx = srcdx * DISTORT_PART_PERCENT / 100; 4438 int maxdx = maxdistortdx * (PI_DIV_2 - SIN_TABLE_SCALE) / SIN_TABLE_SCALE; 4439 int maxdistortsrc = maxdistortdx * PI_DIV_2 / SIN_TABLE_SCALE; 4440 4441 int distortdx = dx < maxdistortdx ? dx : maxdistortdx; 4442 int distortsrcstart = -1; 4443 int distortsrcend = -1; 4444 int distortdststart = -1; 4445 int distortdstend = -1; 4446 int distortanglestart = -1; 4447 int distortangleend = -1; 4448 int normalsrcstart = -1; 4449 int normalsrcend = -1; 4450 int normaldststart = -1; 4451 int normaldstend = -1; 4452 4453 if (dx < maxdx) { 4454 // start 4455 int index = dx >= 0 ? dx * SIN_TABLE_SIZE / maxdx : 0; 4456 if (index > DST_TABLE.length) 4457 index = DST_TABLE.length; 4458 int dstv = DST_TABLE[index] * maxdistortdx / SIN_TABLE_SCALE; 4459 distortdststart = distortsrcstart = dstdx - dstv; 4460 distortsrcend = srcdx; 4461 distortdstend = dstdx; 4462 normalsrcstart = normaldststart = 0; 4463 normalsrcend = distortsrcstart; 4464 normaldstend = distortdststart; 4465 distortanglestart = 0; 4466 distortangleend = SRC_TABLE[index]; 4467 distortdx = maxdistortdx; 4468 } else if (dstdx > maxdistortdx) { 4469 // middle 4470 distortdststart = distortsrcstart = dstdx - maxdistortdx; 4471 distortsrcend = distortsrcstart + maxdistortsrc; 4472 distortdstend = dstdx; 4473 normalsrcstart = normaldststart = 0; 4474 normalsrcend = distortsrcstart; 4475 normaldstend = distortdststart; 4476 distortanglestart = 0; 4477 distortangleend = PI_DIV_2; 4478 } else { 4479 // end 4480 normalsrcstart = normaldststart = normalsrcend = normaldstend = -1; 4481 distortdx = dstdx; 4482 distortsrcstart = 0; 4483 int n = maxdistortdx >= dstdx ? maxdistortdx - dstdx : 0; 4484 distortsrcend = ASIN_TABLE[SIN_TABLE_SIZE * n / maxdistortdx] * maxdistortsrc / SIN_TABLE_SCALE; 4485 distortdststart = 0; 4486 distortdstend = dstdx; 4487 distortangleend = PI_DIV_2; 4488 n = maxdistortdx >= distortdx ? maxdistortdx - distortdx : 0; 4489 distortanglestart = ASIN_TABLE[SIN_TABLE_SIZE * (maxdistortdx - distortdx) / maxdistortdx]; 4490 } 4491 4492 Rect srcrc = new Rect(src); 4493 Rect dstrc = new Rect(dst); 4494 if (normalsrcstart < normalsrcend) { 4495 if (dir > 0) { 4496 srcrc.left = src.left + normalsrcstart; 4497 srcrc.right = src.left + normalsrcend; 4498 dstrc.left = dst.left + normaldststart; 4499 dstrc.right = dst.left + normaldstend; 4500 } else { 4501 srcrc.right = src.right - normalsrcstart; 4502 srcrc.left = src.right - normalsrcend; 4503 dstrc.right = dst.right - normaldststart; 4504 dstrc.left = dst.right - normaldstend; 4505 } 4506 drawDimmedBitmap(canvas, bmp, srcrc, dstrc); 4507 } 4508 if (distortdststart < distortdstend) { 4509 int n = distortdx / 5 + 1; 4510 int dst0 = SIN_TABLE[distortanglestart * SIN_TABLE_SIZE / PI_DIV_2] * maxdistortdx / SIN_TABLE_SCALE; 4511 int src0 = distortanglestart * maxdistortdx / SIN_TABLE_SCALE; 4512 for (int i = 0; i < n; i++) { 4513 int angledelta = distortangleend - distortanglestart; 4514 int startangle = distortanglestart + i * angledelta / n; 4515 int endangle = distortanglestart + (i + 1) * angledelta / n; 4516 int src1 = startangle * maxdistortdx / SIN_TABLE_SCALE - src0; 4517 int src2 = endangle * maxdistortdx / SIN_TABLE_SCALE - src0; 4518 int dst1 = SIN_TABLE[startangle * SIN_TABLE_SIZE / PI_DIV_2] * maxdistortdx / SIN_TABLE_SCALE - dst0; 4519 int dst2 = SIN_TABLE[endangle * SIN_TABLE_SIZE / PI_DIV_2] * maxdistortdx / SIN_TABLE_SCALE - dst0; 4520 int hiliteIndex = startangle * hilitePaints.length / PI_DIV_2; 4521 Paint[] paints; 4522 if (dir > 0) { 4523 dstrc.left = dst.left + distortdststart + dst1; 4524 dstrc.right = dst.left + distortdststart + dst2; 4525 srcrc.left = src.left + distortsrcstart + src1; 4526 srcrc.right = src.left + distortsrcstart + src2; 4527 paints = hilitePaints; 4528 } else { 4529 dstrc.right = dst.right - distortdststart - dst1; 4530 dstrc.left = dst.right - distortdststart - dst2; 4531 srcrc.right = src.right - distortsrcstart - src1; 4532 srcrc.left = src.right - distortsrcstart - src2; 4533 paints = shadePaints; 4534 } 4535 drawDimmedBitmap(canvas, bmp, srcrc, dstrc); 4536 canvas.drawRect(dstrc, paints[hiliteIndex]); 4537 } 4538 } 4539 } 4540 4541 @Override move(int duration, boolean accelerated)4542 public void move(int duration, boolean accelerated) { 4543 if (duration > 0 && pageFlipAnimationSpeedMs != 0) { 4544 int steps = (int) (duration / getAvgAnimationDrawDuration()) + 2; 4545 int x0 = currShift; 4546 int x1 = destShift; 4547 if ((x0 - x1) < 10 && (x0 - x1) > -10) 4548 steps = 2; 4549 for (int i = 1; i < steps; i++) { 4550 int x = x0 + (x1 - x0) * i / steps; 4551 currShift = accelerated ? accelerate(x0, x1, x) : x; 4552 draw(); 4553 } 4554 } 4555 currShift = destShift; 4556 draw(); 4557 } 4558 4559 @Override stop(int x, int y)4560 public void stop(int x, int y) { 4561 if (currentAnimation == null) 4562 return; 4563 alog.v("PageViewAnimation.stop(" + x + ", " + y + ")"); 4564 //if ( started ) { 4565 boolean moved = false; 4566 if (x != -1) { 4567 int threshold = mActivity.getPalmTipPixels() * 7 / 8; 4568 if (direction > 0) { 4569 // | <===== | 4570 int dx = startX - x; 4571 if (dx > threshold) 4572 moved = true; 4573 } else { 4574 // | =====> | 4575 int dx = x - startX; 4576 if (dx > threshold) 4577 moved = true; 4578 } 4579 int duration; 4580 if (moved) { 4581 destShift = maxX; 4582 duration = 300; // 500 ms forward 4583 } else { 4584 destShift = 0; 4585 duration = 200; // 200 ms cancel 4586 } 4587 move(duration, false); 4588 } else { 4589 moved = true; 4590 } 4591 doc.doCommand(ReaderCommand.DCMD_GO_PAGE_DONT_SAVE_HISTORY.nativeId, moved ? page2 : page1); 4592 //} 4593 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 4594 close(); 4595 // preparing images for next page flip 4596 preparePageImage(0); 4597 preparePageImage(direction); 4598 updateCurrentPositionStatus(); 4599 //if ( started ) 4600 // drawPage(); 4601 } 4602 4603 @Override update(int x, int y)4604 public void update(int x, int y) { 4605 alog.v("PageViewAnimation.update(" + x + ", " + y + ")"); 4606 int delta = direction > 0 ? startX - x : x - startX; 4607 if (delta <= 0) 4608 destShift = 0; 4609 else if (delta < maxX) 4610 destShift = delta; 4611 else 4612 destShift = maxX; 4613 } 4614 animate()4615 public void animate() { 4616 alog.v("PageViewAnimation.animate(" + currShift + " => " + destShift + ") speed=" + pageFlipAnimationSpeedMs); 4617 //log.d("animate() is called"); 4618 if (currShift != destShift) { 4619 started = true; 4620 if (pageFlipAnimationSpeedMs == 0) 4621 currShift = destShift; 4622 else { 4623 int delta = currShift - destShift; 4624 if (delta < 0) 4625 delta = -delta; 4626 long avgDraw = getAvgAnimationDrawDuration(); 4627 int maxStep = pageFlipAnimationSpeedMs > 0 ? (int) (maxX * 1000 / avgDraw / pageFlipAnimationSpeedMs) : maxX; 4628 int step; 4629 if (delta > maxStep * 2) 4630 step = maxStep; 4631 else 4632 step = (delta + 3) / 4; 4633 //int step = delta<3 ? 1 : (delta<5 ? 2 : (delta<10 ? 3 : (delta<15 ? 6 : (delta<25 ? 10 : (delta<50 ? 15 : 30))))); 4634 if (currShift < destShift) 4635 currShift += step; 4636 else if (currShift > destShift) 4637 currShift -= step; 4638 alog.v("PageViewAnimation.animate(" + currShift + " => " + destShift + " step=" + step + ")"); 4639 } 4640 //pointerCurrPos = pointerDestPos; 4641 draw(); 4642 if (currShift != destShift) 4643 scheduleAnimation(); 4644 } 4645 } 4646 draw(Canvas canvas)4647 public void draw(Canvas canvas) { 4648 alog.v("PageViewAnimation.draw(" + currShift + ")"); 4649 // BitmapInfo image1 = mCurrentPageInfo; 4650 // BitmapInfo image2 = mNextPageInfo; 4651 if (image1.isReleased() || image2.isReleased()) 4652 return; 4653 int w = image1.bitmap.getWidth(); 4654 int h = image1.bitmap.getHeight(); 4655 int div; 4656 if (direction > 0) { 4657 // FORWARD 4658 div = w - currShift; 4659 Rect shadowRect = new Rect(div, 0, div + w / 10, h); 4660 if (naturalPageFlip) { 4661 if (this.pageCount == 2) { 4662 int w2 = w / 2; 4663 if (div < w2) { 4664 // left - part of old page 4665 Rect src1 = new Rect(0, 0, div, h); 4666 Rect dst1 = new Rect(0, 0, div, h); 4667 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4668 // left, resized part of new page 4669 Rect src2 = new Rect(0, 0, w2, h); 4670 Rect dst2 = new Rect(div, 0, w2, h); 4671 //canvas.drawBitmap(image2.bitmap, src2, dst2, null); 4672 drawDistorted(canvas, image2.bitmap, src2, dst2, -1); 4673 // right, new page 4674 Rect src3 = new Rect(w2, 0, w, h); 4675 Rect dst3 = new Rect(w2, 0, w, h); 4676 drawDimmedBitmap(canvas, image2.bitmap, src3, dst3); 4677 4678 } else { 4679 // left - old page 4680 Rect src1 = new Rect(0, 0, w2, h); 4681 Rect dst1 = new Rect(0, 0, w2, h); 4682 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4683 // right, resized old page 4684 Rect src2 = new Rect(w2, 0, w, h); 4685 Rect dst2 = new Rect(w2, 0, div, h); 4686 //canvas.drawBitmap(image1.bitmap, src2, dst2, null); 4687 drawDistorted(canvas, image1.bitmap, src2, dst2, 1); 4688 // right, new page 4689 Rect src3 = new Rect(div, 0, w, h); 4690 Rect dst3 = new Rect(div, 0, w, h); 4691 drawDimmedBitmap(canvas, image2.bitmap, src3, dst3); 4692 4693 if (div > 0 && div < w) 4694 drawShadow(canvas, shadowRect); 4695 } 4696 } else { 4697 Rect src1 = new Rect(0, 0, w, h); 4698 Rect dst1 = new Rect(0, 0, w - currShift, h); 4699 //log.v("drawing " + image1); 4700 //canvas.drawBitmap(image1.bitmap, src1, dst1, null); 4701 drawDistorted(canvas, image1.bitmap, src1, dst1, 1); 4702 Rect src2 = new Rect(w - currShift, 0, w, h); 4703 Rect dst2 = new Rect(w - currShift, 0, w, h); 4704 //log.v("drawing " + image1); 4705 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4706 4707 if (div > 0 && div < w) 4708 drawShadow(canvas, shadowRect); 4709 } 4710 } else { 4711 if (flipTwoPages) { 4712 Rect src1 = new Rect(currShift, 0, w, h); 4713 Rect dst1 = new Rect(0, 0, w - currShift, h); 4714 //log.v("drawing " + image1); 4715 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4716 Rect src2 = new Rect(0, 0, currShift, h); 4717 Rect dst2 = new Rect(w - currShift, 0, w, h); 4718 //log.v("drawing " + image1); 4719 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4720 } else { 4721 Rect src1 = new Rect(currShift, 0, w, h); 4722 Rect dst1 = new Rect(0, 0, w - currShift, h); 4723 //log.v("drawing " + image1); 4724 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4725 Rect src2 = new Rect(w - currShift, 0, w, h); 4726 Rect dst2 = new Rect(w - currShift, 0, w, h); 4727 //log.v("drawing " + image1); 4728 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4729 } 4730 } 4731 } else { 4732 // BACK 4733 div = currShift; 4734 Rect shadowRect = new Rect(div, 0, div + 10, h); 4735 if (naturalPageFlip) { 4736 if (this.pageCount == 2) { 4737 int w2 = w / 2; 4738 if (div < w2) { 4739 // left - part of old page 4740 Rect src1 = new Rect(0, 0, div, h); 4741 Rect dst1 = new Rect(0, 0, div, h); 4742 drawDimmedBitmap(canvas, image2.bitmap, src1, dst1); 4743 // left, resized part of new page 4744 Rect src2 = new Rect(0, 0, w2, h); 4745 Rect dst2 = new Rect(div, 0, w2, h); 4746 //canvas.drawBitmap(image1.bitmap, src2, dst2, null); 4747 drawDistorted(canvas, image1.bitmap, src2, dst2, -1); 4748 // right, new page 4749 Rect src3 = new Rect(w2, 0, w, h); 4750 Rect dst3 = new Rect(w2, 0, w, h); 4751 drawDimmedBitmap(canvas, image1.bitmap, src3, dst3); 4752 } else { 4753 // left - old page 4754 Rect src1 = new Rect(0, 0, w2, h); 4755 Rect dst1 = new Rect(0, 0, w2, h); 4756 drawDimmedBitmap(canvas, image2.bitmap, src1, dst1); 4757 // right, resized old page 4758 Rect src2 = new Rect(w2, 0, w, h); 4759 Rect dst2 = new Rect(w2, 0, div, h); 4760 //canvas.drawBitmap(image2.bitmap, src2, dst2, null); 4761 drawDistorted(canvas, image2.bitmap, src2, dst2, 1); 4762 // right, new page 4763 Rect src3 = new Rect(div, 0, w, h); 4764 Rect dst3 = new Rect(div, 0, w, h); 4765 drawDimmedBitmap(canvas, image1.bitmap, src3, dst3); 4766 4767 if (div > 0 && div < w) 4768 drawShadow(canvas, shadowRect); 4769 } 4770 } else { 4771 Rect src1 = new Rect(currShift, 0, w, h); 4772 Rect dst1 = new Rect(currShift, 0, w, h); 4773 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4774 Rect src2 = new Rect(0, 0, w, h); 4775 Rect dst2 = new Rect(0, 0, currShift, h); 4776 //canvas.drawBitmap(image2.bitmap, src2, dst2, null); 4777 drawDistorted(canvas, image2.bitmap, src2, dst2, 1); 4778 4779 if (div > 0 && div < w) 4780 drawShadow(canvas, shadowRect); 4781 } 4782 } else { 4783 if (flipTwoPages) { 4784 Rect src1 = new Rect(0, 0, w - currShift, h); 4785 Rect dst1 = new Rect(currShift, 0, w, h); 4786 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4787 Rect src2 = new Rect(w - currShift, 0, w, h); 4788 Rect dst2 = new Rect(0, 0, currShift, h); 4789 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4790 } else { 4791 Rect src1 = new Rect(currShift, 0, w, h); 4792 Rect dst1 = new Rect(currShift, 0, w, h); 4793 drawDimmedBitmap(canvas, image1.bitmap, src1, dst1); 4794 Rect src2 = new Rect(w - currShift, 0, w, h); 4795 Rect dst2 = new Rect(0, 0, currShift, h); 4796 drawDimmedBitmap(canvas, image2.bitmap, src2, dst2); 4797 } 4798 } 4799 } 4800 if (div > 0 && div < w) { 4801 canvas.drawLine(div, 0, div, h, divPaint); 4802 } 4803 } 4804 } 4805 4806 private static final class RingBuffer { 4807 private long [] mArray; 4808 private long mSum; 4809 private long mAvg; 4810 private int mPos; 4811 private int mCount; 4812 private int mSize; 4813 RingBuffer(int size, long initialAvg)4814 public RingBuffer(int size, long initialAvg) { 4815 mSize = size; 4816 mArray = new long[size]; 4817 mPos = 0; 4818 mCount = 0; 4819 mAvg = initialAvg; 4820 mSum = 0; 4821 } 4822 average()4823 public long average() { 4824 return mAvg; 4825 } 4826 add(long val)4827 public void add(long val) { 4828 if (mCount < mSize) 4829 mCount++; 4830 else // array is full 4831 mSum -= mArray[mPos]; // subtract from sum the value to replace 4832 mArray[mPos] = val; // write new value 4833 mSum += val; // update sum 4834 mAvg = mSum /mCount; // calculate average value 4835 mPos++; 4836 if (mPos >= mSize) 4837 mPos = 0; 4838 } 4839 } 4840 4841 RingBuffer mAvgDrawAnimationStats = new RingBuffer(16, 50); 4842 getAvgAnimationDrawDuration()4843 private long getAvgAnimationDrawDuration() { 4844 return mAvgDrawAnimationStats.average(); 4845 } 4846 updateAnimationDurationStats(long duration)4847 private void updateAnimationDurationStats(long duration) { 4848 if (duration <= 0) 4849 duration = 1; 4850 else if (duration > 1000) 4851 return; 4852 mAvgDrawAnimationStats.add(duration); 4853 } 4854 drawPage()4855 private void drawPage() { 4856 drawPage(null, false); 4857 } 4858 drawPage(boolean isPartially)4859 private void drawPage(boolean isPartially) { 4860 drawPage(null, isPartially); 4861 } 4862 drawPage(Runnable doneHandler, boolean isPartially)4863 private void drawPage(Runnable doneHandler, boolean isPartially) { 4864 if (!mInitialized) 4865 return; 4866 log.v("drawPage() : submitting DrawPageTask"); 4867 if (mOpened) 4868 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 4869 post(new DrawPageTask(doneHandler, isPartially)); 4870 } 4871 4872 private int internalDX = 0; 4873 private int internalDY = 0; 4874 4875 private byte[] coverPageBytes = null; 4876 findCoverPage()4877 private void findCoverPage() { 4878 log.d("document is loaded succesfull, checking coverpage data"); 4879 byte[] coverpageBytes = doc.getCoverPageData(); 4880 if (coverpageBytes != null) { 4881 log.d("Found cover page data: " + coverpageBytes.length + " bytes"); 4882 coverPageBytes = coverpageBytes; 4883 } 4884 } 4885 4886 private int currentProgressPosition = 1; 4887 private int currentProgressTitleId = R.string.progress_loading; 4888 private String currentProgressTitle = null; 4889 private int currentCloudSyncProgressPosition = -1; 4890 4891 private int savedEinkUpdateInterval = -1; 4892 private final HashSet<Integer> einkModeClients = new HashSet<Integer>(); 4893 requestDisableFullRefresh(int id)4894 private void requestDisableFullRefresh(int id) { 4895 if (-1 == savedEinkUpdateInterval) { 4896 savedEinkUpdateInterval = mEinkScreen.getUpdateInterval(); 4897 // current e-ink screen update mode without full refresh 4898 mEinkScreen.setupController(mEinkScreen.getUpdateMode(), 0, surface); 4899 } 4900 einkModeClients.add(id); 4901 } 4902 releaseDisableFullRefresh(int id)4903 private void releaseDisableFullRefresh(int id) { 4904 einkModeClients.remove(id); 4905 if (einkModeClients.isEmpty()) { 4906 // restore e-ink full screen refresh period 4907 mEinkScreen.setupController(mEinkScreen.getUpdateMode(), savedEinkUpdateInterval, surface); 4908 savedEinkUpdateInterval = -1; 4909 } 4910 } 4911 inDisabledFullRefresh()4912 private boolean inDisabledFullRefresh() { 4913 return !einkModeClients.isEmpty(); 4914 } 4915 showProgress(int position, int titleResource)4916 private void showProgress(int position, int titleResource) { 4917 log.v("showProgress(" + position + ")"); 4918 boolean first = currentProgressTitleId == 0; 4919 boolean update = false; 4920 if (null == currentProgressTitle || currentProgressTitleId != titleResource) { 4921 currentProgressTitleId = titleResource; 4922 currentProgressTitle = mActivity.getString(currentProgressTitleId); 4923 update = true; 4924 } 4925 if (currentProgressPosition != position || currentProgressTitleId != titleResource) { 4926 currentProgressPosition = position; 4927 update = true; 4928 } 4929 if (update) { 4930 if (DeviceInfo.EINK_SCREEN) 4931 requestDisableFullRefresh(1); 4932 bookView.draw(!first); 4933 } 4934 } 4935 hideProgress()4936 private void hideProgress() { 4937 log.v("hideProgress()"); 4938 if (currentProgressTitleId != 0) { 4939 currentProgressPosition = -1; 4940 currentProgressTitleId = 0; 4941 currentProgressTitle = null; 4942 if (DeviceInfo.EINK_SCREEN) 4943 releaseDisableFullRefresh(1); 4944 bookView.draw(false); 4945 } 4946 } 4947 isProgressActive()4948 private boolean isProgressActive() { 4949 return currentProgressPosition > 0; 4950 } 4951 showCloudSyncProgress(int progress)4952 public void showCloudSyncProgress(int progress) { 4953 log.v("showClodSyncProgress(" + progress + ")"); 4954 if (currentCloudSyncProgressPosition != progress) { 4955 currentCloudSyncProgressPosition = progress; 4956 if (DeviceInfo.EINK_SCREEN) 4957 requestDisableFullRefresh(2); 4958 bookView.draw(true); 4959 } 4960 } 4961 hideCloudSyncProgress()4962 public void hideCloudSyncProgress() { 4963 log.v("hideCloudSyncProgress()"); 4964 if (currentCloudSyncProgressPosition != -1) { 4965 currentCloudSyncProgressPosition = -1; 4966 if (DeviceInfo.EINK_SCREEN) 4967 releaseDisableFullRefresh(2); 4968 bookView.draw(false); 4969 } 4970 } 4971 isCloudSyncProgressActive()4972 private boolean isCloudSyncProgressActive() { 4973 return currentCloudSyncProgressPosition > 0; 4974 } 4975 4976 private class LoadDocumentTask extends Task { 4977 String filename; 4978 String path; 4979 InputStream inputStream; 4980 Runnable doneHandler; 4981 Runnable errorHandler; 4982 String pos; 4983 int profileNumber; 4984 boolean disableInternalStyles; 4985 boolean disableTextAutoformat; 4986 LoadDocumentTask(BookInfo bookInfo, InputStream inputStream, Runnable doneHandler, Runnable errorHandler)4987 LoadDocumentTask(BookInfo bookInfo, InputStream inputStream, Runnable doneHandler, Runnable errorHandler) { 4988 BackgroundThread.ensureGUI(); 4989 mBookInfo = bookInfo; 4990 FileInfo fileInfo = bookInfo.getFileInfo(); 4991 log.v("LoadDocumentTask for " + fileInfo); 4992 if (fileInfo.getTitle() == null && inputStream == null) { 4993 // As a book 'should' have a title, no title means we should 4994 // retrieve the book metadata from the engine to get the 4995 // book language. 4996 // Is it OK to do this here??? Should we use isScanned? 4997 // Should we use another fileInfo flag or a new flag? 4998 mEngine.scanBookProperties(fileInfo); 4999 } 5000 String language = fileInfo.getLanguage(); 5001 log.v("update hyphenation language: " + language + " for " + fileInfo.getTitle()); 5002 this.filename = fileInfo.getPathName(); 5003 this.path = fileInfo.arcname != null ? fileInfo.arcname : fileInfo.pathname; 5004 this.inputStream = inputStream; 5005 this.doneHandler = doneHandler; 5006 this.errorHandler = errorHandler; 5007 //FileInfo fileInfo = new FileInfo(filename); 5008 disableInternalStyles = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_USE_DOCUMENT_STYLES_FLAG); 5009 disableTextAutoformat = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_REFLOW_TXT_FILES_FLAG); 5010 profileNumber = mBookInfo.getFileInfo().getProfileId(); 5011 //Properties oldSettings = new Properties(mSettings); 5012 // TODO: enable storing of profile per book 5013 mActivity.setCurrentProfile(profileNumber); 5014 if (mBookInfo != null && mBookInfo.getLastPosition() != null) 5015 pos = mBookInfo.getLastPosition().getStartPos(); 5016 log.v("LoadDocumentTask : book info " + mBookInfo); 5017 log.v("LoadDocumentTask : last position = " + pos); 5018 if (mBookInfo != null && mBookInfo.getLastPosition() != null) 5019 setTimeElapsed(mBookInfo.getLastPosition().getTimeElapsed()); 5020 //mBitmap = null; 5021 //showProgress(1000, R.string.progress_loading); 5022 //draw(); 5023 BackgroundThread.instance().postGUI(() -> bookView.draw(false)); 5024 //init(); 5025 // close existing document 5026 log.v("LoadDocumentTask : closing current book"); 5027 close(); 5028 final Properties currSettings = new Properties(mSettings); 5029 //setAppSettings(props, oldSettings); 5030 BackgroundThread.instance().postBackground(() -> { 5031 log.v("LoadDocumentTask : switching current profile"); 5032 applySettings(currSettings); //enforce settings reload 5033 log.i("Switching done"); 5034 }); 5035 5036 } 5037 5038 @Override work()5039 public void work() throws IOException { 5040 BackgroundThread.ensureBackground(); 5041 coverPageBytes = null; 5042 log.i("Loading document " + filename); 5043 doc.doCommand(ReaderCommand.DCMD_SET_INTERNAL_STYLES.nativeId, disableInternalStyles ? 0 : 1); 5044 doc.doCommand(ReaderCommand.DCMD_SET_TEXT_FORMAT.nativeId, disableTextAutoformat ? 0 : 1); 5045 doc.doCommand(ReaderCommand.DCMD_SET_REQUESTED_DOM_VERSION.nativeId, mBookInfo.getFileInfo().domVersion); 5046 if (0 == mBookInfo.getFileInfo().domVersion) { 5047 doc.doCommand(ReaderCommand.DCMD_SET_RENDER_BLOCK_RENDERING_FLAGS.nativeId, 0); 5048 } else { 5049 doc.doCommand(ReaderCommand.DCMD_SET_RENDER_BLOCK_RENDERING_FLAGS.nativeId, mBookInfo.getFileInfo().blockRenderingFlags); 5050 } 5051 boolean success; 5052 if (null != inputStream) 5053 success = doc.loadDocumentFromStream(inputStream, filename); 5054 else 5055 success = doc.loadDocument(filename); 5056 if (success) { 5057 log.v("loadDocumentInternal completed successfully"); 5058 5059 doc.requestRender(); 5060 5061 findCoverPage(); 5062 log.v("requesting page image, to render"); 5063 if (internalDX == 0 || internalDY == 0) { 5064 internalDX = surface.getWidth(); 5065 internalDY = surface.getHeight(); 5066 log.d("LoadDocument task: no size defined, resizing using widget size"); 5067 doc.resize(internalDX, internalDY); 5068 } 5069 preparePageImage(0); 5070 log.v("updating loaded book info"); 5071 updateLoadedBookInfo(); 5072 log.i("Document " + filename + " is loaded successfully"); 5073 if (pos != null) { 5074 log.i("Restoring position : " + pos); 5075 restorePositionBackground(pos); 5076 } 5077 CoolReader.dumpHeapAllocation(); 5078 } else { 5079 log.e("Error occurred while trying to load document " + filename); 5080 throw new IOException("Cannot read document"); 5081 } 5082 } 5083 5084 @Override done()5085 public void done() { 5086 BackgroundThread.ensureGUI(); 5087 log.d("LoadDocumentTask, GUI thread is finished successfully"); 5088 if (!Services.isStopped()) { 5089 Services.getHistory().updateBookAccess(mBookInfo, getTimeElapsed()); 5090 final BookInfo finalBookInfo = new BookInfo(mBookInfo); 5091 mActivity.waitForCRDBService(() -> mActivity.getDB().saveBookInfo(finalBookInfo)); 5092 if (coverPageBytes != null && mBookInfo.getFileInfo() != null) { 5093 // TODO: fix it 5094 /* 5095 DocumentFormat format = mBookInfo.getFileInfo().format; 5096 if (null != format) { 5097 if (format.needCoverPageCaching()) { 5098 // if (mActivity.getBrowser() != null) 5099 // mActivity.getBrowser().setCoverpageData(new FileInfo(mBookInfo.getFileInfo()), coverPageBytes); 5100 } 5101 } 5102 */ 5103 if (DeviceInfo.EINK_NOOK) 5104 updateNookTouchCoverpage(mBookInfo.getFileInfo().getPathName(), coverPageBytes); 5105 //mEngine.setProgressDrawable(coverPageDrawable); 5106 } 5107 if (DeviceInfo.EINK_SONY) { 5108 SonyBookSelector selector = new SonyBookSelector(mActivity); 5109 long l = selector.getContentId(path); 5110 if (l != 0) { 5111 selector.setReadingTime(l); 5112 selector.requestBookSelection(l); 5113 } 5114 } 5115 mOpened = true; 5116 5117 highlightBookmarks(); 5118 5119 hideProgress(); 5120 drawPage(); 5121 BackgroundThread.instance().postGUI(() -> { 5122 mActivity.showReader(); 5123 if (null != doneHandler) 5124 doneHandler.run(); 5125 }); 5126 // Save last opened book ONLY if book opened from real file not stream. 5127 if (null == inputStream) 5128 mActivity.setLastBook(filename); 5129 } 5130 } 5131 fail(Exception e)5132 public void fail(Exception e) { 5133 BackgroundThread.ensureGUI(); 5134 close(); 5135 log.v("LoadDocumentTask failed for " + mBookInfo, e); 5136 final FileInfo finalFileInfo = new FileInfo(mBookInfo.getFileInfo()); 5137 mActivity.waitForCRDBService(() -> { 5138 if (!Services.isStopped()) 5139 Services.getHistory().removeBookInfo(mActivity.getDB(), finalFileInfo, true, false); 5140 }); 5141 mBookInfo = null; 5142 log.d("LoadDocumentTask is finished with exception " + e.getMessage()); 5143 mOpened = false; 5144 BackgroundThread.instance().executeBackground(() -> { 5145 doc.createDefaultDocument(mActivity.getString(R.string.error), mActivity.getString(R.string.error_while_opening, filename)); 5146 doc.requestRender(); 5147 preparePageImage(0); 5148 drawPage(); 5149 }); 5150 hideProgress(); 5151 mActivity.showToast("Error while loading document"); 5152 if (errorHandler != null) { 5153 log.e("LoadDocumentTask: Calling error handler"); 5154 errorHandler.run(); 5155 } 5156 } 5157 } 5158 5159 private final static boolean dontStretchWhileDrawing = true; 5160 private final static boolean centerPageInsteadOfResizing = true; 5161 dimRect(Canvas canvas, Rect dst)5162 private void dimRect(Canvas canvas, Rect dst) { 5163 if (DeviceInfo.EINK_SCREEN) 5164 return; // no backlight 5165 int alpha = dimmingAlpha; 5166 if (alpha != 255) { 5167 Paint p = new Paint(); 5168 p.setColor((255 - alpha) << 24); 5169 canvas.drawRect(dst, p); 5170 } 5171 } 5172 drawDimmedBitmap(Canvas canvas, Bitmap bmp, Rect src, Rect dst)5173 private void drawDimmedBitmap(Canvas canvas, Bitmap bmp, Rect src, Rect dst) { 5174 canvas.drawBitmap(bmp, src, dst, null); 5175 dimRect(canvas, dst); 5176 } 5177 drawPageBackground(Canvas canvas, Rect dst, int side)5178 protected void drawPageBackground(Canvas canvas, Rect dst, int side) { 5179 Bitmap bmp = currentBackgroundTextureBitmap; 5180 if (bmp != null) { 5181 int h = bmp.getHeight(); 5182 int w = bmp.getWidth(); 5183 Rect src = new Rect(0, 0, w, h); 5184 if (currentBackgroundTextureTiled) { 5185 // TILED 5186 for (int x = 0; x < dst.width(); x += w) { 5187 int ww = w; 5188 if (x + ww > dst.width()) 5189 ww = dst.width() - x; 5190 for (int y = 0; y < dst.height(); y += h) { 5191 int hh = h; 5192 if (y + hh > dst.height()) 5193 hh = dst.height() - y; 5194 Rect d = new Rect(x, y, x + ww, y + hh); 5195 Rect s = new Rect(0, 0, ww, hh); 5196 drawDimmedBitmap(canvas, bmp, s, d); 5197 } 5198 } 5199 } else { 5200 // STRETCHED 5201 if (side == VIEWER_TOOLBAR_LONG_SIDE) 5202 side = canvas.getWidth() > canvas.getHeight() ? VIEWER_TOOLBAR_TOP : VIEWER_TOOLBAR_LEFT; 5203 else if (side == VIEWER_TOOLBAR_SHORT_SIDE) 5204 side = canvas.getWidth() < canvas.getHeight() ? VIEWER_TOOLBAR_TOP : VIEWER_TOOLBAR_LEFT; 5205 switch (side) { 5206 case VIEWER_TOOLBAR_LEFT: { 5207 int d = dst.width() * dst.height() / h; 5208 if (d > w) 5209 d = w; 5210 src.left = src.right - d; 5211 } 5212 break; 5213 case VIEWER_TOOLBAR_RIGHT: { 5214 int d = dst.width() * dst.height() / h; 5215 if (d > w) 5216 d = w; 5217 src.right = src.left + d; 5218 } 5219 break; 5220 case VIEWER_TOOLBAR_TOP: { 5221 int d = dst.height() * dst.width() / w; 5222 if (d > h) 5223 d = h; 5224 src.top = src.bottom - d; 5225 } 5226 break; 5227 case VIEWER_TOOLBAR_BOTTOM: { 5228 int d = dst.height() * dst.width() / w; 5229 if (d > h) 5230 d = h; 5231 src.bottom = src.top + d; 5232 } 5233 break; 5234 } 5235 drawDimmedBitmap(canvas, bmp, src, dst); 5236 } 5237 } else { 5238 canvas.drawColor(currentBackgroundColor | 0xFF000000); 5239 } 5240 } 5241 5242 protected void drawPageBackground(Canvas canvas) { 5243 Rect dst = new Rect(0, 0, canvas.getWidth(), canvas.getHeight()); 5244 drawPageBackground(canvas, dst, VIEWER_TOOLBAR_NONE); 5245 } 5246 5247 public class ToolbarBackgroundDrawable extends Drawable { 5248 private int location = VIEWER_TOOLBAR_NONE; 5249 private int alpha; 5250 5251 public void setLocation(int location) { 5252 this.location = location; 5253 } 5254 5255 @Override 5256 public void draw(Canvas canvas) { 5257 Rect dst = new Rect(0, 0, canvas.getWidth(), canvas.getHeight()); 5258 try { 5259 drawPageBackground(canvas, dst, location); 5260 } catch (Exception e) { 5261 L.e("Exception in ToolbarBackgroundDrawable.draw", e); 5262 } 5263 } 5264 5265 @Override 5266 public int getOpacity() { 5267 return 255 - alpha; 5268 } 5269 5270 @Override 5271 public void setAlpha(int alpha) { 5272 this.alpha = alpha; 5273 5274 } 5275 5276 @Override 5277 public void setColorFilter(ColorFilter cf) { 5278 // not supported 5279 } 5280 } 5281 5282 public ToolbarBackgroundDrawable createToolbarBackgroundDrawable() { 5283 return new ToolbarBackgroundDrawable(); 5284 } 5285 5286 protected void doDrawProgress(Canvas canvas, int position, String title) { 5287 log.v("doDrawProgress(" + position + ")"); 5288 if (null == title) 5289 return; 5290 int w = canvas.getWidth(); 5291 int h = canvas.getHeight(); 5292 int mins = Math.min(w, h) * 7 / 10; 5293 int ph = mins / 20; 5294 int textColor = mSettings.getColor(PROP_FONT_COLOR, 0x000000); 5295 int fontSize = 15; // 15pt 5296 float factor = mActivity.getDensityFactor(); 5297 Rect rc = new Rect(w / 2 - mins / 2, h / 2 - ph / 2, w / 2 + mins / 2, h / 2 + ph / 2); 5298 Utils.drawFrame(canvas, rc, Utils.createSolidPaint(0xC0000000 | textColor)); 5299 //canvas.drawRect(rc, createSolidPaint(0xFFC0C0A0)); 5300 rc.left += 2; 5301 rc.right -= 2; 5302 rc.top += 2; 5303 rc.bottom -= 2; 5304 int x = rc.left + (rc.right - rc.left) * position / 10000; 5305 Rect rc1 = new Rect(rc); 5306 rc1.right = x; 5307 canvas.drawRect(rc1, Utils.createSolidPaint(0x80000000 | textColor)); 5308 Paint textPaint = Utils.createSolidPaint(0xFF000000 | textColor); 5309 textPaint.setTextAlign(Paint.Align.CENTER); 5310 textPaint.setTextSize(fontSize*factor); 5311 textPaint.setSubpixelText(true); 5312 canvas.drawText(title, (rc.left + rc.right) / 2, rc1.top - fontSize * factor, textPaint); 5313 //canvas.drawText(String.valueOf(position * 100 / 10000) + "%", rc.left + 4, rc1.bottom - 4, textPaint); 5314 // Rect rc2 = new Rect(rc); 5315 // rc.left = x; 5316 // canvas.drawRect(rc2, createSolidPaint(0xFFC0C0A0)); 5317 } 5318 5319 protected void doDrawCloudSyncProgress(Canvas canvas, int position) { 5320 log.v("doDrawCloudSyncProgress(" + position + ")"); 5321 int w = canvas.getWidth(); 5322 int h = canvas.getHeight(); 5323 int ph = Math.min(w, h)/100; 5324 if (ph < 5) 5325 ph = 5; 5326 int textColor = mSettings.getColor(PROP_FONT_COLOR, 0x000000); 5327 int pageHeaderPos = mSettings.getInt(PROP_STATUS_LOCATION, VIEWER_STATUS_PAGE_HEADER); 5328 Rect rc; 5329 if (VIEWER_STATUS_PAGE_FOOTER == pageHeaderPos) 5330 rc = new Rect(0, h - ph, w - 1, h - 2); 5331 else 5332 rc = new Rect(0, 1, w - 1, ph); 5333 int x = rc.left + (rc.right - rc.left) * position / 10000; 5334 Rect rc1 = new Rect(rc); 5335 rc1.right = x; 5336 canvas.drawRect(rc1, Utils.createSolidPaint(0x40000000 | textColor)); 5337 } 5338 5339 private int dimmingAlpha = 255; // no dimming 5340 5341 public void setDimmingAlpha(int alpha) { 5342 if (alpha > 255) 5343 alpha = 255; 5344 if (alpha < 32) 5345 alpha = 32; 5346 if (dimmingAlpha != alpha) { 5347 dimmingAlpha = alpha; 5348 mEngine.execute(new Task() { 5349 @Override 5350 public void work() throws Exception { 5351 bookView.draw(); 5352 } 5353 5354 }); 5355 } 5356 } 5357 5358 private void restorePositionBackground(String pos) { 5359 BackgroundThread.ensureBackground(); 5360 if (pos != null) { 5361 BackgroundThread.ensureBackground(); 5362 doc.goToPosition(pos, false); 5363 preparePageImage(0); 5364 drawPage(); 5365 updateCurrentPositionStatus(); 5366 } 5367 } 5368 5369 private int lastSavePositionTaskId = 0; 5370 5371 private final static int DEF_SAVE_POSITION_INTERVAL = 180000; // 3 minutes 5372 5373 private void scheduleSaveCurrentPositionBookmark(final int delayMillis) { 5374 // GUI thread required 5375 BackgroundThread.instance().executeGUI(() -> { 5376 final int mylastSavePositionTaskId = ++lastSavePositionTaskId; 5377 if (isBookLoaded() && mBookInfo != null) { 5378 final Bookmark bmk = getCurrentPositionBookmark(); 5379 if (bmk == null) 5380 return; 5381 final BookInfo bookInfo = mBookInfo; 5382 if (delayMillis <= 1) { 5383 if (bookInfo != null && mActivity.getDB() != null) { 5384 log.v("saving last position immediately"); 5385 savePositionBookmark(bmk); 5386 Services.getHistory().updateBookAccess(bookInfo, getTimeElapsed()); 5387 } 5388 } else { 5389 BackgroundThread.instance().postGUI(() -> { 5390 if (mylastSavePositionTaskId == lastSavePositionTaskId) { 5391 if (bookInfo != null) { 5392 log.v("saving last position"); 5393 if (!Services.isStopped()) { 5394 // this delayed task can be completed after calling CoolReader.onDestroy(), 5395 // which in turn calls Services.stopServices(). 5396 savePositionBookmark(bmk); 5397 Services.getHistory().updateBookAccess(bookInfo, getTimeElapsed()); 5398 } 5399 } 5400 } 5401 }, delayMillis); 5402 } 5403 } 5404 }); 5405 5406 // if (DeviceInfo.EINK_SONY && isBookLoaded()) { 5407 // getCurrentPositionProperties(new PositionPropertiesCallback() { 5408 // @Override 5409 // public void onPositionProperties(PositionProperties props, 5410 // String positionText) { 5411 // // update position for Sony T2 5412 // if (props != null && mBookInfo != null) { 5413 // String fname = mBookInfo.getFileInfo().getBasePath(); 5414 // if (fname != null && fname.length() > 0) 5415 // setBookPositionForExternalShell(fname, props.pageNumber, props.pageCount); 5416 // } 5417 // } 5418 // }); 5419 // } 5420 } 5421 5422 // Sony T2 update position method - by Jotas 5423 public void setBookPositionForExternalShell(String filename, long current_page, long total_pages) { 5424 if (DeviceInfo.EINK_SONY) { 5425 log.d("Trying to update last book and position in Sony T2 shell: file=" + filename + " currentPage=" + current_page + " totalPages=" + total_pages); 5426 File f = new File(filename); 5427 if (f.exists()) { 5428 String file_path = f.getAbsolutePath(); 5429 try { 5430 file_path = f.getCanonicalPath(); 5431 } catch (Exception e) { 5432 Log.d("cr3Sony", "setBookPosition getting filename/path", e); 5433 } 5434 5435 try { 5436 Uri uri = Uri.parse("content://com.sony.drbd.ebook.internal.provider/continuerea ding"); 5437 ContentValues contentvalues = new ContentValues(); 5438 contentvalues.put("file_path", file_path); 5439 contentvalues.put("current_page", current_page); 5440 contentvalues.put("total_pages", total_pages); 5441 if (mActivity.getContentResolver().insert(uri, contentvalues) != null) 5442 Log.d("cr3Sony", "setBookPosition: filename = " + filename + "start=" + current_page + "end=" + total_pages); 5443 else 5444 Log.d("crsony", "setBookPosition : error inserting in database!"); 5445 5446 } catch (Exception e) { 5447 Log.d("cr3Sony", "setBookPositon parse/values!", e); 5448 } 5449 } 5450 } 5451 } 5452 5453 5454 public interface PositionPropertiesCallback { 5455 void onPositionProperties(PositionProperties props, String positionText); 5456 } 5457 5458 public void getCurrentPositionProperties(final PositionPropertiesCallback callback) { 5459 BackgroundThread.instance().postBackground(() -> { 5460 final Bookmark bmk = (doc != null) ? doc.getCurrentPageBookmarkNoRender() : null; 5461 final PositionProperties props = (bmk != null) ? doc.getPositionProps(bmk.getStartPos(), true) : null; 5462 BackgroundThread.instance().postBackground(() -> { 5463 String posText = null; 5464 if (props != null) { 5465 int percent = (int) (10000 * (long) props.y / props.fullHeight); 5466 String percentText = "" + (percent / 100) + "." + (percent % 10) + "%"; 5467 posText = "" + props.pageNumber + " / " + props.pageCount + " (" + percentText + ")"; 5468 } 5469 callback.onPositionProperties(props, posText); 5470 }); 5471 }); 5472 } 5473 5474 5475 public Bookmark getCurrentPositionBookmark() { 5476 if (!mOpened) 5477 return null; 5478 Bookmark bmk = doc.getCurrentPageBookmarkNoRender(); 5479 if (bmk != null) { 5480 bmk.setTimeStamp(System.currentTimeMillis()); 5481 bmk.setType(Bookmark.TYPE_LAST_POSITION); 5482 if (mBookInfo != null) 5483 mBookInfo.setLastPosition(bmk); 5484 } 5485 return bmk; 5486 } 5487 5488 Bookmark lastSavedBookmark = null; 5489 5490 public void savePositionBookmark(Bookmark bmk) { 5491 if (bmk != null && mBookInfo != null && isBookLoaded()) { 5492 //setBookPosition(); 5493 if (lastSavedBookmark == null || !lastSavedBookmark.getStartPos().equals(bmk.getStartPos())) { 5494 if (!Services.isStopped()) { 5495 Services.getHistory().updateRecentDir(); 5496 mActivity.getDB().saveBookInfo(mBookInfo); 5497 mActivity.getDB().flush(); 5498 lastSavedBookmark = bmk; 5499 } 5500 } 5501 } 5502 } 5503 5504 public Bookmark saveCurrentPositionBookmarkSync(final boolean saveToDB) { 5505 ++lastSavePositionTaskId; 5506 Bookmark bmk = BackgroundThread.instance().callBackground(new Callable<Bookmark>() { 5507 @Override 5508 public Bookmark call() throws Exception { 5509 if (!mOpened) 5510 return null; 5511 return doc.getCurrentPageBookmark(); 5512 } 5513 }); 5514 if (bmk != null) { 5515 //setBookPosition(); 5516 bmk.setTimeStamp(System.currentTimeMillis()); 5517 bmk.setType(Bookmark.TYPE_LAST_POSITION); 5518 if (mBookInfo != null) 5519 mBookInfo.setLastPosition(bmk); 5520 if (saveToDB) { 5521 Services.getHistory().updateRecentDir(); 5522 mActivity.getDB().saveBookInfo(mBookInfo); 5523 mActivity.getDB().flush(); 5524 } 5525 } 5526 return bmk; 5527 } 5528 5529 public void save() { 5530 BackgroundThread.ensureGUI(); 5531 if (isBookLoaded() && mBookInfo != null) { 5532 if (!Services.isStopped()) { 5533 log.v("saving last immediately"); 5534 log.d("bookmark count 1 = " + mBookInfo.getBookmarkCount()); 5535 Services.getHistory().updateBookAccess(mBookInfo, getTimeElapsed()); 5536 log.d("bookmark count 2 = " + mBookInfo.getBookmarkCount()); 5537 mActivity.getDB().saveBookInfo(mBookInfo); 5538 log.d("bookmark count 3 = " + mBookInfo.getBookmarkCount()); 5539 mActivity.getDB().flush(); 5540 } 5541 } 5542 //scheduleSaveCurrentPositionBookmark(0); 5543 //post( new SavePositionTask() ); 5544 } 5545 5546 public void close() { 5547 BackgroundThread.ensureGUI(); 5548 log.i("ReaderView.close() is called"); 5549 if (!mOpened) 5550 return; 5551 cancelSwapTask(); 5552 stopImageViewer(); 5553 save(); 5554 //scheduleSaveCurrentPositionBookmark(0); 5555 //save(); 5556 post(new Task() { 5557 public void work() { 5558 BackgroundThread.ensureBackground(); 5559 if (mOpened) { 5560 mOpened = false; 5561 log.i("ReaderView().close() : closing current document"); 5562 doc.doCommand(ReaderCommand.DCMD_CLOSE_BOOK.nativeId, 0); 5563 } 5564 } 5565 5566 public void done() { 5567 BackgroundThread.ensureGUI(); 5568 if (currentAnimation == null) { 5569 if (mCurrentPageInfo != null) { 5570 mCurrentPageInfo.recycle(); 5571 mCurrentPageInfo = null; 5572 } 5573 if (mNextPageInfo != null) { 5574 mNextPageInfo.recycle(); 5575 mNextPageInfo = null; 5576 } 5577 } else 5578 invalidImages = true; 5579 factory.compact(); 5580 mCurrentPageInfo = null; 5581 } 5582 }); 5583 } 5584 5585 public void destroy() { 5586 log.i("ReaderView.destroy() is called"); 5587 if (mInitialized) { 5588 //close(); 5589 BackgroundThread.instance().postBackground(() -> { 5590 BackgroundThread.ensureBackground(); 5591 if (mInitialized) { 5592 log.i("ReaderView.destroyInternal() calling"); 5593 doc.destroy(); 5594 mInitialized = false; 5595 currentBackgroundTexture = Engine.NO_TEXTURE; 5596 } 5597 }); 5598 //engine.waitTasksCompletion(); 5599 if (null != ttsToolbar) 5600 ttsToolbar.stopAndClose(); 5601 } 5602 } 5603 5604 private String getCSSForFormat(DocumentFormat fileFormat) { 5605 if (fileFormat == null) 5606 fileFormat = DocumentFormat.FB2; 5607 File[] dataDirs = Engine.getDataDirectories(null, false, false); 5608 String defaultCss = mEngine.loadResourceUtf8(fileFormat.getCSSResourceId()); 5609 for (File dir : dataDirs) { 5610 File file = new File(dir, fileFormat.getCssName()); 5611 if (file.exists()) { 5612 String css = Engine.loadFileUtf8(file); 5613 if (css != null) { 5614 int p1 = css.indexOf("@import"); 5615 if (p1 < 0) 5616 p1 = css.indexOf("@include"); 5617 int p2 = css.indexOf("\";"); 5618 if (p1 >= 0 && p2 >= 0 && p1 < p2) { 5619 css = css.substring(0, p1) + "\n" + defaultCss + "\n" + css.substring(p2 + 2); 5620 } 5621 return css; 5622 } 5623 } 5624 } 5625 return defaultCss; 5626 } 5627 5628 boolean enable_progress_callback = true; 5629 ReaderCallback readerCallback = new ReaderCallback() { 5630 5631 public boolean OnExportProgress(int percent) { 5632 log.d("readerCallback.OnExportProgress " + percent); 5633 return true; 5634 } 5635 5636 public void OnExternalLink(String url, String nodeXPath) { 5637 } 5638 5639 public void OnFormatEnd() { 5640 log.d("readerCallback.OnFormatEnd"); 5641 //mEngine.hideProgress(); 5642 hideProgress(); 5643 drawPage(); 5644 scheduleSwapTask(); 5645 } 5646 5647 public boolean OnFormatProgress(final int percent) { 5648 if (enable_progress_callback) { 5649 log.d("readerCallback.OnFormatProgress " + percent); 5650 showProgress(percent * 4 / 10 + 5000, R.string.progress_formatting); 5651 } 5652 // executeSync( new Callable<Object>() { 5653 // public Object call() { 5654 // BackgroundThread.ensureGUI(); 5655 // log.d("readerCallback.OnFormatProgress " + percent); 5656 // showProgress( percent*4/10 + 5000, R.string.progress_formatting); 5657 // return null; 5658 // } 5659 // }); 5660 return true; 5661 } 5662 5663 public void OnFormatStart() { 5664 log.d("readerCallback.OnFormatStart"); 5665 } 5666 5667 public void OnLoadFileEnd() { 5668 log.d("readerCallback.OnLoadFileEnd"); 5669 if (internalDX == 0 && internalDY == 0) { 5670 internalDX = requestedWidth; 5671 internalDY = requestedHeight; 5672 log.d("OnLoadFileEnd: resizeInternal(" + internalDX + "," + internalDY + ")"); 5673 doc.resize(internalDX, internalDY); 5674 } 5675 } 5676 5677 public void OnLoadFileError(String message) { 5678 log.d("readerCallback.OnLoadFileError(" + message + ")"); 5679 } 5680 5681 public void OnLoadFileFirstPagesReady() { 5682 log.d("readerCallback.OnLoadFileFirstPagesReady"); 5683 } 5684 5685 public String OnLoadFileFormatDetected(final DocumentFormat fileFormat) { 5686 log.i("readerCallback.OnLoadFileFormatDetected " + fileFormat); 5687 if (fileFormat != null) { 5688 return getCSSForFormat(fileFormat); 5689 } 5690 return null; 5691 // 5692 // String res = executeSync( new Callable<String>() { 5693 // public String call() { 5694 // BackgroundThread.ensureGUI(); 5695 // log.i("readerCallback.OnLoadFileFormatDetected " + fileFormat); 5696 // if (fileFormat != null) { 5697 // String s = getCSSForFormat(fileFormat); 5698 // log.i("setting .css for file format " + fileFormat + " from resource " + fileFormat.getCssName()); 5699 // return s; 5700 // } 5701 // return null; 5702 // } 5703 // }); 5704 //// int internalStyles = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_USE_DOCUMENT_STYLES_FLAG) ? 0 : 1; 5705 //// int txtReflow = mBookInfo.getFileInfo().getFlag(FileInfo.DONT_REFLOW_TXT_FILES_FLAG) ? 0 : 2; 5706 //// log.d("internalStyles: " + internalStyles); 5707 //// doc.doCommand(ReaderCommand.DCMD_SET_INTERNAL_STYLES.nativeId, internalStyles | txtReflow); 5708 // return res; 5709 } 5710 5711 public boolean OnLoadFileProgress(final int percent) { 5712 BackgroundThread.ensureBackground(); 5713 if (enable_progress_callback) { 5714 log.d("readerCallback.OnLoadFileProgress " + percent); 5715 showProgress(percent * 4 / 10 + 1000, R.string.progress_loading); 5716 } 5717 // executeSync( new Callable<Object>() { 5718 // public Object call() { 5719 // BackgroundThread.ensureGUI(); 5720 // log.d("readerCallback.OnLoadFileProgress " + percent); 5721 // showProgress( percent*4/10 + 1000, R.string.progress_loading); 5722 // return null; 5723 // } 5724 // }); 5725 return true; 5726 } 5727 5728 public void OnLoadFileStart(String filename) { 5729 cancelSwapTask(); 5730 BackgroundThread.ensureBackground(); 5731 log.d("readerCallback.OnLoadFileStart " + filename); 5732 if (enable_progress_callback) { 5733 showProgress(1000, R.string.progress_loading); 5734 } 5735 } 5736 5737 /// Override to handle external links 5738 public void OnImageCacheClear() { 5739 //log.d("readerCallback.OnImageCacheClear"); 5740 clearImageCache(); 5741 } 5742 5743 public boolean OnRequestReload() { 5744 //reloadDocument(); 5745 return true; 5746 } 5747 5748 }; 5749 5750 private volatile SwapToCacheTask currentSwapTask; 5751 5752 private void scheduleSwapTask() { 5753 currentSwapTask = new SwapToCacheTask(); 5754 currentSwapTask.reschedule(); 5755 } 5756 5757 private void cancelSwapTask() { 5758 currentSwapTask = null; 5759 } 5760 5761 private class SwapToCacheTask extends Task { 5762 boolean isTimeout; 5763 long startTime; 5764 5765 public SwapToCacheTask() { 5766 startTime = System.currentTimeMillis(); 5767 } 5768 5769 public void reschedule() { 5770 if (this != currentSwapTask) 5771 return; 5772 BackgroundThread.instance().postGUI(() -> post(SwapToCacheTask.this), 2000); 5773 } 5774 5775 @Override 5776 public void work() throws Exception { 5777 if (this != currentSwapTask) 5778 return; 5779 int res = doc.swapToCache(); 5780 isTimeout = res == DocView.SWAP_TIMEOUT; 5781 long duration = System.currentTimeMillis() - startTime; 5782 if (!isTimeout) { 5783 log.i("swapToCacheInternal is finished with result " + res + " in " + duration + " ms"); 5784 } else { 5785 log.d("swapToCacheInternal exited by TIMEOUT in " + duration + " ms: rescheduling"); 5786 } 5787 } 5788 5789 @Override 5790 public void done() { 5791 if (isTimeout) 5792 reschedule(); 5793 } 5794 5795 } 5796 5797 private boolean invalidImages = true; 5798 5799 public void clearImageCache() { 5800 BackgroundThread.instance().postBackground(() -> invalidImages = true); 5801 } 5802 5803 public void setStyleSheet(final String css) { 5804 BackgroundThread.ensureGUI(); 5805 if (css != null && css.length() > 0) { 5806 post(new Task() { 5807 public void work() { 5808 doc.setStylesheet(css); 5809 } 5810 }); 5811 } 5812 } 5813 5814 public void goToPosition(int position) { 5815 BackgroundThread.ensureGUI(); 5816 doEngineCommand(ReaderCommand.DCMD_GO_POS, position); 5817 } 5818 5819 public void moveBy(final int delta) { 5820 BackgroundThread.ensureGUI(); 5821 log.d("moveBy(" + delta + ")"); 5822 post(new Task() { 5823 public void work() { 5824 BackgroundThread.ensureBackground(); 5825 doc.doCommand(ReaderCommand.DCMD_SCROLL_BY.nativeId, delta); 5826 scheduleSaveCurrentPositionBookmark(DEF_SAVE_POSITION_INTERVAL); 5827 } 5828 5829 public void done() { 5830 drawPage(); 5831 } 5832 }); 5833 } 5834 5835 public void goToPage(int pageNumber) { 5836 BackgroundThread.ensureGUI(); 5837 doEngineCommand(ReaderCommand.DCMD_GO_PAGE, pageNumber - 1); 5838 } 5839 5840 public void goToPercent(final int percent) { 5841 BackgroundThread.ensureGUI(); 5842 if (percent >= 0 && percent <= 100) 5843 post(new Task() { 5844 public void work() { 5845 PositionProperties pos = doc.getPositionProps(null, true); 5846 if (pos != null && pos.pageCount > 0) { 5847 int pageNumber = pos.pageCount * percent / 100; 5848 doCommandFromBackgroundThread(ReaderCommand.DCMD_GO_PAGE, pageNumber); 5849 } 5850 } 5851 }); 5852 } 5853 5854 public interface MoveSelectionCallback { 5855 // selection is changed 5856 public void onNewSelection(Selection selection); 5857 5858 // cannot move selection 5859 public void onFail(); 5860 } 5861 5862 public void moveSelection(final ReaderCommand command, final int param, final MoveSelectionCallback callback) { 5863 post(new Task() { 5864 private boolean res; 5865 private Selection selection = new Selection(); 5866 5867 @Override 5868 public void work() throws Exception { 5869 res = doc.moveSelection(selection, command.nativeId, param); 5870 } 5871 5872 @Override 5873 public void done() { 5874 if (callback != null) { 5875 clearImageCache(); 5876 surface.invalidate(); 5877 drawPage(); 5878 if (res) 5879 callback.onNewSelection(selection); 5880 else 5881 callback.onFail(); 5882 } 5883 } 5884 5885 @Override 5886 public void fail(Exception e) { 5887 if (callback != null) 5888 callback.onFail(); 5889 } 5890 5891 5892 }); 5893 } 5894 5895 private void showSwitchProfileDialog() { 5896 SwitchProfileDialog dlg = new SwitchProfileDialog(mActivity, this); 5897 dlg.show(); 5898 } 5899 5900 // private int currentProfile = 0; 5901 // public int getCurrentProfile() { 5902 // if (currentProfile == 0) { 5903 // currentProfile = mSettings.getInt(PROP_PROFILE_NUMBER, 1); 5904 // if (currentProfile < 1 || currentProfile > MAX_PROFILES) 5905 // currentProfile = 1; 5906 // } 5907 // return currentProfile; 5908 // } 5909 5910 public void setCurrentProfile(int profile) { 5911 if (mActivity.getCurrentProfile() == profile) 5912 return; 5913 if (mBookInfo != null && mBookInfo.getFileInfo() != null) { 5914 mBookInfo.getFileInfo().setProfileId(profile); 5915 mActivity.getDB().saveBookInfo(mBookInfo); 5916 } 5917 log.i("Apply new profile settings"); 5918 mActivity.setCurrentProfile(profile); 5919 } 5920 5921 private final static String NOOK_TOUCH_COVERPAGE_DIR = "/media/screensavers/currentbook"; 5922 5923 private void updateNookTouchCoverpage(String bookFileName, 5924 byte[] coverpageBytes) { 5925 try { 5926 String imageFileName; 5927 int lastSlash = bookFileName.lastIndexOf("/"); 5928 // exclude path and extension 5929 if (lastSlash >= 0 && lastSlash < bookFileName.length()) { 5930 imageFileName = bookFileName.substring(lastSlash); 5931 } else { 5932 imageFileName = bookFileName; 5933 } 5934 int lastDot = imageFileName.lastIndexOf("."); 5935 if (lastDot > 0) { 5936 imageFileName = imageFileName.substring(0, lastDot); 5937 } 5938 // guess image type 5939 if (coverpageBytes.length > 8 // PNG signature length 5940 && coverpageBytes[0] == (byte) 0x89 // PNG signature start 4 bytes 5941 && coverpageBytes[1] == 0x50 5942 && coverpageBytes[2] == 0x4E 5943 && coverpageBytes[3] == 0x47) { 5944 imageFileName += ".png"; 5945 } else if (coverpageBytes.length > 3 // Checking only the first 3 5946 // bytes of JPEG header 5947 && coverpageBytes[0] == (byte) 0xFF 5948 && coverpageBytes[1] == (byte) 0xD8 5949 && coverpageBytes[2] == (byte) 0xFF) { 5950 imageFileName += ".jpg"; 5951 } else if (coverpageBytes.length > 3 // Checking only the first 3 5952 // bytes of GIF header 5953 && coverpageBytes[0] == 0x47 5954 && coverpageBytes[1] == 0x49 5955 && coverpageBytes[2] == 0x46) { 5956 imageFileName += ".gif"; 5957 } else if (coverpageBytes.length > 2 // Checking only the first 2 5958 // bytes of BMP signature 5959 && coverpageBytes[0] == 0x42 && coverpageBytes[1] == 0x4D) { 5960 imageFileName += ".bmp"; 5961 } else { 5962 imageFileName += ".jpg"; // default image type 5963 } 5964 // create directory if it does not exist 5965 File d = new File(NOOK_TOUCH_COVERPAGE_DIR); 5966 if (!d.exists()) { 5967 d.mkdir(); 5968 } 5969 // create file only if file with same name does not exist 5970 File f = new File(d, imageFileName); 5971 if (!f.exists()) { 5972 // delete other files in directory so that only current cover is 5973 // shown all the time 5974 File[] files = d.listFiles(); 5975 for (File oldFile : files) { 5976 oldFile.delete(); 5977 } 5978 // write the image file 5979 FileOutputStream fos = new FileOutputStream(f); 5980 fos.write(coverpageBytes); 5981 fos.close(); 5982 } 5983 } catch (Exception ex) { 5984 log.e("Error writing cover page: ", ex); 5985 } 5986 } 5987 5988 private static final int GC_INTERVAL = 15000; // 15 seconds 5989 DelayedExecutor gcTask = DelayedExecutor.createGUI("gc"); 5990 5991 public void scheduleGc() { 5992 try { 5993 gcTask.postDelayed(() -> { 5994 log.v("Initiating garbage collection"); 5995 System.gc(); 5996 }, GC_INTERVAL); 5997 } catch (Exception e) { 5998 // ignore 5999 } 6000 } 6001 6002 public void cancelGc() { 6003 try { 6004 gcTask.cancel(); 6005 } catch (Exception e) { 6006 // ignore 6007 } 6008 } 6009 6010 private void switchFontFace(int direction) { 6011 String currentFontFace = mSettings.getProperty(PROP_FONT_FACE, ""); 6012 String[] mFontFaces = Engine.getFontFaceList(); 6013 int index = 0; 6014 int countFaces = mFontFaces.length; 6015 for (int i = 0; i < countFaces; i++) { 6016 if (mFontFaces[i].equals(currentFontFace)) { 6017 index = i; 6018 break; 6019 } 6020 } 6021 index += direction; 6022 if (index < 0) 6023 index = countFaces - 1; 6024 else if (index >= countFaces) 6025 index = 0; 6026 saveSetting(PROP_FONT_FACE, mFontFaces[index]); 6027 syncViewSettings(getSettings(), true, true); 6028 } 6029 6030 public void showInputDialog(final String title, final String prompt, final boolean isNumberEdit, final int minValue, final int maxValue, final int lastValue, final InputHandler handler) { 6031 BackgroundThread.instance().executeGUI(() -> { 6032 final InputDialog dlg = new InputDialog(mActivity, title, prompt, isNumberEdit, minValue, maxValue, lastValue, handler); 6033 dlg.show(); 6034 }); 6035 } 6036 6037 public void showGoToPageDialog() { 6038 getCurrentPositionProperties((props, positionText) -> { 6039 if (props == null) 6040 return; 6041 String pos = mActivity.getString(R.string.dlg_goto_current_position) + " " + positionText; 6042 String prompt = mActivity.getString(R.string.dlg_goto_input_page_number); 6043 showInputDialog(mActivity.getString(R.string.mi_goto_page), pos + "\n" + prompt, true, 6044 1, props.pageCount, props.pageNumber, 6045 new InputHandler() { 6046 int pageNumber = 0; 6047 6048 @Override 6049 public boolean validate(String s) { 6050 pageNumber = Integer.parseInt(s); 6051 return pageNumber > 0 && pageNumber <= props.pageCount; 6052 } 6053 6054 @Override 6055 public void onOk(String s) { 6056 goToPage(pageNumber); 6057 } 6058 6059 @Override 6060 public void onCancel() { 6061 } 6062 }); 6063 }); 6064 } 6065 6066 public void showGoToPercentDialog() { 6067 getCurrentPositionProperties((props, positionText) -> { 6068 if (props == null) 6069 return; 6070 String pos = mActivity.getString(R.string.dlg_goto_current_position) + " " + positionText; 6071 String prompt = mActivity.getString(R.string.dlg_goto_input_percent); 6072 showInputDialog(mActivity.getString(R.string.mi_goto_percent), pos + "\n" + prompt, true, 6073 0, 100, props.y * 100 / props.fullHeight, 6074 new InputHandler() { 6075 int percent = 0; 6076 6077 @Override 6078 public boolean validate(String s) { 6079 percent = Integer.valueOf(s); 6080 return percent >= 0 && percent <= 100; 6081 } 6082 6083 @Override 6084 public void onOk(String s) { 6085 goToPercent(percent); 6086 } 6087 6088 @Override 6089 public void onCancel() { 6090 } 6091 }); 6092 }); 6093 } 6094 6095 @Override 6096 public boolean onKey(View v, int keyCode, KeyEvent event) { 6097 // TODO Auto-generated method stub 6098 if (event.getAction() == KeyEvent.ACTION_DOWN) 6099 return onKeyDown(keyCode, event); 6100 else if (event.getAction() == KeyEvent.ACTION_UP) 6101 return onKeyUp(keyCode, event); 6102 return false; 6103 } 6104 6105 @Override 6106 public boolean onTouch(View v, MotionEvent event) { 6107 return onTouchEvent(event); 6108 } 6109 6110 public boolean onKeyDown(int keyCode, final KeyEvent event) { 6111 6112 if (keyCode == 0) 6113 keyCode = event.getScanCode(); 6114 keyCode = translateKeyCode(keyCode); 6115 6116 mActivity.onUserActivity(); 6117 6118 if (currentImageViewer != null) 6119 return currentImageViewer.onKeyDown(keyCode, event); 6120 6121 // backKeyDownHere = false; 6122 if (event.getRepeatCount() == 0) { 6123 log.v("onKeyDown(" + keyCode + ", " + event + ")"); 6124 keyDownTimestampMap.put(keyCode, System.currentTimeMillis()); 6125 6126 if (keyCode == KeyEvent.KEYCODE_BACK) { 6127 // force saving position on BACK key press 6128 scheduleSaveCurrentPositionBookmark(1); 6129 } 6130 } 6131 if (keyCode == KeyEvent.KEYCODE_POWER || keyCode == KeyEvent.KEYCODE_ENDCALL) { 6132 mActivity.releaseBacklightControl(); 6133 return false; 6134 } 6135 6136 if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { 6137 if (isAutoScrollActive()) { 6138 if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) 6139 changeAutoScrollSpeed(1); 6140 else 6141 changeAutoScrollSpeed(-1); 6142 return true; 6143 } 6144 if (!enableVolumeKeys) { 6145 return false; 6146 } 6147 } 6148 6149 if (isAutoScrollActive()) 6150 return true; // autoscroll will be stopped in onKeyUp 6151 6152 keyCode = overrideKey(keyCode); 6153 ReaderAction action = ReaderAction.findForKey(keyCode, mSettings); 6154 ReaderAction longAction = ReaderAction.findForLongKey(keyCode, mSettings); 6155 //ReaderAction dblAction = ReaderAction.findForDoubleKey( keyCode, mSettings ); 6156 6157 if (event.getRepeatCount() == 0) { 6158 if (keyCode == currentDoubleClickActionKeyCode && currentDoubleClickActionStart + DOUBLE_CLICK_INTERVAL > android.os.SystemClock.uptimeMillis()) { 6159 if (currentDoubleClickAction != null) { 6160 log.d("executing doubleclick action " + currentDoubleClickAction); 6161 onAction(currentDoubleClickAction); 6162 } 6163 currentDoubleClickActionStart = 0; 6164 currentDoubleClickActionKeyCode = 0; 6165 currentDoubleClickAction = null; 6166 currentSingleClickAction = null; 6167 return true; 6168 } else { 6169 if (currentSingleClickAction != null) { 6170 onAction(currentSingleClickAction); 6171 } 6172 currentDoubleClickActionStart = 0; 6173 currentDoubleClickActionKeyCode = 0; 6174 currentDoubleClickAction = null; 6175 currentSingleClickAction = null; 6176 } 6177 } 6178 6179 if (event.getRepeatCount() > 0) { 6180 if (!isTracked(event)) 6181 return true; // ignore 6182 // repeating key down 6183 boolean isLongPress = (event.getEventTime() - event.getDownTime()) >= AUTOREPEAT_KEYPRESS_TIME; 6184 if (isLongPress) { 6185 if (actionToRepeat != null) { 6186 if (!repeatActionActive) { 6187 log.v("autorepeating action : " + actionToRepeat); 6188 repeatActionActive = true; 6189 onAction(actionToRepeat, () -> { 6190 if (trackedKeyEvent != null && trackedKeyEvent.getDownTime() == event.getDownTime()) { 6191 log.v("action is completed : " + actionToRepeat); 6192 repeatActionActive = false; 6193 } 6194 }); 6195 } 6196 } else { 6197 stopTracking(); 6198 log.v("executing action on long press : " + longAction); 6199 onAction(longAction); 6200 } 6201 } 6202 return true; 6203 } 6204 6205 if (!action.isNone() && action.canRepeat() && longAction.isRepeat()) { 6206 // start tracking repeat 6207 startTrackingKey(event); 6208 actionToRepeat = action; 6209 log.v("running action with scheduled autorepeat : " + actionToRepeat); 6210 repeatActionActive = true; 6211 onAction(actionToRepeat, () -> { 6212 if (trackedKeyEvent == event) { 6213 log.v("action is completed : " + actionToRepeat); 6214 repeatActionActive = false; 6215 } 6216 }); 6217 return true; 6218 } else { 6219 actionToRepeat = null; 6220 } 6221 6222 /* if ( keyCode>=KeyEvent.KEYCODE_0 && keyCode<=KeyEvent.KEYCODE_9 ) { 6223 // will process in keyup handler 6224 startTrackingKey(event); 6225 return true; 6226 }*/ 6227 if (action.isNone() && longAction.isNone()) 6228 return false; 6229 startTrackingKey(event); 6230 return true; 6231 } 6232 6233 public boolean onKeyUp(int keyCode, final KeyEvent event) { 6234 if (keyCode == 0) 6235 keyCode = event.getScanCode(); 6236 mActivity.onUserActivity(); 6237 keyCode = translateKeyCode(keyCode); 6238 if (currentImageViewer != null) 6239 return currentImageViewer.onKeyUp(keyCode, event); 6240 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 6241 if (isAutoScrollActive()) 6242 return true; 6243 if (!enableVolumeKeys) 6244 return false; 6245 } 6246 if (isAutoScrollActive()) { 6247 stopAutoScroll(); 6248 return true; 6249 } 6250 if (keyCode == KeyEvent.KEYCODE_POWER || keyCode == KeyEvent.KEYCODE_ENDCALL) { 6251 mActivity.releaseBacklightControl(); 6252 return false; 6253 } 6254 boolean tracked = isTracked(event); 6255 // if ( keyCode!=KeyEvent.KEYCODE_BACK ) 6256 // backKeyDownHere = false; 6257 6258 if (keyCode == KeyEvent.KEYCODE_BACK && !tracked) 6259 return true; 6260 //backKeyDownHere = false; 6261 6262 // apply orientation 6263 keyCode = overrideKey(keyCode); 6264 boolean isLongPress = false; 6265 Long keyDownTs = keyDownTimestampMap.get(keyCode); 6266 if (keyDownTs != null && System.currentTimeMillis() - keyDownTs >= LONG_KEYPRESS_TIME) 6267 isLongPress = true; 6268 ReaderAction action = ReaderAction.findForKey(keyCode, mSettings); 6269 ReaderAction longAction = ReaderAction.findForLongKey(keyCode, mSettings); 6270 ReaderAction dblAction = ReaderAction.findForDoubleKey(keyCode, mSettings); 6271 stopTracking(); 6272 6273 /* if ( keyCode>=KeyEvent.KEYCODE_0 && keyCode<=KeyEvent.KEYCODE_9 && tracked ) { 6274 // goto/set shortcut bookmark 6275 int shortcut = keyCode - KeyEvent.KEYCODE_0; 6276 if ( shortcut==0 ) 6277 shortcut = 10; 6278 if ( isLongPress ) 6279 addBookmark(shortcut); 6280 else 6281 goToBookmark(shortcut); 6282 return true; 6283 }*/ 6284 if (action.isNone() || !tracked) { 6285 return false; 6286 } 6287 if (!action.isNone() && action.canRepeat() && longAction.isRepeat()) { 6288 // already processed by onKeyDown() 6289 return true; 6290 } 6291 6292 if (isLongPress) { 6293 action = longAction; 6294 } else { 6295 if (!dblAction.isNone()) { 6296 // wait for possible double click 6297 currentDoubleClickActionStart = android.os.SystemClock.uptimeMillis(); 6298 currentDoubleClickAction = dblAction; 6299 currentSingleClickAction = action; 6300 currentDoubleClickActionKeyCode = keyCode; 6301 final int myKeyCode = keyCode; 6302 BackgroundThread.instance().postGUI(() -> { 6303 if (currentSingleClickAction != null && currentDoubleClickActionKeyCode == myKeyCode) { 6304 log.d("onKeyUp: single click action " + currentSingleClickAction.id + " found for key " + myKeyCode + " single click"); 6305 onAction(currentSingleClickAction); 6306 } 6307 currentDoubleClickActionStart = 0; 6308 currentDoubleClickActionKeyCode = 0; 6309 currentDoubleClickAction = null; 6310 currentSingleClickAction = null; 6311 }, DOUBLE_CLICK_INTERVAL); 6312 // posted 6313 return true; 6314 } 6315 } 6316 if (!action.isNone()) { 6317 log.d("onKeyUp: action " + action.id + " found for key " + keyCode + (isLongPress ? " (long)" : "")); 6318 onAction(action); 6319 return true; 6320 } 6321 6322 // not processed 6323 return false; 6324 } 6325 6326 public boolean onTouchEvent(MotionEvent event) { 6327 6328 if (!isTouchScreenEnabled) { 6329 return true; 6330 } 6331 if (event.getX() == 0 && event.getY() == 0) 6332 return true; 6333 mActivity.onUserActivity(); 6334 6335 if (currentImageViewer != null) 6336 return currentImageViewer.onTouchEvent(event); 6337 6338 if (isAutoScrollActive()) { 6339 //if (currentTapHandler != null && currentTapHandler.isInitialState()) { 6340 if (event.getAction() == MotionEvent.ACTION_DOWN) { 6341 int x = (int) event.getX(); 6342 int y = (int) event.getY(); 6343 int z = getTapZone(x, y, surface.getWidth(), surface.getHeight()); 6344 if (z == 7) 6345 changeAutoScrollSpeed(-1); 6346 else if (z == 9) 6347 changeAutoScrollSpeed(1); 6348 else 6349 stopAutoScroll(); 6350 } 6351 return true; 6352 } 6353 6354 if (currentTapHandler == null) 6355 currentTapHandler = new TapHandler(); 6356 currentTapHandler.checkExpiration(); 6357 return currentTapHandler.onTouchEvent(event); 6358 } 6359 6360 @Override 6361 public void onFocusChange(View arg0, boolean arg1) { 6362 stopTracking(); 6363 if (currentAutoScrollAnimation != null) 6364 stopAutoScroll(); 6365 } 6366 6367 public void redraw() { 6368 //BackgroundThread.instance().executeBackground(new Runnable() { 6369 BackgroundThread.instance().executeGUI(() -> { 6370 surface.invalidate(); 6371 invalidImages = true; 6372 //preparePageImage(0); 6373 bookView.draw(); 6374 }); 6375 } 6376 6377 public ReaderView(CoolReader activity, Engine engine, Properties props) { 6378 //super(activity); 6379 log.i("Creating normal SurfaceView"); 6380 surface = new ReaderSurface(activity); 6381 6382 bookView = (BookView) surface; 6383 surface.setOnTouchListener(this); 6384 surface.setOnKeyListener(this); 6385 surface.setOnFocusChangeListener(this); 6386 doc = new DocView(Engine.lock); 6387 doc.setReaderCallback(readerCallback); 6388 SurfaceHolder holder = surface.getHolder(); 6389 holder.addCallback(this); 6390 6391 BackgroundThread.ensureGUI(); 6392 this.mActivity = activity; 6393 this.mEngine = engine; 6394 this.mEinkScreen = activity.getEinkScreen(); 6395 surface.setFocusable(true); 6396 surface.setFocusableInTouchMode(true); 6397 // set initial size to exclude java.lang.IllegalArgumentException in Bitmap.createBitmap(0, 0) 6398 // surface.getWidth() at this point return 0 6399 requestedWidth = 100; 6400 requestedHeight = 100; 6401 6402 BackgroundThread.instance().postBackground(() -> { 6403 log.d("ReaderView - in background thread: calling createInternal()"); 6404 doc.create(); 6405 mInitialized = true; 6406 }); 6407 6408 log.i("Posting create view task"); 6409 post(new CreateViewTask(props)); 6410 } 6411 } 6412