/*
* Copyright (C) 2007-2015 FBReader.ORG Limited <contact@fbreader.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
package org.geometerplus.fbreader.fbreader;
import java.util.*;
import org.fbreader.util.ComparisonUtil;
import org.geometerplus.zlibrary.core.application.*;
import org.geometerplus.zlibrary.core.drm.FileEncryptionInfo;
import org.geometerplus.zlibrary.core.drm.EncryptionMethod;
import org.geometerplus.zlibrary.core.util.*;
import org.geometerplus.zlibrary.text.hyphenation.ZLTextHyphenator;
import org.geometerplus.zlibrary.text.model.ZLTextModel;
import org.geometerplus.zlibrary.text.view.*;
import org.geometerplus.fbreader.book.*;
import org.geometerplus.fbreader.bookmodel.*;
import org.geometerplus.fbreader.fbreader.options.*;
import org.geometerplus.fbreader.formats.*;
import org.geometerplus.fbreader.network.sync.SyncData;
import org.geometerplus.fbreader.util.*;
public final class FBReaderApp extends ZLApplication {
public interface ExternalFileOpener {
public void openFile(ExternalFormatPlugin plugin, Book book, Bookmark bookmark);
}
public static interface Notifier {
void showMissingBookNotification(SyncData.ServerBookInfo info);
}
private ExternalFileOpener myExternalFileOpener;
public void setExternalFileOpener(ExternalFileOpener o) {
myExternalFileOpener = o;
}
public final MiscOptions MiscOptions = new MiscOptions();
public final ImageOptions ImageOptions = new ImageOptions();
public final ViewOptions ViewOptions = new ViewOptions();
public final PageTurningOptions PageTurningOptions = new PageTurningOptions();
public final SyncOptions SyncOptions = new SyncOptions();
private final ZLKeyBindings myBindings = new ZLKeyBindings();
public final FBView BookTextView;
public final FBView FootnoteView;
private String myFootnoteModelId;
public volatile BookModel Model;
public volatile Book ExternalBook;
private ZLTextPosition myJumpEndPosition;
private Date myJumpTimeStamp;
public final IBookCollection<Book> Collection;
private final SyncData mySyncData = new SyncData();
public FBReaderApp(SystemInfo systemInfo, final IBookCollection<Book> collection) {
super(systemInfo);
Collection = collection;
collection.addListener(new IBookCollection.Listener<Book>() {
public void onBookEvent(BookEvent event, Book book) {
switch (event) {
case BookmarkStyleChanged:
case BookmarksUpdated:
if (Model != null && (book == null || collection.sameBook(book, Model.Book))) {
if (BookTextView.getModel() != null) {
setBookmarkHighlightings(BookTextView, null);
}
if (FootnoteView.getModel() != null && myFootnoteModelId != null) {
setBookmarkHighlightings(FootnoteView, myFootnoteModelId);
}
}
break;
case Updated:
onBookUpdated(book);
break;
}
}
public void onBuildEvent(IBookCollection.Status status) {
}
});
addAction(ActionCode.INCREASE_FONT, new ChangeFontSizeAction(this, +2));
addAction(ActionCode.DECREASE_FONT, new ChangeFontSizeAction(this, -2));
addAction(ActionCode.FIND_NEXT, new FindNextAction(this));
addAction(ActionCode.FIND_PREVIOUS, new FindPreviousAction(this));
addAction(ActionCode.CLEAR_FIND_RESULTS, new ClearFindResultsAction(this));
addAction(ActionCode.SELECTION_CLEAR, new SelectionClearAction(this));
addAction(ActionCode.TURN_PAGE_FORWARD, new TurnPageAction(this, true));
addAction(ActionCode.TURN_PAGE_BACK, new TurnPageAction(this, false));
addAction(ActionCode.MOVE_CURSOR_UP, new MoveCursorAction(this, FBView.Direction.up));
addAction(ActionCode.MOVE_CURSOR_DOWN, new MoveCursorAction(this, FBView.Direction.down));
addAction(ActionCode.MOVE_CURSOR_LEFT, new MoveCursorAction(this, FBView.Direction.rightToLeft));
addAction(ActionCode.MOVE_CURSOR_RIGHT, new MoveCursorAction(this, FBView.Direction.leftToRight));
addAction(ActionCode.VOLUME_KEY_SCROLL_FORWARD, new VolumeKeyTurnPageAction(this, true));
addAction(ActionCode.VOLUME_KEY_SCROLL_BACK, new VolumeKeyTurnPageAction(this, false));
addAction(ActionCode.EXIT, new ExitAction(this));
BookTextView = new FBView(this);
FootnoteView = new FBView(this);
setView(BookTextView);
}
public Book getCurrentBook() {
final BookModel m = Model;
return m != null ? m.Book : ExternalBook;
}
public void openHelpBook() {
openBook(Collection.getBookByFile(BookUtil.getHelpFile().getPath()), null, null, null);
}
public Book getCurrentServerBook(Notifier notifier) {
final SyncData.ServerBookInfo info = mySyncData.getServerBookInfo();
if (info == null) {
return null;
}
for (String hash : info.Hashes) {
final Book book = Collection.getBookByHash(hash);
if (book != null) {
return book;
}
}
if (notifier != null) {
notifier.showMissingBookNotification(info);
}
return null;
}
public void openBook(Book book, final Bookmark bookmark, Runnable postAction, Notifier notifier) {
if (Model != null) {
if (book == null || bookmark == null && Collection.sameBook(book, Model.Book)) {
return;
}
}
if (book == null) {
book = getCurrentServerBook(notifier);
if (book == null) {
book = Collection.getRecentBook(0);
}
if (book == null || !BookUtil.fileByBook(book).exists()) {
book = Collection.getBookByFile(BookUtil.getHelpFile().getPath());
}
if (book == null) {
return;
}
}
final Book bookToOpen = book;
bookToOpen.addNewLabel(Book.READ_LABEL);
Collection.saveBook(bookToOpen);
final SynchronousExecutor executor = createExecutor("loadingBook");
executor.execute(new Runnable() {
public void run() {
openBookInternal(bookToOpen, bookmark, false);
}
}, postAction);
}
private void reloadBook() {
final Book book = getCurrentBook();
if (book != null) {
final SynchronousExecutor executor = createExecutor("loadingBook");
executor.execute(new Runnable() {
public void run() {
openBookInternal(book, null, true);
}
}, null);
}
}
public ZLKeyBindings keyBindings() {
return myBindings;
}
public FBView getTextView() {
return (FBView)getCurrentView();
}
public AutoTextSnippet getFootnoteData(String id) {
if (Model == null) {
return null;
}
final BookModel.Label label = Model.getLabel(id);
if (label == null) {
return null;
}
final ZLTextModel model;
if (label.ModelId != null) {
model = Model.getFootnoteModel(label.ModelId);
} else {
model = Model.getTextModel();
}
if (model == null) {
return null;
}
final ZLTextWordCursor cursor =
new ZLTextWordCursor(new ZLTextParagraphCursor(model, label.ParagraphIndex));
final AutoTextSnippet longSnippet = new AutoTextSnippet(cursor, 140);
if (longSnippet.IsEndOfText) {
return longSnippet;
} else {
return new AutoTextSnippet(cursor, 100);
}
}
public void tryOpenFootnote(String id) {
if (Model != null) {
myJumpEndPosition = null;
myJumpTimeStamp = null;
final BookModel.Label label = Model.getLabel(id);
if (label != null) {
if (label.ModelId == null) {
if (getTextView() == BookTextView) {
addInvisibleBookmark();
myJumpEndPosition = new ZLTextFixedPosition(label.ParagraphIndex, 0, 0);
myJumpTimeStamp = new Date();
}
BookTextView.gotoPosition(label.ParagraphIndex, 0, 0);
setView(BookTextView);
} else {
setFootnoteModel(label.ModelId);
setView(FootnoteView);
FootnoteView.gotoPosition(label.ParagraphIndex, 0, 0);
}
getViewWidget().repaint();
storePosition();
}
}
}
public void clearTextCaches() {
BookTextView.clearCaches();
FootnoteView.clearCaches();
}
public Bookmark addSelectionBookmark() {
final FBView fbView = getTextView();
final TextSnippet snippet = fbView.getSelectedSnippet();
if (snippet == null) {
return null;
}
final Bookmark bookmark = new Bookmark(
Collection,
Model.Book,
fbView.getModel().getId(),
snippet,
true
);
Collection.saveBookmark(bookmark);
fbView.clearSelection();
return bookmark;
}
private void setBookmarkHighlightings(ZLTextView view, String modelId) {
view.removeHighlightings(BookmarkHighlighting.class);
for (BookmarkQuery query = new BookmarkQuery(Model.Book, 20); ; query = query.next()) {
final List<Bookmark> bookmarks = Collection.bookmarks(query);
if (bookmarks.isEmpty()) {
break;
}
for (Bookmark b : bookmarks) {
if (b.getEnd() == null) {
BookmarkUtil.findEnd(b, view);
}
if (ComparisonUtil.equal(modelId, b.ModelId)) {
view.addHighlighting(new BookmarkHighlighting(view, Collection, b));
}
}
}
}
private void setFootnoteModel(String modelId) {
final ZLTextModel model = Model.getFootnoteModel(modelId);
FootnoteView.setModel(model);
if (model != null) {
myFootnoteModelId = modelId;
setBookmarkHighlightings(FootnoteView, modelId);
}
}
private synchronized void openBookInternal(final Book book, Bookmark bookmark, boolean force) {
if (!force && Model != null && Collection.sameBook(book, Model.Book)) {
if (bookmark != null) {
gotoBookmark(bookmark, false);
}
return;
}
hideActivePopup();
storePosition();
BookTextView.setModel(null);
FootnoteView.setModel(null);
clearTextCaches();
Model = null;
ExternalBook = null;
System.gc();
System.gc();
final PluginCollection pluginCollection = PluginCollection.Instance(SystemInfo);
final FormatPlugin plugin;
try {
plugin = BookUtil.getPlugin(pluginCollection, book);
} catch (BookReadingException e) {
processException(e);
return;
}
if (plugin instanceof ExternalFormatPlugin) {
ExternalBook = book;
final Bookmark bm;
if (bookmark != null) {
bm = bookmark;
} else {
ZLTextPosition pos = getStoredPosition(book);
if (pos == null) {
pos = new ZLTextFixedPosition(0, 0, 0);
}
bm = new Bookmark(Collection, book, "", new EmptyTextSnippet(pos), false);
}
myExternalFileOpener.openFile((ExternalFormatPlugin)plugin, book, bm);
return;
}
try {
Model = BookModel.createModel(book, plugin);
Collection.saveBook(book);
ZLTextHyphenator.Instance().load(book.getLanguage());
BookTextView.setModel(Model.getTextModel());
setBookmarkHighlightings(BookTextView, null);
gotoStoredPosition();
if (bookmark == null) {
setView(BookTextView);
} else {
gotoBookmark(bookmark, false);
}
Collection.addToRecentlyOpened(book);
final StringBuilder title = new StringBuilder(book.getTitle());
if (!book.authors().isEmpty()) {
boolean first = true;
for (Author a : book.authors()) {
title.append(first ? " (" : ", ");
title.append(a.DisplayName);
first = false;
}
title.append(")");
}
setTitle(title.toString());
} catch (BookReadingException e) {
processException(e);
}
getViewWidget().reset();
getViewWidget().repaint();
for (FileEncryptionInfo info : plugin.readEncryptionInfos(book)) {
if (info != null && !EncryptionMethod.isSupported(info.Method)) {
showErrorMessage("unsupportedEncryptionMethod", book.getPath());
break;
}
}
}
private List<Bookmark> invisibleBookmarks() {
final List<Bookmark> bookmarks = Collection.bookmarks(
new BookmarkQuery(Model.Book, false, 10)
);
Collections.sort(bookmarks, new Bookmark.ByTimeComparator());
return bookmarks;
}
public boolean jumpBack() {
try {
if (getTextView() != BookTextView) {
showBookTextView();
return true;
}
if (myJumpEndPosition == null || myJumpTimeStamp == null) {
return false;
}
// more than 2 minutes ago
if (myJumpTimeStamp.getTime() + 2 * 60 * 1000 < new Date().getTime()) {
return false;
}
if (!myJumpEndPosition.equals(BookTextView.getStartCursor())) {
return false;
}
final List<Bookmark> bookmarks = invisibleBookmarks();
if (bookmarks.isEmpty()) {
return false;
}
final Bookmark b = bookmarks.get(0);
Collection.deleteBookmark(b);
gotoBookmark(b, true);
return true;
} finally {
myJumpEndPosition = null;
myJumpTimeStamp = null;
}
}
private void gotoBookmark(Bookmark bookmark, boolean exactly) {
final String modelId = bookmark.ModelId;
if (modelId == null) {
addInvisibleBookmark();
if (exactly) {
BookTextView.gotoPosition(bookmark);
} else {
BookTextView.gotoHighlighting(
new BookmarkHighlighting(BookTextView, Collection, bookmark)
);
}
setView(BookTextView);
} else {
setFootnoteModel(modelId);
if (exactly) {
FootnoteView.gotoPosition(bookmark);
} else {
FootnoteView.gotoHighlighting(
new BookmarkHighlighting(FootnoteView, Collection, bookmark)
);
}
setView(FootnoteView);
}
getViewWidget().repaint();
storePosition();
}
public void showBookTextView() {
setView(BookTextView);
}
public void onWindowClosing() {
storePosition();
}
private class PositionSaver implements Runnable {
private final Book myBook;
private final ZLTextPosition myPosition;
private final RationalNumber myProgress;
PositionSaver(Book book, ZLTextPosition position, RationalNumber progress) {
myBook = book;
myPosition = position;
myProgress = progress;
}
public void run() {
Collection.storePosition(myBook.getId(), myPosition);
myBook.setProgress(myProgress);
Collection.saveBook(myBook);
}
}
private class SaverThread extends Thread {
private final List<Runnable> myTasks =
Collections.synchronizedList(new LinkedList<Runnable>());
SaverThread() {
setPriority(MIN_PRIORITY);
}
void add(Runnable task) {
myTasks.add(task);
}
public void run() {
while (true) {
synchronized (myTasks) {
while (!myTasks.isEmpty()) {
myTasks.remove(0).run();
}
}
try {
sleep(500);
} catch (InterruptedException e) {
}
}
}
}
public void useSyncInfo(boolean openOtherBook, Notifier notifier) {
if (openOtherBook && SyncOptions.ChangeCurrentBook.getValue()) {
final Book fromServer = getCurrentServerBook(notifier);
if (fromServer != null && !Collection.sameBook(fromServer, Collection.getRecentBook(0))) {
openBook(fromServer, null, null, notifier);
return;
}
}
if (myStoredPositionBook != null &&
mySyncData.hasPosition(Collection.getHash(myStoredPositionBook, true))) {
gotoStoredPosition();
storePosition();
}
}
private final SaverThread mySaverThread = new SaverThread();
private volatile ZLTextPosition myStoredPosition;
private volatile Book myStoredPositionBook;
private ZLTextFixedPosition getStoredPosition(Book book) {
final ZLTextFixedPosition.WithTimestamp fromServer =
mySyncData.getAndCleanPosition(Collection.getHash(book, true));
final ZLTextFixedPosition.WithTimestamp local =
Collection.getStoredPosition(book.getId());
if (local == null) {
return fromServer != null ? fromServer : new ZLTextFixedPosition(0, 0, 0);
} else if (fromServer == null) {
return local;
} else {
return fromServer.Timestamp >= local.Timestamp ? fromServer : local;
}
}
private void gotoStoredPosition() {
myStoredPositionBook = Model != null ? Model.Book : null;
if (myStoredPositionBook == null) {
return;
}
myStoredPosition = getStoredPosition(myStoredPositionBook);
BookTextView.gotoPosition(myStoredPosition);
savePosition();
}
public void storePosition() {
final Book bk = Model != null ? Model.Book : null;
if (bk != null && bk == myStoredPositionBook && myStoredPosition != null && BookTextView != null) {
final ZLTextPosition position = new ZLTextFixedPosition(BookTextView.getStartCursor());
if (!myStoredPosition.equals(position)) {
myStoredPosition = position;
savePosition();
}
}
}
private void savePosition() {
final RationalNumber progress = BookTextView.getProgress();
synchronized (mySaverThread) {
if (!mySaverThread.isAlive()) {
mySaverThread.start();
}
mySaverThread.add(new PositionSaver(myStoredPositionBook, myStoredPosition, progress));
}
}
public boolean hasCancelActions() {
return new CancelMenuHelper().getActionsList(Collection).size() > 1;
}
public void runCancelAction(CancelMenuHelper.ActionType type, Bookmark bookmark) {
switch (type) {
case library:
runAction(ActionCode.SHOW_LIBRARY);
break;
case networkLibrary:
runAction(ActionCode.SHOW_NETWORK_LIBRARY);
break;
case previousBook:
openBook(Collection.getRecentBook(1), null, null, null);
break;
case returnTo:
Collection.deleteBookmark(bookmark);
gotoBookmark(bookmark, true);
break;
case close:
closeWindow();
break;
}
}
private synchronized void updateInvisibleBookmarksList(Bookmark b) {
if (Model != null && Model.Book != null && b != null) {
for (Bookmark bm : invisibleBookmarks()) {
if (b.equals(bm)) {
Collection.deleteBookmark(bm);
}
}
Collection.saveBookmark(b);
final List<Bookmark> bookmarks = invisibleBookmarks();
for (int i = 3; i < bookmarks.size(); ++i) {
Collection.deleteBookmark(bookmarks.get(i));
}
}
}
public void addInvisibleBookmark(ZLTextWordCursor cursor) {
if (cursor == null) {
return;
}
cursor = new ZLTextWordCursor(cursor);
if (cursor.isNull()) {
return;
}
final ZLTextView textView = getTextView();
final ZLTextModel textModel;
final Book book;
final AutoTextSnippet snippet;
// textView.model will not be changed inside synchronised block
synchronized (textView) {
textModel = textView.getModel();
final BookModel model = Model;
book = model != null ? model.Book : null;
if (book == null || textView != BookTextView || textModel == null) {
return;
}
snippet = new AutoTextSnippet(cursor, 30);
}
updateInvisibleBookmarksList(new Bookmark(
Collection, book, textModel.getId(), snippet, false
));
}
public void addInvisibleBookmark() {
if (Model.Book != null && getTextView() == BookTextView) {
updateInvisibleBookmarksList(createBookmark(30, false));
}
}
public Bookmark createBookmark(int maxChars, boolean visible) {
final FBView view = getTextView();
final ZLTextWordCursor cursor = view.getStartCursor();
if (cursor.isNull()) {
return null;
}
return new Bookmark(
Collection,
Model.Book,
view.getModel().getId(),
new AutoTextSnippet(cursor, maxChars),
visible
);
}
public TOCTree getCurrentTOCElement() {
final ZLTextWordCursor cursor = BookTextView.getStartCursor();
if (Model == null || cursor == null) {
return null;
}
int index = cursor.getParagraphIndex();
if (cursor.isEndOfParagraph()) {
++index;
}
TOCTree treeToSelect = null;
for (TOCTree tree : Model.TOCTree) {
final TOCTree.Reference reference = tree.getReference();
if (reference == null) {
continue;
}
if (reference.ParagraphIndex > index) {
break;
}
treeToSelect = tree;
}
return treeToSelect;
}
public void onBookUpdated(Book book) {
if (Model == null || Model.Book == null || !Collection.sameBook(Model.Book, book)) {
return;
}
final String newEncoding = book.getEncodingNoDetection();
final String oldEncoding = Model.Book.getEncodingNoDetection();
Model.Book.updateFrom(book);
if (newEncoding != null && !newEncoding.equals(oldEncoding)) {
reloadBook();
} else {
ZLTextHyphenator.Instance().load(Model.Book.getLanguage());
clearTextCaches();
getViewWidget().repaint();
}
}
}