public class DocumentModel extends ListenerProxy { protected static final LogContext LCTX = LogContext.ROOT.lctx("DocModel"); protected PageIndex currentIndex = PageIndex.FIRST; private static final Page[] EMPTY_PAGES = {}; private final CodecContext context; private final DecodeService decodeService; private Page[] pages = EMPTY_PAGES; public DocumentModel(final CodecType activityType) { super(CurrentPageListener.class); if (activityType != null) { try { context = activityType.getContextClass().newInstance(); decodeService = new DecodeServiceBase(context); } catch (final Throwable th) { throw new RuntimeException(th); } } else { context = null; decodeService = new DecodeServiceStub(); } } public void open(String fileName, String password) { decodeService.open(fileName, password); } public DecodeService getDecodeService() { return decodeService; } public Page[] getPages() { return pages; } public Iterable<Page> getPages(final int start) { return new PageIterator(start, pages.length); } public Iterable<Page> getPages(final int start, final int end) { return new PageIterator(start, Math.min(end, pages.length)); } public int getPageCount() { return LengthUtils.length(pages); } public void recycle() { decodeService.recycle(); recyclePages(); } private void recyclePages() { if (LengthUtils.isNotEmpty(pages)) { final List<Bitmaps> bitmapsToRecycle = new ArrayList<Bitmaps>(); for (final Page page : pages) { page.recycle(bitmapsToRecycle); } BitmapManager.release(bitmapsToRecycle); BitmapManager.release(); } pages = EMPTY_PAGES; } public Page getPageObject(final int viewIndex) { return pages != null && 0 <= viewIndex && viewIndex < pages.length ? pages[viewIndex] : null; } public Page getPageByDocIndex(final int docIndex) { for (Page page : pages) { if (page.index.docIndex == docIndex) { return page; } } return null; } /** * Gets the current page object. * * @return the current page object */ public Page getCurrentPageObject() { return getPageObject(this.currentIndex.viewIndex); } /** * Gets the last page object. * * @return the last page object */ public Page getLastPageObject() { return getPageObject(pages.length - 1); } public void setCurrentPageIndex(final PageIndex newIndex) { if (!CompareUtils.equals(currentIndex, newIndex)) { if (LCTX.isDebugEnabled()) { LCTX.d("Current page changed: " + "currentIndex" + " -> " + newIndex); } final PageIndex oldIndex = this.currentIndex; this.currentIndex = newIndex; this.<CurrentPageListener>getListener().currentPageChanged(oldIndex, newIndex); } } public PageIndex getCurrentIndex() { return this.currentIndex; } public int getCurrentViewPageIndex() { return this.currentIndex.viewIndex; } public int getCurrentDocPageIndex() { return this.currentIndex.docIndex; } public void setCurrentPageByFirstVisible(final int firstVisiblePage) { final Page page = getPageObject(firstVisiblePage); if (page != null) { setCurrentPageIndex(page.index); } } public void initPages( final IActivityController base, final IActivityController.IBookLoadTask task) { recyclePages(); final BookSettings bs = SettingsManager.getBookSettings(); if (base == null || bs == null || context == null || decodeService == null) { return; } final IView view = base.getView(); final CodecPageInfo defCpi = new CodecPageInfo(); defCpi.width = (view.getWidth()); defCpi.height = (view.getHeight()); int viewIndex = 0; final long start = System.currentTimeMillis(); try { final ArrayList<Page> list = new ArrayList<Page>(); final CodecPageInfo[] infos = retrievePagesInfo(base, bs, task); for (int docIndex = 0; docIndex < infos.length; docIndex++) { if (!bs.splitPages || infos[docIndex] == null || (infos[docIndex].width < infos[docIndex].height)) { final Page page = new Page( base, new PageIndex(docIndex, viewIndex++), PageType.FULL_PAGE, infos[docIndex] != null ? infos[docIndex] : defCpi); list.add(page); } else { final Page page1 = new Page( base, new PageIndex(docIndex, viewIndex++), PageType.LEFT_PAGE, infos[docIndex]); list.add(page1); final Page page2 = new Page( base, new PageIndex(docIndex, viewIndex++), PageType.RIGHT_PAGE, infos[docIndex]); list.add(page2); } } pages = list.toArray(new Page[list.size()]); if (pages.length > 0) { createBookThumbnail(bs, pages[0], false); } } finally { LCTX.d("Loading page info: " + (System.currentTimeMillis() - start) + " ms"); } } public void createBookThumbnail(final BookSettings bs, final Page page, final boolean override) { final ThumbnailFile thumbnailFile = CacheManager.getThumbnailFile(bs.fileName); if (!override && thumbnailFile.exists()) { return; } int width = 200, height = 200; final RectF bounds = page.getBounds(1.0f); final float pageWidth = bounds.width(); final float pageHeight = bounds.height(); if (pageHeight > pageWidth) { width = (int) (200 * pageWidth / pageHeight); } else { height = (int) (200 * pageHeight / pageWidth); } BitmapRef image = decodeService.createThumbnail( width, height, page.index.docIndex, page.type.getInitialRect()); thumbnailFile.setImage(image != null ? image.getBitmap() : null); BitmapManager.release(image); } private CodecPageInfo[] retrievePagesInfo( final IActivityController base, final BookSettings bs, final IActivityController.IBookLoadTask task) { final PageCacheFile pagesFile = CacheManager.getPageFile(bs.fileName); if (decodeService.isPageSizeCacheable() && pagesFile.exists()) { final CodecPageInfo[] infos = pagesFile.load(); if (infos != null) { return infos; } } final CodecPageInfo[] infos = new CodecPageInfo[getDecodeService().getPageCount()]; final CodecPageInfo unified = decodeService.getUnifiedPageInfo(); for (int i = 0; i < infos.length; i++) { if (task != null) { task.setProgressDialogMessage(R.string.msg_getting_page_size, (i + 1), infos.length); } infos[i] = unified != null ? unified : getDecodeService().getPageInfo(i); } if (decodeService.isPageSizeCacheable()) { pagesFile.save(infos); } return infos; } private final class PageIterator implements Iterable<Page>, Iterator<Page> { private final int end; private int index; private PageIterator(final int start, final int end) { this.index = start; this.end = end; } @Override public boolean hasNext() { return 0 <= index && index < end; } @Override public Page next() { return hasNext() ? pages[index++] : null; } @Override public void remove() {} @Override public Iterator<Page> iterator() { return this; } } }
public class DrawThread extends Thread { private static final LogContext LCTX = LogContext.ROOT.lctx("Imaging"); private final SurfaceHolder surfaceHolder; private final BlockingQueue<ViewState> queue = new ArrayBlockingQueue<ViewState>(16, true); private final Flag stop = new Flag(); public DrawThread(final SurfaceHolder surfaceHolder) { this.surfaceHolder = surfaceHolder; } public void finish() { stop.set(); try { this.join(); } catch (final InterruptedException e) { } } @Override public void run() { while (!stop.get()) { draw(false); } } protected void draw(final boolean useLastState) { final ViewState viewState = takeTask(1, TimeUnit.SECONDS, useLastState); if (viewState == null) { return; } Canvas canvas = null; try { canvas = surfaceHolder.lockCanvas(null); EventPool.newEventDraw(viewState, canvas).process(); } catch (final Throwable th) { LCTX.e("Unexpected error on drawing: " + th.getMessage(), th); } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } } public ViewState takeTask(final long timeout, final TimeUnit unit, final boolean useLastState) { ViewState task = null; try { task = queue.poll(timeout, unit); if (task != null && useLastState) { final ArrayList<ViewState> list = new ArrayList<ViewState>(); // Workaround for possible ConcurrentModificationException while (true) { list.clear(); try { if (queue.drainTo(list) > 0) { task = list.get(list.size() - 1); } break; } catch (Throwable ex) { // Go to next attempt LCTX.e( "Unexpected error on retrieving last view state from draw queue: " + ex.getMessage()); } } } } catch (final InterruptedException e) { Thread.interrupted(); } catch (Throwable ex) { // Go to next attempt LCTX.e("Unexpected error on retrieving view state from draw queue: " + ex.getMessage()); } return task; } public void draw(final ViewState viewState) { if (viewState != null) { // Workaround for possible ConcurrentModificationException while (true) { try { queue.offer(viewState); break; } catch (Throwable ex) { // Go to next attempt LCTX.e("Unexpected error on adding view state to draw queue: " + ex.getMessage()); } } } } }
public class TouchManager { private static final LogContext LCTX = LogContext.ROOT.lctx("Actions"); public static final String DEFAULT_PROFILE = "DocumentView.Default"; private static final Map<String, TouchProfile> profiles = new HashMap<String, TouchManager.TouchProfile>(); private static final LinkedList<TouchProfile> stack = new LinkedList<TouchProfile>(); public static void loadFromSettings(final AppSettings newSettings) { profiles.clear(); stack.clear(); boolean fromJSON = false; final String str = newSettings.tapProfiles; if (LengthUtils.isNotEmpty(str)) { try { final List<TouchProfile> list = fromJSON(str); for (final TouchProfile p : list) { profiles.put(p.name, p); } } catch (final Throwable ex) { LCTX.e("Error on tap configuration load: ", ex); } fromJSON = profiles.containsKey(DEFAULT_PROFILE); } if (!fromJSON) { if (LCTX.isDebugEnabled()) { LCTX.d("Creating default tap configuration..."); } final TouchProfile def = addProfile(DEFAULT_PROFILE); { final Region r = def.addRegion(0, 0, 100, 100); r.setAction(Touch.DoubleTap, R.id.actions_openOptionsMenu, true); } { final Region r = def.addRegion(0, 0, 100, 10); r.setAction(Touch.SingleTap, R.id.actions_verticalConfigScrollUp, true); } { final Region r = def.addRegion(0, 90, 100, 100); r.setAction(Touch.SingleTap, R.id.actions_verticalConfigScrollDown, true); } persist(); } stack.addFirst(profiles.get(DEFAULT_PROFILE)); } public static void persist() { try { final JSONObject json = toJSON(); AppSettings.updateTapProfiles(json.toString()); } catch (final JSONException ex) { ex.printStackTrace(); } } public static void setActionEnabled(final String profile, final int id, final boolean enabled) { final TouchProfile tp = profiles.get(profile); for (final Region r : tp.regions) { for (final ActionRef a : r.actions) { if (a != null && a.id == id) { a.enabled = enabled; } } } } public static void setActionEnabled( final String profile, final int id, final boolean enabled, final int left, final int top, final int right, final int bottom) { final TouchProfile tp = profiles.get(profile); for (final Region r : tp.regions) { for (final ActionRef a : r.actions) { if (a != null && a.id == id) { a.enabled = enabled; r.rect.left = left; r.rect.top = top; r.rect.right = right; r.rect.bottom = bottom; return; } } } } public static Integer getAction( final Touch type, final float x, final float y, final float width, final float height) { return AppSettings.current().tapsEnabled ? stack.peek().getAction(type, x, y, width, height) : null; } public static TouchProfile addProfile(final String name) { final TouchProfile tp = new TouchProfile(name); profiles.put(tp.name, tp); return tp; } public static TouchProfile topProfile() { return stack.isEmpty() ? null : stack.peek(); } public static TouchProfile pushProfile(final String name) { final TouchProfile prev = stack.isEmpty() ? null : stack.peek(); final TouchProfile tp = profiles.get(name); if (tp != null) { stack.addFirst(tp); } return prev; } public static TouchProfile popProfile() { if (stack.size() > 1) { stack.removeFirst(); } return stack.peek(); } public static JSONObject toJSON() throws JSONException { final JSONObject object = new JSONObject(); final JSONArray array = new JSONArray(); for (final TouchProfile p : profiles.values()) { array.put(p.toJSON()); } object.put("profiles", array); return object; } private static List<TouchProfile> fromJSON(final String str) throws JSONException { final List<TouchProfile> list = new ArrayList<TouchProfile>(); final JSONObject root = new JSONObject(str); final JSONArray profiles = root.getJSONArray("profiles"); for (int pIndex = 0; pIndex < profiles.length(); pIndex++) { final JSONObject p = profiles.getJSONObject(pIndex); final TouchProfile profile = TouchProfile.fromJSON(p); list.add(profile); } return list; } public static class TouchProfile { public final String name; final LinkedList<Region> regions = new LinkedList<Region>(); public TouchProfile(final String name) { super(); this.name = name; } public ListIterator<Region> regions() { return regions.listIterator(); } public void clear() { regions.clear(); } public Integer getAction( final Touch type, final float x, final float y, final float width, final float height) { LCTX.d("getAction(" + type + ", " + x + ", " + y + ", " + width + ", " + height + ")"); for (final Region r : regions) { final RectF rect = r.getActualRect(width, height); LCTX.d("Region: " + rect); if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) { final ActionRef action = r.getAction(type); LCTX.d("Action: " + action); if (action != null && action.enabled) { return action.id; } } } return null; } public Region getRegion(final float x, final float y, final float width, final float height) { LCTX.d("getRegion(" + x + ", " + y + ", " + width + ", " + height + ")"); for (final Region r : regions) { final RectF rect = r.getActualRect(width, height); LCTX.d("Region: " + rect); if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) { return r; } } return null; } public Region addRegion(final int left, final int top, final int right, final int bottom) { final Region r = new Region(new Rect(left, top, right, bottom)); return addRegion(r); } public Region addRegion(final Region r) { regions.addFirst(r); return r; } public void removeRegion(final Region r) { regions.remove(r); } @Override public String toString() { final StringBuilder buf = new StringBuilder(this.getClass().getSimpleName()); buf.append("["); buf.append("name").append("=").append(name); buf.append(", "); buf.append("regions").append("=").append(regions); buf.append("]"); return buf.toString(); } public JSONObject toJSON() throws JSONException { final JSONObject object = new JSONObject(); object.put("name", this.name); final JSONArray array = new JSONArray(); for (final Region r : regions) { array.put(r.toJSON()); } object.put("regions", array); return object; } public static TouchProfile fromJSON(final JSONObject json) throws JSONException { final TouchProfile profile = new TouchProfile(json.getString("name")); final JSONArray regions = json.getJSONArray("regions"); for (int rIndex = 0; rIndex < regions.length(); rIndex++) { final JSONObject r = regions.getJSONObject(rIndex); final Region region = Region.fromJSON(r); profile.regions.add(region); } return profile; } } public static enum Touch { SingleTap, DoubleTap, LongTap, TwoFingerTap; } public static class Region { private final Rect rect; private final ActionRef[] actions = new ActionRef[Touch.values().length]; public Region(final Rect r) { rect = r; } public Region(final Region r) { this.rect = new Rect(r.rect); for (int i = 0; i < actions.length; i++) { this.actions[i] = r.actions[i]; } } public Rect getRect() { return rect; } public ActionRef getAction(final Touch type) { return actions[type.ordinal()]; } public ActionRef setAction(final Touch type, final int id, final boolean enabled) { final ActionRef a = new ActionRef(type, id, enabled); actions[type.ordinal()] = a; return a; } public RectF getActualRect(final float width, final float height) { return new RectF( width * rect.left / 100.0f, height * rect.top / 100.0f, width * rect.right / 100.0f, height * rect.bottom / 100.0f); } public void clear(Touch type) { this.actions[type.ordinal()] = null; } public void clear() { for (int i = 0; i < actions.length; i++) { this.actions[i] = null; } } @Override public String toString() { final StringBuilder buf = new StringBuilder(this.getClass().getSimpleName()); buf.append("["); buf.append("rect").append("=").append(rect); buf.append(", "); buf.append("actions").append("=").append(Arrays.toString(actions)); buf.append("]"); return buf.toString(); } public JSONObject toJSON() throws JSONException { final JSONObject object = new JSONObject(); final JSONObject r = new JSONObject(); r.put("left", rect.left); r.put("top", rect.top); r.put("right", rect.right); r.put("bottom", rect.bottom); object.put("rect", r); final JSONArray a = new JSONArray(); for (final ActionRef action : actions) { if (action != null) { a.put(action.toJSON()); } } object.put("actions", a); return object; } public static Region fromJSON(final JSONObject json) throws JSONException { final JSONObject r = json.getJSONObject("rect"); final Rect rect = new Rect(r.getInt("left"), r.getInt("top"), r.getInt("right"), r.getInt("bottom")); final Region region = new Region(rect); final JSONArray actions = json.getJSONArray("actions"); for (int aIndex = 0; aIndex < actions.length(); aIndex++) { try { final JSONObject a = actions.getJSONObject(aIndex); final Touch type = Touch.valueOf(a.getString("type")); final String name = a.getString("name"); final Integer id = ActionEx.getActionId(name); if (id != null) { region.setAction(type, id, a.getBoolean("enabled")); } else { LCTX.e("Unknown action name: " + name); } } catch (final JSONException ex) { throw new JSONException( "Old perssitent format found. Touch action are returned to default ones: " + ex.getMessage()); } } return region; } } public static class ActionRef { public final Touch type; public final int id; public final String name; public boolean enabled; public ActionRef(final Touch type, final int id, final boolean enabled) { this.type = type; this.id = id; this.name = ActionEx.getActionName(id); this.enabled = enabled; } public JSONObject toJSON() throws JSONException { final JSONObject object = new JSONObject(); object.put("type", type.name()); object.put("name", name); object.put("enabled", enabled); return object; } @Override public String toString() { return "(" + type + ", " + name + ", " + enabled + ")"; } } }