/* * Copyright (C) 2010-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.android.fbreader.libraryService; import java.util.*; import android.app.Service; import android.content.Intent; import android.graphics.Bitmap; import android.os.IBinder; import android.os.FileObserver; import org.geometerplus.zlibrary.core.options.Config; import org.geometerplus.zlibrary.text.view.ZLTextFixedPosition; import org.geometerplus.zlibrary.text.view.ZLTextPosition; import org.geometerplus.zlibrary.ui.android.image.ZLAndroidImageData; import org.geometerplus.zlibrary.ui.android.image.ZLAndroidImageManager; import org.geometerplus.fbreader.Paths; import org.geometerplus.fbreader.book.*; import org.geometerplus.android.fbreader.api.FBReaderIntents; import org.geometerplus.android.fbreader.httpd.DataService; import org.geometerplus.android.fbreader.httpd.DataUtil; public class LibraryService extends Service { private static SQLiteBooksDatabase ourDatabase; private static final Object ourDatabaseLock = new Object(); final DataService.Connection DataConnection = new DataService.Connection(); private static final class Observer extends FileObserver { private static final int MASK = MOVE_SELF | MOVED_TO | MOVED_FROM | DELETE_SELF | DELETE | CLOSE_WRITE | ATTRIB; private final String myPrefix; private final BookCollection myCollection; public Observer(String path, BookCollection collection) { super(path, MASK); myPrefix = path + '/'; myCollection = collection; } @Override public void onEvent(int event, String path) { event = event & ALL_EVENTS; System.err.println("Event " + event + " on " + path); switch (event) { case MOVE_SELF: // TODO: File(path) removed; stop watching (?) break; case MOVED_TO: myCollection.rescan(myPrefix + path); break; case MOVED_FROM: case DELETE: myCollection.rescan(myPrefix + path); break; case DELETE_SELF: // TODO: File(path) removed; watching is stopped automatically (?) break; case CLOSE_WRITE: case ATTRIB: myCollection.rescan(myPrefix + path); break; default: System.err.println("Unexpected event " + event + " on " + myPrefix + path); break; } } } public final class LibraryImplementation extends LibraryInterface.Stub { private final BooksDatabase myDatabase; private final List<FileObserver> myFileObservers = new LinkedList<FileObserver>(); private BookCollection myCollection; LibraryImplementation(BooksDatabase db) { myDatabase = db; myCollection = new BookCollection( Paths.systemInfo(LibraryService.this), myDatabase, Paths.bookPath() ); reset(true); } public void reset(final boolean force) { Config.Instance().runOnConnect(new Runnable() { public void run() { resetInternal(force); } }); } private void resetInternal(boolean force) { final List<String> bookDirectories = Paths.bookPath(); if (!force && myCollection.status() != BookCollection.Status.NotStarted && bookDirectories.equals(myCollection.BookDirectories) ) { return; } deactivate(); myFileObservers.clear(); myCollection = new BookCollection( Paths.systemInfo(LibraryService.this), myDatabase, bookDirectories ); for (String dir : bookDirectories) { final Observer observer = new Observer(dir, myCollection); observer.startWatching(); myFileObservers.add(observer); } myCollection.addListener(new BookCollection.Listener<DbBook>() { public void onBookEvent(BookEvent event, DbBook book) { final Intent intent = new Intent(FBReaderIntents.Event.LIBRARY_BOOK); intent.putExtra("type", event.toString()); intent.putExtra("book", SerializerUtil.serialize(book)); sendBroadcast(intent); } public void onBuildEvent(BookCollection.Status status) { final Intent intent = new Intent(FBReaderIntents.Event.LIBRARY_BUILD); intent.putExtra("type", status.toString()); sendBroadcast(intent); } }); myCollection.startBuild(); } public void deactivate() { for (FileObserver observer : myFileObservers) { observer.stopWatching(); } } public String status() { return myCollection.status().toString(); } public int size() { return myCollection.size(); } public List<String> books(String query) { return SerializerUtil.serializeBookList( myCollection.books(SerializerUtil.deserializeBookQuery(query)) ); } public boolean hasBooks(String query) { return myCollection.hasBooks(SerializerUtil.deserializeBookQuery(query).Filter); } public List<String> recentBooks() { return recentlyOpenedBooks(12); } public List<String> recentlyOpenedBooks(int count) { return SerializerUtil.serializeBookList(myCollection.recentlyOpenedBooks(count)); } public List<String> recentlyAddedBooks(int count) { return SerializerUtil.serializeBookList(myCollection.recentlyAddedBooks(count)); } public String getRecentBook(int index) { return SerializerUtil.serialize(myCollection.getRecentBook(index)); } public String getBookByFile(String path) { return SerializerUtil.serialize(myCollection.getBookByFile(path)); } public String getBookById(long id) { return SerializerUtil.serialize(myCollection.getBookById(id)); } public String getBookByUid(String type, String id) { return SerializerUtil.serialize(myCollection.getBookByUid(new UID(type, id))); } public String getBookByHash(String hash) { return SerializerUtil.serialize(myCollection.getBookByHash(hash)); } public List<String> authors() { final List<Author> authors = myCollection.authors(); final List<String> strings = new ArrayList<String>(authors.size()); for (Author a : authors) { strings.add(Util.authorToString(a)); } return strings; } public boolean hasSeries() { return myCollection.hasSeries(); } public List<String> series() { return myCollection.series(); } public List<String> tags() { final List<Tag> tags = myCollection.tags(); final List<String> strings = new ArrayList<String>(tags.size()); for (Tag t : tags) { strings.add(Util.tagToString(t)); } return strings; } public List<String> titles(String query) { return myCollection.titles(SerializerUtil.deserializeBookQuery(query)); } public List<String> firstTitleLetters() { return myCollection.firstTitleLetters(); } public boolean saveBook(String book) { return myCollection.saveBook(SerializerUtil.deserializeBook(book, myCollection)); } public boolean canRemoveBook(String book, boolean deleteFromDisk) { return myCollection.canRemoveBook(SerializerUtil.deserializeBook(book, myCollection), deleteFromDisk); } public void removeBook(String book, boolean deleteFromDisk) { myCollection.removeBook(SerializerUtil.deserializeBook(book, myCollection), deleteFromDisk); } public void addToRecentlyOpened(String book) { myCollection.addToRecentlyOpened(SerializerUtil.deserializeBook(book, myCollection)); } public void removeFromRecentlyOpened(String book) { myCollection.removeFromRecentlyOpened(SerializerUtil.deserializeBook(book, myCollection)); } public List<String> labels() { return myCollection.labels(); } public PositionWithTimestamp getStoredPosition(long bookId) { final ZLTextPosition position = myCollection.getStoredPosition(bookId); return position != null ? new PositionWithTimestamp(position) : null; } public void storePosition(long bookId, PositionWithTimestamp pos) { if (pos == null) { return; } myCollection.storePosition(bookId, new ZLTextFixedPosition.WithTimestamp( pos.ParagraphIndex, pos.ElementIndex, pos.CharIndex, pos.Timestamp )); } @Override public boolean isHyperlinkVisited(String book, String linkId) { return myCollection.isHyperlinkVisited(SerializerUtil.deserializeBook(book, myCollection), linkId); } @Override public void markHyperlinkAsVisited(String book, String linkId) { myCollection.markHyperlinkAsVisited(SerializerUtil.deserializeBook(book, myCollection), linkId); } @Override public String getCoverUrl(String path) { return DataUtil.buildUrl(DataConnection, "cover", path); } @Override public String getDescription(String book) { return BookUtil.getAnnotation(SerializerUtil.deserializeBook(book, myCollection), myCollection.PluginCollection); } @Override public Bitmap getCover(final String bookString, final int maxWidth, final int maxHeight, boolean[] delayed) { // this method kept for compatibility delayed[0] = false; return null; } private Bitmap getResizedBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { if (maxWidth <= 0 || maxHeight <= 0) { return null; } final int bWidth = bitmap.getWidth(); final int bHeight = bitmap.getHeight(); if (maxWidth > bWidth && maxHeight > bHeight) { return null; } final int w, h; if (bWidth * maxHeight > bHeight * maxWidth) { w = maxWidth; h = Math.max(1, (int)(bHeight * (w + .5f) / bWidth)); } else { h = maxHeight; w = Math.max(1, (int)(bWidth * (h + .5f) / bHeight)); } if (2 * w <= bWidth && 2 * h <= bHeight) { return bitmap; } return Bitmap.createScaledBitmap(bitmap, w, h, false); } public List<String> bookmarks(String query) { return SerializerUtil.serializeBookmarkList(myCollection.bookmarks( SerializerUtil.deserializeBookmarkQuery(query, myCollection) )); } public String saveBookmark(String serialized) { final Bookmark bookmark = SerializerUtil.deserializeBookmark(serialized); myCollection.saveBookmark(bookmark); return SerializerUtil.serialize(bookmark); } public void deleteBookmark(String serialized) { myCollection.deleteBookmark(SerializerUtil.deserializeBookmark(serialized)); } public List<String> deletedBookmarkUids() { return myCollection.deletedBookmarkUids(); } public void purgeBookmarks(List<String> uids) { myCollection.purgeBookmarks(uids); } public String getHighlightingStyle(int styleId) { return SerializerUtil.serialize(myCollection.getHighlightingStyle(styleId)); } public List<String> highlightingStyles() { return SerializerUtil.serializeStyleList(myCollection.highlightingStyles()); } public void saveHighlightingStyle(String style) { myCollection.saveHighlightingStyle(SerializerUtil.deserializeStyle(style)); } public int getDefaultHighlightingStyleId() { return myCollection.getDefaultHighlightingStyleId(); } public void setDefaultHighlightingStyleId(int styleId) { myCollection.setDefaultHighlightingStyleId(styleId); } public void rescan(String path) { myCollection.rescan(path); } public String getHash(String book, boolean force) { return myCollection.getHash(SerializerUtil.deserializeBook(book, myCollection), force); } public void setHash(String book, String hash) { myCollection.setHash(SerializerUtil.deserializeBook(book, myCollection), hash); } public List<String> formats() { final List<IBookCollection.FormatDescriptor> descriptors = myCollection.formats(); final List<String> serialized = new ArrayList<String>(descriptors.size()); for (IBookCollection.FormatDescriptor d : descriptors) { serialized.add(Util.formatDescriptorToString(d)); } return serialized; } public boolean setActiveFormats(List<String> formatIds) { if (myCollection.setActiveFormats(formatIds)) { reset(true); return true; } else { return false; } } } private volatile LibraryImplementation myLibrary; @Override public void onStart(Intent intent, int startId) { onStartCommand(intent, 0, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } @Override public IBinder onBind(Intent intent) { return myLibrary; } @Override public void onCreate() { super.onCreate(); synchronized (ourDatabaseLock) { if (ourDatabase == null) { ourDatabase = new SQLiteBooksDatabase(LibraryService.this); } } myLibrary = new LibraryImplementation(ourDatabase); bindService( new Intent(this, DataService.class), DataConnection, DataService.BIND_AUTO_CREATE ); } @Override public void onDestroy() { unbindService(DataConnection); if (myLibrary != null) { final LibraryImplementation l = myLibrary; myLibrary = null; l.deactivate(); } super.onDestroy(); } }