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