summaryrefslogtreecommitdiff
path: root/platform/android/viewer/src/com/artifex
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/viewer/src/com/artifex')
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/Annotation.java18
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/ArrayDeque.java855
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/AsyncTask.java670
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/CancellableAsyncTask.java79
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/CancellableTaskDefinition.java8
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFActivity.java227
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java66
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFItem.java15
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/Deque.java554
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/FilePicker.java21
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfo.java14
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoExternal.java14
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoInternal.java14
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoRemote.java18
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoVisitor.java7
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFActivity.java1417
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlert.java21
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java30
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCancellableTaskDefinition.java38
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCore.java402
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java87
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageView.java692
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReaderView.java276
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java43
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowView.java182
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFView.java33
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivity.java31
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivityData.java17
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/OutlineAdapter.java46
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/OutlineItem.java14
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/PageView.java698
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/PrintDialogActivity.java145
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/ReaderView.java936
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java35
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/SearchTask.java128
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/SearchTaskResult.java24
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/Separation.java15
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/Stepper.java42
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/TextChar.java12
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/TextWord.java17
-rw-r--r--platform/android/viewer/src/com/artifex/mupdfdemo/WidgetType.java9
41 files changed, 7970 insertions, 0 deletions
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/Annotation.java b/platform/android/viewer/src/com/artifex/mupdfdemo/Annotation.java
new file mode 100644
index 00000000..cf915524
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/Annotation.java
@@ -0,0 +1,18 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.RectF;
+
+public class Annotation extends RectF {
+ enum Type {
+ TEXT, LINK, FREETEXT, LINE, SQUARE, CIRCLE, POLYGON, POLYLINE, HIGHLIGHT,
+ UNDERLINE, SQUIGGLY, STRIKEOUT, STAMP, CARET, INK, POPUP, FILEATTACHMENT,
+ SOUND, MOVIE, WIDGET, SCREEN, PRINTERMARK, TRAPNET, WATERMARK, A3D, UNKNOWN
+ }
+
+ public final Type type;
+
+ public Annotation(float x0, float y0, float x1, float y1, int _type) {
+ super(x0, y0, x1, y1);
+ type = _type == -1 ? Type.UNKNOWN : Type.values()[_type];
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/ArrayDeque.java b/platform/android/viewer/src/com/artifex/mupdfdemo/ArrayDeque.java
new file mode 100644
index 00000000..4f06ea41
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/ArrayDeque.java
@@ -0,0 +1,855 @@
+/*
+ * Written by Josh Bloch of Google Inc. and released to the public domain,
+ * as explained at http://creativecommons.org/publicdomain/zero/1.0/.
+ */
+
+package com.artifex.mupdfdemo;
+
+import java.util.AbstractCollection;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.Stack;
+
+// BEGIN android-note
+// removed link to collections framework docs
+// END android-note
+
+/**
+ * Resizable-array implementation of the {@link Deque} interface. Array
+ * deques have no capacity restrictions; they grow as necessary to support
+ * usage. They are not thread-safe; in the absence of external
+ * synchronization, they do not support concurrent access by multiple threads.
+ * Null elements are prohibited. This class is likely to be faster than
+ * {@link Stack} when used as a stack, and faster than {@link LinkedList}
+ * when used as a queue.
+ *
+ * <p>Most <tt>ArrayDeque</tt> operations run in amortized constant time.
+ * Exceptions include {@link #remove(Object) remove}, {@link
+ * #removeFirstOccurrence removeFirstOccurrence}, {@link #removeLastOccurrence
+ * removeLastOccurrence}, {@link #contains contains}, {@link #iterator
+ * iterator.remove()}, and the bulk operations, all of which run in linear
+ * time.
+ *
+ * <p>The iterators returned by this class's <tt>iterator</tt> method are
+ * <i>fail-fast</i>: If the deque is modified at any time after the iterator
+ * is created, in any way except through the iterator's own <tt>remove</tt>
+ * method, the iterator will generally throw a {@link
+ * ConcurrentModificationException}. Thus, in the face of concurrent
+ * modification, the iterator fails quickly and cleanly, rather than risking
+ * arbitrary, non-deterministic behavior at an undetermined time in the
+ * future.
+ *
+ * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
+ * as it is, generally speaking, impossible to make any hard guarantees in the
+ * presence of unsynchronized concurrent modification. Fail-fast iterators
+ * throw <tt>ConcurrentModificationException</tt> on a best-effort basis.
+ * Therefore, it would be wrong to write a program that depended on this
+ * exception for its correctness: <i>the fail-fast behavior of iterators
+ * should be used only to detect bugs.</i>
+ *
+ * <p>This class and its iterator implement all of the
+ * <em>optional</em> methods of the {@link Collection} and {@link
+ * Iterator} interfaces.
+ *
+ * @author Josh Bloch and Doug Lea
+ * @since 1.6
+ * @param <E> the type of elements held in this collection
+ */
+public class ArrayDeque<E> extends AbstractCollection<E>
+ implements Deque<E>, Cloneable, java.io.Serializable
+{
+ /**
+ * The array in which the elements of the deque are stored.
+ * The capacity of the deque is the length of this array, which is
+ * always a power of two. The array is never allowed to become
+ * full, except transiently within an addX method where it is
+ * resized (see doubleCapacity) immediately upon becoming full,
+ * thus avoiding head and tail wrapping around to equal each
+ * other. We also guarantee that all array cells not holding
+ * deque elements are always null.
+ */
+ private transient Object[] elements;
+
+ /**
+ * The index of the element at the head of the deque (which is the
+ * element that would be removed by remove() or pop()); or an
+ * arbitrary number equal to tail if the deque is empty.
+ */
+ private transient int head;
+
+ /**
+ * The index at which the next element would be added to the tail
+ * of the deque (via addLast(E), add(E), or push(E)).
+ */
+ private transient int tail;
+
+ /**
+ * The minimum capacity that we'll use for a newly created deque.
+ * Must be a power of 2.
+ */
+ private static final int MIN_INITIAL_CAPACITY = 8;
+
+ // ****** Array allocation and resizing utilities ******
+
+ /**
+ * Allocate empty array to hold the given number of elements.
+ *
+ * @param numElements the number of elements to hold
+ */
+ private void allocateElements(int numElements) {
+ int initialCapacity = MIN_INITIAL_CAPACITY;
+ // Find the best power of two to hold elements.
+ // Tests "<=" because arrays aren't kept full.
+ if (numElements >= initialCapacity) {
+ initialCapacity = numElements;
+ initialCapacity |= (initialCapacity >>> 1);
+ initialCapacity |= (initialCapacity >>> 2);
+ initialCapacity |= (initialCapacity >>> 4);
+ initialCapacity |= (initialCapacity >>> 8);
+ initialCapacity |= (initialCapacity >>> 16);
+ initialCapacity++;
+
+ if (initialCapacity < 0) // Too many elements, must back off
+ initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
+ }
+ elements = new Object[initialCapacity];
+ }
+
+ /**
+ * Double the capacity of this deque. Call only when full, i.e.,
+ * when head and tail have wrapped around to become equal.
+ */
+ private void doubleCapacity() {
+ // assert head == tail;
+ int p = head;
+ int n = elements.length;
+ int r = n - p; // number of elements to the right of p
+ int newCapacity = n << 1;
+ if (newCapacity < 0)
+ throw new IllegalStateException("Sorry, deque too big");
+ Object[] a = new Object[newCapacity];
+ System.arraycopy(elements, p, a, 0, r);
+ System.arraycopy(elements, 0, a, r, p);
+ elements = a;
+ head = 0;
+ tail = n;
+ }
+
+ /**
+ * Copies the elements from our element array into the specified array,
+ * in order (from first to last element in the deque). It is assumed
+ * that the array is large enough to hold all elements in the deque.
+ *
+ * @return its argument
+ */
+ private <T> T[] copyElements(T[] a) {
+ if (head < tail) {
+ System.arraycopy(elements, head, a, 0, size());
+ } else if (head > tail) {
+ int headPortionLen = elements.length - head;
+ System.arraycopy(elements, head, a, 0, headPortionLen);
+ System.arraycopy(elements, 0, a, headPortionLen, tail);
+ }
+ return a;
+ }
+
+ /**
+ * Constructs an empty array deque with an initial capacity
+ * sufficient to hold 16 elements.
+ */
+ public ArrayDeque() {
+ elements = new Object[16];
+ }
+
+ /**
+ * Constructs an empty array deque with an initial capacity
+ * sufficient to hold the specified number of elements.
+ *
+ * @param numElements lower bound on initial capacity of the deque
+ */
+ public ArrayDeque(int numElements) {
+ allocateElements(numElements);
+ }
+
+ /**
+ * Constructs a deque containing the elements of the specified
+ * collection, in the order they are returned by the collection's
+ * iterator. (The first element returned by the collection's
+ * iterator becomes the first element, or <i>front</i> of the
+ * deque.)
+ *
+ * @param c the collection whose elements are to be placed into the deque
+ * @throws NullPointerException if the specified collection is null
+ */
+ public ArrayDeque(Collection<? extends E> c) {
+ allocateElements(c.size());
+ addAll(c);
+ }
+
+ // The main insertion and extraction methods are addFirst,
+ // addLast, pollFirst, pollLast. The other methods are defined in
+ // terms of these.
+
+ /**
+ * Inserts the specified element at the front of this deque.
+ *
+ * @param e the element to add
+ * @throws NullPointerException if the specified element is null
+ */
+ public void addFirst(E e) {
+ if (e == null)
+ throw new NullPointerException("e == null");
+ elements[head = (head - 1) & (elements.length - 1)] = e;
+ if (head == tail)
+ doubleCapacity();
+ }
+
+ /**
+ * Inserts the specified element at the end of this deque.
+ *
+ * <p>This method is equivalent to {@link #add}.
+ *
+ * @param e the element to add
+ * @throws NullPointerException if the specified element is null
+ */
+ public void addLast(E e) {
+ if (e == null)
+ throw new NullPointerException("e == null");
+ elements[tail] = e;
+ if ( (tail = (tail + 1) & (elements.length - 1)) == head)
+ doubleCapacity();
+ }
+
+ /**
+ * Inserts the specified element at the front of this deque.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> (as specified by {@link Deque#offerFirst})
+ * @throws NullPointerException if the specified element is null
+ */
+ public boolean offerFirst(E e) {
+ addFirst(e);
+ return true;
+ }
+
+ /**
+ * Inserts the specified element at the end of this deque.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> (as specified by {@link Deque#offerLast})
+ * @throws NullPointerException if the specified element is null
+ */
+ public boolean offerLast(E e) {
+ addLast(e);
+ return true;
+ }
+
+ /**
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E removeFirst() {
+ E x = pollFirst();
+ if (x == null)
+ throw new NoSuchElementException();
+ return x;
+ }
+
+ /**
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E removeLast() {
+ E x = pollLast();
+ if (x == null)
+ throw new NoSuchElementException();
+ return x;
+ }
+
+ public E pollFirst() {
+ int h = head;
+ @SuppressWarnings("unchecked") E result = (E) elements[h];
+ // Element is null if deque empty
+ if (result == null)
+ return null;
+ elements[h] = null; // Must null out slot
+ head = (h + 1) & (elements.length - 1);
+ return result;
+ }
+
+ public E pollLast() {
+ int t = (tail - 1) & (elements.length - 1);
+ @SuppressWarnings("unchecked") E result = (E) elements[t];
+ if (result == null)
+ return null;
+ elements[t] = null;
+ tail = t;
+ return result;
+ }
+
+ /**
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E getFirst() {
+ @SuppressWarnings("unchecked") E result = (E) elements[head];
+ if (result == null)
+ throw new NoSuchElementException();
+ return result;
+ }
+
+ /**
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E getLast() {
+ @SuppressWarnings("unchecked")
+ E result = (E) elements[(tail - 1) & (elements.length - 1)];
+ if (result == null)
+ throw new NoSuchElementException();
+ return result;
+ }
+
+ public E peekFirst() {
+ @SuppressWarnings("unchecked") E result = (E) elements[head];
+ // elements[head] is null if deque empty
+ return result;
+ }
+
+ public E peekLast() {
+ @SuppressWarnings("unchecked")
+ E result = (E) elements[(tail - 1) & (elements.length - 1)];
+ return result;
+ }
+
+ /**
+ * Removes the first occurrence of the specified element in this
+ * deque (when traversing the deque from head to tail).
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the first element <tt>e</tt> such that
+ * <tt>o.equals(e)</tt> (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if the deque contained the specified element
+ */
+ public boolean removeFirstOccurrence(Object o) {
+ if (o == null)
+ return false;
+ int mask = elements.length - 1;
+ int i = head;
+ Object x;
+ while ( (x = elements[i]) != null) {
+ if (o.equals(x)) {
+ delete(i);
+ return true;
+ }
+ i = (i + 1) & mask;
+ }
+ return false;
+ }
+
+ /**
+ * Removes the last occurrence of the specified element in this
+ * deque (when traversing the deque from head to tail).
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the last element <tt>e</tt> such that
+ * <tt>o.equals(e)</tt> (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if the deque contained the specified element
+ */
+ public boolean removeLastOccurrence(Object o) {
+ if (o == null)
+ return false;
+ int mask = elements.length - 1;
+ int i = (tail - 1) & mask;
+ Object x;
+ while ( (x = elements[i]) != null) {
+ if (o.equals(x)) {
+ delete(i);
+ return true;
+ }
+ i = (i - 1) & mask;
+ }
+ return false;
+ }
+
+ // *** Queue methods ***
+
+ /**
+ * Inserts the specified element at the end of this deque.
+ *
+ * <p>This method is equivalent to {@link #addLast}.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> (as specified by {@link Collection#add})
+ * @throws NullPointerException if the specified element is null
+ */
+ public boolean add(E e) {
+ addLast(e);
+ return true;
+ }
+
+ /**
+ * Inserts the specified element at the end of this deque.
+ *
+ * <p>This method is equivalent to {@link #offerLast}.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> (as specified by {@link Queue#offer})
+ * @throws NullPointerException if the specified element is null
+ */
+ public boolean offer(E e) {
+ return offerLast(e);
+ }
+
+ /**
+ * Retrieves and removes the head of the queue represented by this deque.
+ *
+ * This method differs from {@link #poll poll} only in that it throws an
+ * exception if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #removeFirst}.
+ *
+ * @return the head of the queue represented by this deque
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E remove() {
+ return removeFirst();
+ }
+
+ /**
+ * Retrieves and removes the head of the queue represented by this deque
+ * (in other words, the first element of this deque), or returns
+ * <tt>null</tt> if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #pollFirst}.
+ *
+ * @return the head of the queue represented by this deque, or
+ * <tt>null</tt> if this deque is empty
+ */
+ public E poll() {
+ return pollFirst();
+ }
+
+ /**
+ * Retrieves, but does not remove, the head of the queue represented by
+ * this deque. This method differs from {@link #peek peek} only in
+ * that it throws an exception if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #getFirst}.
+ *
+ * @return the head of the queue represented by this deque
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E element() {
+ return getFirst();
+ }
+
+ /**
+ * Retrieves, but does not remove, the head of the queue represented by
+ * this deque, or returns <tt>null</tt> if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #peekFirst}.
+ *
+ * @return the head of the queue represented by this deque, or
+ * <tt>null</tt> if this deque is empty
+ */
+ public E peek() {
+ return peekFirst();
+ }
+
+ // *** Stack methods ***
+
+ /**
+ * Pushes an element onto the stack represented by this deque. In other
+ * words, inserts the element at the front of this deque.
+ *
+ * <p>This method is equivalent to {@link #addFirst}.
+ *
+ * @param e the element to push
+ * @throws NullPointerException if the specified element is null
+ */
+ public void push(E e) {
+ addFirst(e);
+ }
+
+ /**
+ * Pops an element from the stack represented by this deque. In other
+ * words, removes and returns the first element of this deque.
+ *
+ * <p>This method is equivalent to {@link #removeFirst()}.
+ *
+ * @return the element at the front of this deque (which is the top
+ * of the stack represented by this deque)
+ * @throws NoSuchElementException {@inheritDoc}
+ */
+ public E pop() {
+ return removeFirst();
+ }
+
+ private void checkInvariants() {
+ // assert elements[tail] == null;
+ // assert head == tail ? elements[head] == null :
+ // (elements[head] != null &&
+ // elements[(tail - 1) & (elements.length - 1)] != null);
+ // assert elements[(head - 1) & (elements.length - 1)] == null;
+ }
+
+ /**
+ * Removes the element at the specified position in the elements array,
+ * adjusting head and tail as necessary. This can result in motion of
+ * elements backwards or forwards in the array.
+ *
+ * <p>This method is called delete rather than remove to emphasize
+ * that its semantics differ from those of {@link List#remove(int)}.
+ *
+ * @return true if elements moved backwards
+ */
+ private boolean delete(int i) {
+ //checkInvariants();
+ final Object[] elements = this.elements;
+ final int mask = elements.length - 1;
+ final int h = head;
+ final int t = tail;
+ final int front = (i - h) & mask;
+ final int back = (t - i) & mask;
+
+ // Invariant: head <= i < tail mod circularity
+ if (front >= ((t - h) & mask))
+ throw new ConcurrentModificationException();
+
+ // Optimize for least element motion
+ if (front < back) {
+ if (h <= i) {
+ System.arraycopy(elements, h, elements, h + 1, front);
+ } else { // Wrap around
+ System.arraycopy(elements, 0, elements, 1, i);
+ elements[0] = elements[mask];
+ System.arraycopy(elements, h, elements, h + 1, mask - h);
+ }
+ elements[h] = null;
+ head = (h + 1) & mask;
+ return false;
+ } else {
+ if (i < t) { // Copy the null tail as well
+ System.arraycopy(elements, i + 1, elements, i, back);
+ tail = t - 1;
+ } else { // Wrap around
+ System.arraycopy(elements, i + 1, elements, i, mask - i);
+ elements[mask] = elements[0];
+ System.arraycopy(elements, 1, elements, 0, t);
+ tail = (t - 1) & mask;
+ }
+ return true;
+ }
+ }
+
+ // *** Collection Methods ***
+
+ /**
+ * Returns the number of elements in this deque.
+ *
+ * @return the number of elements in this deque
+ */
+ public int size() {
+ return (tail - head) & (elements.length - 1);
+ }
+
+ /**
+ * Returns <tt>true</tt> if this deque contains no elements.
+ *
+ * @return <tt>true</tt> if this deque contains no elements
+ */
+ public boolean isEmpty() {
+ return head == tail;
+ }
+
+ /**
+ * Returns an iterator over the elements in this deque. The elements
+ * will be ordered from first (head) to last (tail). This is the same
+ * order that elements would be dequeued (via successive calls to
+ * {@link #remove} or popped (via successive calls to {@link #pop}).
+ *
+ * @return an iterator over the elements in this deque
+ */
+ public Iterator<E> iterator() {
+ return new DeqIterator();
+ }
+
+ public Iterator<E> descendingIterator() {
+ return new DescendingIterator();
+ }
+
+ private class DeqIterator implements Iterator<E> {
+ /**
+ * Index of element to be returned by subsequent call to next.
+ */
+ private int cursor = head;
+
+ /**
+ * Tail recorded at construction (also in remove), to stop
+ * iterator and also to check for comodification.
+ */
+ private int fence = tail;
+
+ /**
+ * Index of element returned by most recent call to next.
+ * Reset to -1 if element is deleted by a call to remove.
+ */
+ private int lastRet = -1;
+
+ public boolean hasNext() {
+ return cursor != fence;
+ }
+
+ public E next() {
+ if (cursor == fence)
+ throw new NoSuchElementException();
+ @SuppressWarnings("unchecked") E result = (E) elements[cursor];
+ // This check doesn't catch all possible comodifications,
+ // but does catch the ones that corrupt traversal
+ if (tail != fence || result == null)
+ throw new ConcurrentModificationException();
+ lastRet = cursor;
+ cursor = (cursor + 1) & (elements.length - 1);
+ return result;
+ }
+
+ public void remove() {
+ if (lastRet < 0)
+ throw new IllegalStateException();
+ if (delete(lastRet)) { // if left-shifted, undo increment in next()
+ cursor = (cursor - 1) & (elements.length - 1);
+ fence = tail;
+ }
+ lastRet = -1;
+ }
+ }
+
+ private class DescendingIterator implements Iterator<E> {
+ /*
+ * This class is nearly a mirror-image of DeqIterator, using
+ * tail instead of head for initial cursor, and head instead of
+ * tail for fence.
+ */
+ private int cursor = tail;
+ private int fence = head;
+ private int lastRet = -1;
+
+ public boolean hasNext() {
+ return cursor != fence;
+ }
+
+ public E next() {
+ if (cursor == fence)
+ throw new NoSuchElementException();
+ cursor = (cursor - 1) & (elements.length - 1);
+ @SuppressWarnings("unchecked") E result = (E) elements[cursor];
+ if (head != fence || result == null)
+ throw new ConcurrentModificationException();
+ lastRet = cursor;
+ return result;
+ }
+
+ public void remove() {
+ if (lastRet < 0)
+ throw new IllegalStateException();
+ if (!delete(lastRet)) {
+ cursor = (cursor + 1) & (elements.length - 1);
+ fence = head;
+ }
+ lastRet = -1;
+ }
+ }
+
+ /**
+ * Returns <tt>true</tt> if this deque contains the specified element.
+ * More formally, returns <tt>true</tt> if and only if this deque contains
+ * at least one element <tt>e</tt> such that <tt>o.equals(e)</tt>.
+ *
+ * @param o object to be checked for containment in this deque
+ * @return <tt>true</tt> if this deque contains the specified element
+ */
+ public boolean contains(Object o) {
+ if (o == null)
+ return false;
+ int mask = elements.length - 1;
+ int i = head;
+ Object x;
+ while ( (x = elements[i]) != null) {
+ if (o.equals(x))
+ return true;
+ i = (i + 1) & mask;
+ }
+ return false;
+ }
+
+ /**
+ * Removes a single instance of the specified element from this deque.
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the first element <tt>e</tt> such that
+ * <tt>o.equals(e)</tt> (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * <p>This method is equivalent to {@link #removeFirstOccurrence}.
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if this deque contained the specified element
+ */
+ public boolean remove(Object o) {
+ return removeFirstOccurrence(o);
+ }
+
+ /**
+ * Removes all of the elements from this deque.
+ * The deque will be empty after this call returns.
+ */
+ public void clear() {
+ int h = head;
+ int t = tail;
+ if (h != t) { // clear all cells
+ head = tail = 0;
+ int i = h;
+ int mask = elements.length - 1;
+ do {
+ elements[i] = null;
+ i = (i + 1) & mask;
+ } while (i != t);
+ }
+ }
+
+ /**
+ * Returns an array containing all of the elements in this deque
+ * in proper sequence (from first to last element).
+ *
+ * <p>The returned array will be "safe" in that no references to it are
+ * maintained by this deque. (In other words, this method must allocate
+ * a new array). The caller is thus free to modify the returned array.
+ *
+ * <p>This method acts as bridge between array-based and collection-based
+ * APIs.
+ *
+ * @return an array containing all of the elements in this deque
+ */
+ public Object[] toArray() {
+ return copyElements(new Object[size()]);
+ }
+
+ /**
+ * Returns an array containing all of the elements in this deque in
+ * proper sequence (from first to last element); the runtime type of the
+ * returned array is that of the specified array. If the deque fits in
+ * the specified array, it is returned therein. Otherwise, a new array
+ * is allocated with the runtime type of the specified array and the
+ * size of this deque.
+ *
+ * <p>If this deque fits in the specified array with room to spare
+ * (i.e., the array has more elements than this deque), the element in
+ * the array immediately following the end of the deque is set to
+ * <tt>null</tt>.
+ *
+ * <p>Like the {@link #toArray()} method, this method acts as bridge between
+ * array-based and collection-based APIs. Further, this method allows
+ * precise control over the runtime type of the output array, and may,
+ * under certain circumstances, be used to save allocation costs.
+ *
+ * <p>Suppose <tt>x</tt> is a deque known to contain only strings.
+ * The following code can be used to dump the deque into a newly
+ * allocated array of <tt>String</tt>:
+ *
+ * <pre> {@code String[] y = x.toArray(new String[0]);}</pre>
+ *
+ * Note that <tt>toArray(new Object[0])</tt> is identical in function to
+ * <tt>toArray()</tt>.
+ *
+ * @param a the array into which the elements of the deque are to
+ * be stored, if it is big enough; otherwise, a new array of the
+ * same runtime type is allocated for this purpose
+ * @return an array containing all of the elements in this deque
+ * @throws ArrayStoreException if the runtime type of the specified array
+ * is not a supertype of the runtime type of every element in
+ * this deque
+ * @throws NullPointerException if the specified array is null
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T[] toArray(T[] a) {
+ int size = size();
+ if (a.length < size)
+ a = (T[])java.lang.reflect.Array.newInstance(
+ a.getClass().getComponentType(), size);
+ copyElements(a);
+ if (a.length > size)
+ a[size] = null;
+ return a;
+ }
+
+ // *** Object methods ***
+
+ /**
+ * Returns a copy of this deque.
+ *
+ * @return a copy of this deque
+ */
+ public ArrayDeque<E> clone() {
+ try {
+ @SuppressWarnings("unchecked")
+ ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
+ result.elements = Arrays.copyOf(elements, elements.length);
+ return result;
+
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Appease the serialization gods.
+ */
+ private static final long serialVersionUID = 2340985798034038923L;
+
+ /**
+ * Serialize this deque.
+ *
+ * @serialData The current size (<tt>int</tt>) of the deque,
+ * followed by all of its elements (each an object reference) in
+ * first-to-last order.
+ */
+ private void writeObject(java.io.ObjectOutputStream s)
+ throws java.io.IOException {
+ s.defaultWriteObject();
+
+ // Write out size
+ s.writeInt(size());
+
+ // Write out elements in order.
+ int mask = elements.length - 1;
+ for (int i = head; i != tail; i = (i + 1) & mask)
+ s.writeObject(elements[i]);
+ }
+
+ /**
+ * Deserialize this deque.
+ */
+ private void readObject(java.io.ObjectInputStream s)
+ throws java.io.IOException, ClassNotFoundException {
+ s.defaultReadObject();
+
+ // Read in size and allocate array
+ int size = s.readInt();
+ allocateElements(size);
+ head = 0;
+ tail = size;
+
+ // Read in all elements in the proper order.
+ for (int i = 0; i < size; i++)
+ elements[i] = s.readObject();
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/AsyncTask.java b/platform/android/viewer/src/com/artifex/mupdfdemo/AsyncTask.java
new file mode 100644
index 00000000..b370794c
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/AsyncTask.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.artifex.mupdfdemo;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.os.Process;
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * <p>AsyncTask enables proper and easy use of the UI thread. This class allows to
+ * perform background operations and publish results on the UI thread without
+ * having to manipulate threads and/or handlers.</p>
+ *
+ * <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
+ * and does not constitute a generic threading framework. AsyncTasks should ideally be
+ * used for short operations (a few seconds at the most.) If you need to keep threads
+ * running for long periods of time, it is highly recommended you use the various APIs
+ * provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor},
+ * {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
+ *
+ * <p>An asynchronous task is defined by a computation that runs on a background thread and
+ * whose result is published on the UI thread. An asynchronous task is defined by 3 generic
+ * types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
+ * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
+ * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using tasks and threads, read the
+ * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
+ * Threads</a> developer guide.</p>
+ * </div>
+ *
+ * <h2>Usage</h2>
+ * <p>AsyncTask must be subclassed to be used. The subclass will override at least
+ * one method ({@link #doInBackground}), and most often will override a
+ * second one ({@link #onPostExecute}.)</p>
+ *
+ * <p>Here is an example of subclassing:</p>
+ * <pre class="prettyprint">
+ * private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; {
+ * protected Long doInBackground(URL... urls) {
+ * int count = urls.length;
+ * long totalSize = 0;
+ * for (int i = 0; i < count; i++) {
+ * totalSize += Downloader.downloadFile(urls[i]);
+ * publishProgress((int) ((i / (float) count) * 100));
+ * // Escape early if cancel() is called
+ * if (isCancelled()) break;
+ * }
+ * return totalSize;
+ * }
+ *
+ * protected void onProgressUpdate(Integer... progress) {
+ * setProgressPercent(progress[0]);
+ * }
+ *
+ * protected void onPostExecute(Long result) {
+ * showDialog("Downloaded " + result + " bytes");
+ * }
+ * }
+ * </pre>
+ *
+ * <p>Once created, a task is executed very simply:</p>
+ * <pre class="prettyprint">
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * </pre>
+ *
+ * <h2>AsyncTask's generic types</h2>
+ * <p>The three types used by an asynchronous task are the following:</p>
+ * <ol>
+ * <li><code>Params</code>, the type of the parameters sent to the task upon
+ * execution.</li>
+ * <li><code>Progress</code>, the type of the progress units published during
+ * the background computation.</li>
+ * <li><code>Result</code>, the type of the result of the background
+ * computation.</li>
+ * </ol>
+ * <p>Not all types are always used by an asynchronous task. To mark a type as unused,
+ * simply use the type {@link Void}:</p>
+ * <pre>
+ * private class MyTask extends AsyncTask&lt;Void, Void, Void&gt; { ... }
+ * </pre>
+ *
+ * <h2>The 4 steps</h2>
+ * <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
+ * <ol>
+ * <li>{@link #onPreExecute()}, invoked on the UI thread before the task
+ * is executed. This step is normally used to setup the task, for instance by
+ * showing a progress bar in the user interface.</li>
+ * <li>{@link #doInBackground}, invoked on the background thread
+ * immediately after {@link #onPreExecute()} finishes executing. This step is used
+ * to perform background computation that can take a long time. The parameters
+ * of the asynchronous task are passed to this step. The result of the computation must
+ * be returned by this step and will be passed back to the last step. This step
+ * can also use {@link #publishProgress} to publish one or more units
+ * of progress. These values are published on the UI thread, in the
+ * {@link #onProgressUpdate} step.</li>
+ * <li>{@link #onProgressUpdate}, invoked on the UI thread after a
+ * call to {@link #publishProgress}. The timing of the execution is
+ * undefined. This method is used to display any form of progress in the user
+ * interface while the background computation is still executing. For instance,
+ * it can be used to animate a progress bar or show logs in a text field.</li>
+ * <li>{@link #onPostExecute}, invoked on the UI thread after the background
+ * computation finishes. The result of the background computation is passed to
+ * this step as a parameter.</li>
+ * </ol>
+ *
+ * <h2>Cancelling a task</h2>
+ * <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
+ * this method will cause subsequent calls to {@link #isCancelled()} to return true.
+ * After invoking this method, {@link #onCancelled(Object)}, instead of
+ * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
+ * returns. To ensure that a task is cancelled as quickly as possible, you should always
+ * check the return value of {@link #isCancelled()} periodically from
+ * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
+ *
+ * <h2>Threading rules</h2>
+ * <p>There are a few threading rules that must be followed for this class to
+ * work properly:</p>
+ * <ul>
+ * <li>The AsyncTask class must be loaded on the UI thread. This is done
+ * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
+ * <li>The task instance must be created on the UI thread.</li>
+ * <li>{@link #execute} must be invoked on the UI thread.</li>
+ * <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
+ * {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
+ * <li>The task can be executed only once (an exception will be thrown if
+ * a second execution is attempted.)</li>
+ * </ul>
+ *
+ * <h2>Memory observability</h2>
+ * <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
+ * operations are safe without explicit synchronizations.</p>
+ * <ul>
+ * <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
+ * in {@link #doInBackground}.
+ * <li>Set member fields in {@link #doInBackground}, and refer to them in
+ * {@link #onProgressUpdate} and {@link #onPostExecute}.
+ * </ul>
+ *
+ * <h2>Order of execution</h2>
+ * <p>When first introduced, AsyncTasks were executed serially on a single background
+ * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+ * to a pool of threads allowing multiple tasks to operate in parallel. Starting with
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
+ * thread to avoid common application errors caused by parallel execution.</p>
+ * <p>If you truly want parallel execution, you can invoke
+ * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
+ * {@link #THREAD_POOL_EXECUTOR}.</p>
+ */
+public abstract class AsyncTask<Params, Progress, Result> {
+ private static final String LOG_TAG = "AsyncTask";
+
+ private static final int CORE_POOL_SIZE = 5;
+ private static final int MAXIMUM_POOL_SIZE = 128;
+ private static final int KEEP_ALIVE = 1;
+
+ private static final ThreadFactory sThreadFactory = new ThreadFactory() {
+ private final AtomicInteger mCount = new AtomicInteger(1);
+
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
+ }
+ };
+
+ private static final BlockingQueue<Runnable> sPoolWorkQueue =
+ new LinkedBlockingQueue<Runnable>(10);
+
+ /**
+ * An {@link Executor} that can be used to execute tasks in parallel.
+ */
+ public static final Executor THREAD_POOL_EXECUTOR
+ = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
+ TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
+
+ /**
+ * An {@link Executor} that executes tasks one at a time in serial
+ * order. This serialization is global to a particular process.
+ */
+ public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
+
+ private static final int MESSAGE_POST_RESULT = 0x1;
+ private static final int MESSAGE_POST_PROGRESS = 0x2;
+
+ private static final InternalHandler sHandler = new InternalHandler();
+
+ private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
+ private final WorkerRunnable<Params, Result> mWorker;
+ private final FutureTask<Result> mFuture;
+
+ private volatile Status mStatus = Status.PENDING;
+
+ private final AtomicBoolean mCancelled = new AtomicBoolean();
+ private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
+
+ private static class SerialExecutor implements Executor {
+ final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
+ Runnable mActive;
+
+ public synchronized void execute(final Runnable r) {
+ mTasks.offer(new Runnable() {
+ public void run() {
+ try {
+ r.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+
+ protected synchronized void scheduleNext() {
+ if ((mActive = mTasks.poll()) != null) {
+ THREAD_POOL_EXECUTOR.execute(mActive);
+ }
+ }
+ }
+
+ /**
+ * Indicates the current status of the task. Each status will be set only once
+ * during the lifetime of a task.
+ */
+ public enum Status {
+ /**
+ * Indicates that the task has not been executed yet.
+ */
+ PENDING,
+ /**
+ * Indicates that the task is running.
+ */
+ RUNNING,
+ /**
+ * Indicates that {@link AsyncTask#onPostExecute} has finished.
+ */
+ FINISHED,
+ }
+
+ /** @hide Used to force static handler to be created. */
+ public static void init() {
+ sHandler.getLooper();
+ }
+
+ /** @hide */
+ public static void setDefaultExecutor(Executor exec) {
+ sDefaultExecutor = exec;
+ }
+
+ /**
+ * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
+ */
+ public AsyncTask() {
+ mWorker = new WorkerRunnable<Params, Result>() {
+ public Result call() throws Exception {
+ mTaskInvoked.set(true);
+
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ //noinspection unchecked
+ return postResult(doInBackground(mParams));
+ }
+ };
+
+ mFuture = new FutureTask<Result>(mWorker) {
+ @Override
+ protected void done() {
+ try {
+ postResultIfNotInvoked(get());
+ } catch (InterruptedException e) {
+ android.util.Log.w(LOG_TAG, e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException("An error occured while executing doInBackground()",
+ e.getCause());
+ } catch (CancellationException e) {
+ postResultIfNotInvoked(null);
+ }
+ }
+ };
+ }
+
+ private void postResultIfNotInvoked(Result result) {
+ final boolean wasTaskInvoked = mTaskInvoked.get();
+ if (!wasTaskInvoked) {
+ postResult(result);
+ }
+ }
+
+ private Result postResult(Result result) {
+ @SuppressWarnings("unchecked")
+ Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
+ new AsyncTaskResult<Result>(this, result));
+ message.sendToTarget();
+ return result;
+ }
+
+ /**
+ * Returns the current status of this task.
+ *
+ * @return The current status.
+ */
+ public final Status getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Override this method to perform a computation on a background thread. The
+ * specified parameters are the parameters passed to {@link #execute}
+ * by the caller of this task.
+ *
+ * This method can call {@link #publishProgress} to publish updates
+ * on the UI thread.
+ *
+ * @param params The parameters of the task.
+ *
+ * @return A result, defined by the subclass of this task.
+ *
+ * @see #onPreExecute()
+ * @see #onPostExecute
+ * @see #publishProgress
+ */
+ protected abstract Result doInBackground(Params... params);
+
+ /**
+ * Runs on the UI thread before {@link #doInBackground}.
+ *
+ * @see #onPostExecute
+ * @see #doInBackground
+ */
+ protected void onPreExecute() {
+ }
+
+ /**
+ * <p>Runs on the UI thread after {@link #doInBackground}. The
+ * specified result is the value returned by {@link #doInBackground}.</p>
+ *
+ * <p>This method won't be invoked if the task was cancelled.</p>
+ *
+ * @param result The result of the operation computed by {@link #doInBackground}.
+ *
+ * @see #onPreExecute
+ * @see #doInBackground
+ * @see #onCancelled(Object)
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected void onPostExecute(Result result) {
+ }
+
+ /**
+ * Runs on the UI thread after {@link #publishProgress} is invoked.
+ * The specified values are the values passed to {@link #publishProgress}.
+ *
+ * @param values The values indicating progress.
+ *
+ * @see #publishProgress
+ * @see #doInBackground
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected void onProgressUpdate(Progress... values) {
+ }
+
+ /**
+ * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+ * {@link #doInBackground(Object[])} has finished.</p>
+ *
+ * <p>The default implementation simply invokes {@link #onCancelled()} and
+ * ignores the result. If you write your own implementation, do not call
+ * <code>super.onCancelled(result)</code>.</p>
+ *
+ * @param result The result, if any, computed in
+ * {@link #doInBackground(Object[])}, can be null
+ *
+ * @see #cancel(boolean)
+ * @see #isCancelled()
+ */
+ @SuppressWarnings({"UnusedParameters"})
+ protected void onCancelled(Result result) {
+ onCancelled();
+ }
+
+ /**
+ * <p>Applications should preferably override {@link #onCancelled(Object)}.
+ * This method is invoked by the default implementation of
+ * {@link #onCancelled(Object)}.</p>
+ *
+ * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+ * {@link #doInBackground(Object[])} has finished.</p>
+ *
+ * @see #onCancelled(Object)
+ * @see #cancel(boolean)
+ * @see #isCancelled()
+ */
+ protected void onCancelled() {
+ }
+
+ /**
+ * Returns <tt>true</tt> if this task was cancelled before it completed
+ * normally. If you are calling {@link #cancel(boolean)} on the task,
+ * the value returned by this method should be checked periodically from
+ * {@link #doInBackground(Object[])} to end the task as soon as possible.
+ *
+ * @return <tt>true</tt> if task was cancelled before it completed
+ *
+ * @see #cancel(boolean)
+ */
+ public final boolean isCancelled() {
+ return mCancelled.get();
+ }
+
+ /**
+ * <p>Attempts to cancel execution of this task. This attempt will
+ * fail if the task has already completed, already been cancelled,
+ * or could not be cancelled for some other reason. If successful,
+ * and this task has not started when <tt>cancel</tt> is called,
+ * this task should never run. If the task has already started,
+ * then the <tt>mayInterruptIfRunning</tt> parameter determines
+ * whether the thread executing this task should be interrupted in
+ * an attempt to stop the task.</p>
+ *
+ * <p>Calling this method will result in {@link #onCancelled(Object)} being
+ * invoked on the UI thread after {@link #doInBackground(Object[])}
+ * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
+ * is never invoked. After invoking this method, you should check the
+ * value returned by {@link #isCancelled()} periodically from
+ * {@link #doInBackground(Object[])} to finish the task as early as
+ * possible.</p>
+ *
+ * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+ * task should be interrupted; otherwise, in-progress tasks are allowed
+ * to complete.
+ *
+ * @return <tt>false</tt> if the task could not be cancelled,
+ * typically because it has already completed normally;
+ * <tt>true</tt> otherwise
+ *
+ * @see #isCancelled()
+ * @see #onCancelled(Object)
+ */
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ mCancelled.set(true);
+ return mFuture.cancel(mayInterruptIfRunning);
+ }
+
+ /**
+ * Waits if necessary for the computation to complete, and then
+ * retrieves its result.
+ *
+ * @return The computed result.
+ *
+ * @throws CancellationException If the computation was cancelled.
+ * @throws ExecutionException If the computation threw an exception.
+ * @throws InterruptedException If the current thread was interrupted
+ * while waiting.
+ */
+ public final Result get() throws InterruptedException, ExecutionException {
+ return mFuture.get();
+ }
+
+ /**
+ * Waits if necessary for at most the given time for the computation
+ * to complete, and then retrieves its result.
+ *
+ * @param timeout Time to wait before cancelling the operation.
+ * @param unit The time unit for the timeout.
+ *
+ * @return The computed result.
+ *
+ * @throws CancellationException If the computation was cancelled.
+ * @throws ExecutionException If the computation threw an exception.
+ * @throws InterruptedException If the current thread was interrupted
+ * while waiting.
+ * @throws TimeoutException If the wait timed out.
+ */
+ public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
+ ExecutionException, TimeoutException {
+ return mFuture.get(timeout, unit);
+ }
+
+ /**
+ * Executes the task with the specified parameters. The task returns
+ * itself (this) so that the caller can keep a reference to it.
+ *
+ * <p>Note: this function schedules the task on a queue for a single background
+ * thread or pool of threads depending on the platform version. When first
+ * introduced, AsyncTasks were executed serially on a single background thread.
+ * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+ * to a pool of threads allowing multiple tasks to operate in parallel. Starting
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
+ * executed on a single thread to avoid common application errors caused
+ * by parallel execution. If you truly want parallel execution, you can use
+ * the {@link #executeOnExecutor} version of this method
+ * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
+ * on its use.
+ *
+ * <p>This method must be invoked on the UI thread.
+ *
+ * @param params The parameters of the task.
+ *
+ * @return This instance of AsyncTask.
+ *
+ * @throws IllegalStateException If {@link #getStatus()} returns either
+ * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+ *
+ * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+ * @see #execute(Runnable)
+ */
+ public final AsyncTask<Params, Progress, Result> execute(Params... params) {
+ return executeOnExecutor(sDefaultExecutor, params);
+ }
+
+ /**
+ * Executes the task with the specified parameters. The task returns
+ * itself (this) so that the caller can keep a reference to it.
+ *
+ * <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
+ * allow multiple tasks to run in parallel on a pool of threads managed by
+ * AsyncTask, however you can also use your own {@link Executor} for custom
+ * behavior.
+ *
+ * <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
+ * a thread pool is generally <em>not</em> what one wants, because the order
+ * of their operation is not defined. For example, if these tasks are used
+ * to modify any state in common (such as writing a file due to a button click),
+ * there are no guarantees on the order of the modifications.
+ * Without careful work it is possible in rare cases for the newer version
+ * of the data to be over-written by an older one, leading to obscure data
+ * loss and stability issues. Such changes are best
+ * executed in serial; to guarantee such work is serialized regardless of
+ * platform version you can use this function with {@link #SERIAL_EXECUTOR}.
+ *
+ * <p>This method must be invoked on the UI thread.
+ *
+ * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
+ * convenient process-wide thread pool for tasks that are loosely coupled.
+ * @param params The parameters of the task.
+ *
+ * @return This instance of AsyncTask.
+ *
+ * @throws IllegalStateException If {@link #getStatus()} returns either
+ * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+ *
+ * @see #execute(Object[])
+ */
+ public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
+ Params... params) {
+ if (mStatus != Status.PENDING) {
+ switch (mStatus) {
+ case RUNNING:
+ throw new IllegalStateException("Cannot execute task:"
+ + " the task is already running.");
+ case FINISHED:
+ throw new IllegalStateException("Cannot execute task:"
+ + " the task has already been executed "
+ + "(a task can be executed only once)");
+ }
+ }
+
+ mStatus = Status.RUNNING;
+
+ onPreExecute();
+
+ mWorker.mParams = params;
+ exec.execute(mFuture);
+
+ return this;
+ }
+
+ /**
+ * Convenience version of {@link #execute(Object...)} for use with
+ * a simple Runnable object. See {@link #execute(Object[])} for more
+ * information on the order of execution.
+ *
+ * @see #execute(Object[])
+ * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+ */
+ public static void execute(Runnable runnable) {
+ sDefaultExecutor.execute(runnable);
+ }
+
+ /**
+ * This method can be invoked from {@link #doInBackground} to
+ * publish updates on the UI thread while the background computation is
+ * still running. Each call to this method will trigger the execution of
+ * {@link #onProgressUpdate} on the UI thread.
+ *
+ * {@link #onProgressUpdate} will note be called if the task has been
+ * canceled.
+ *
+ * @param values The progress values to update the UI with.
+ *
+ * @see #onProgressUpdate
+ * @see #doInBackground
+ */
+ protected final void publishProgress(Progress... values) {
+ if (!isCancelled()) {
+ sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
+ new AsyncTaskResult<Progress>(this, values)).sendToTarget();
+ }
+ }
+
+ private void finish(Result result) {
+ if (isCancelled()) {
+ onCancelled(result);
+ } else {
+ onPostExecute(result);
+ }
+ mStatus = Status.FINISHED;
+ }
+
+ private static class InternalHandler extends Handler {
+ @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncTaskResult result = (AsyncTaskResult) msg.obj;
+ switch (msg.what) {
+ case MESSAGE_POST_RESULT:
+ // There is only one result
+ result.mTask.finish(result.mData[0]);
+ break;
+ case MESSAGE_POST_PROGRESS:
+ result.mTask.onProgressUpdate(result.mData);
+ break;
+ }
+ }
+ }
+
+ private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
+ Params[] mParams;
+ }
+
+ @SuppressWarnings({"RawUseOfParameterizedType"})
+ private static class AsyncTaskResult<Data> {
+ final AsyncTask mTask;
+ final Data[] mData;
+
+ AsyncTaskResult(AsyncTask task, Data... data) {
+ mTask = task;
+ mData = data;
+ }
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableAsyncTask.java b/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableAsyncTask.java
new file mode 100644
index 00000000..fcb1b744
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableAsyncTask.java
@@ -0,0 +1,79 @@
+package com.artifex.mupdfdemo;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+// Ideally this would be a subclass of AsyncTask, however the cancel() method is final, and cannot
+// be overridden. I felt that having two different, but similar cancel methods was a bad idea.
+public class CancellableAsyncTask<Params, Result>
+{
+ private final AsyncTask<Params, Void, Result> asyncTask;
+ private final CancellableTaskDefinition<Params, Result> ourTask;
+
+ public void onPreExecute()
+ {
+
+ }
+
+ public void onPostExecute(Result result)
+ {
+
+ }
+
+ public CancellableAsyncTask(final CancellableTaskDefinition<Params, Result> task)
+ {
+ if (task == null)
+ throw new IllegalArgumentException();
+
+ this.ourTask = task;
+ asyncTask = new AsyncTask<Params, Void, Result>()
+ {
+ @Override
+ protected Result doInBackground(Params... params)
+ {
+ return task.doInBackground(params);
+ }
+
+ @Override
+ protected void onPreExecute()
+ {
+ CancellableAsyncTask.this.onPreExecute();
+ }
+
+ @Override
+ protected void onPostExecute(Result result)
+ {
+ CancellableAsyncTask.this.onPostExecute(result);
+ task.doCleanup();
+ }
+ };
+ }
+
+ public void cancelAndWait()
+ {
+ this.asyncTask.cancel(true);
+ ourTask.doCancel();
+
+ try
+ {
+ this.asyncTask.get();
+ }
+ catch (InterruptedException e)
+ {
+ }
+ catch (ExecutionException e)
+ {
+ }
+ catch (CancellationException e)
+ {
+ }
+
+ ourTask.doCleanup();
+ }
+
+ public void execute(Params ... params)
+ {
+ asyncTask.execute(params);
+ }
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableTaskDefinition.java b/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableTaskDefinition.java
new file mode 100644
index 00000000..62b04f30
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/CancellableTaskDefinition.java
@@ -0,0 +1,8 @@
+package com.artifex.mupdfdemo;
+
+public interface CancellableTaskDefinition <Params, Result>
+{
+ public Result doInBackground(Params ... params);
+ public void doCancel();
+ public void doCleanup();
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFActivity.java b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFActivity.java
new file mode 100644
index 00000000..f6068bac
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFActivity.java
@@ -0,0 +1,227 @@
+package com.artifex.mupdfdemo;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileObserver;
+import android.os.Handler;
+import android.view.View;
+import android.widget.ListView;
+
+enum Purpose {
+ PickPDF,
+ PickKeyFile
+}
+
+public class ChoosePDFActivity extends ListActivity {
+ static public final String PICK_KEY_FILE = "com.artifex.mupdfdemo.PICK_KEY_FILE";
+ static private File mDirectory;
+ static private Map<String, Integer> mPositions = new HashMap<String, Integer>();
+ private File mParent;
+ private File [] mDirs;
+ private File [] mFiles;
+ private Handler mHandler;
+ private Runnable mUpdateFiles;
+ private ChoosePDFAdapter adapter;
+ private Purpose mPurpose;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPurpose = PICK_KEY_FILE.equals(getIntent().getAction()) ? Purpose.PickKeyFile : Purpose.PickPDF;
+
+
+ String storageState = Environment.getExternalStorageState();
+
+ if (!Environment.MEDIA_MOUNTED.equals(storageState)
+ && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(storageState))
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.no_media_warning);
+ builder.setMessage(R.string.no_media_hint);
+ AlertDialog alert = builder.create();
+ alert.setButton(AlertDialog.BUTTON_POSITIVE,getString(R.string.dismiss),
+ new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
+ return;
+ }
+
+ if (mDirectory == null)
+ mDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+
+ // Create a list adapter...
+ adapter = new ChoosePDFAdapter(getLayoutInflater());
+ setListAdapter(adapter);
+
+ // ...that is updated dynamically when files are scanned
+ mHandler = new Handler();
+ mUpdateFiles = new Runnable() {
+ public void run() {
+ Resources res = getResources();
+ String appName = res.getString(R.string.app_name);
+ String version = res.getString(R.string.version);
+ String title = res.getString(R.string.picker_title_App_Ver_Dir);
+ setTitle(String.format(title, appName, version, mDirectory));
+
+ mParent = mDirectory.getParentFile();
+
+ mDirs = mDirectory.listFiles(new FileFilter() {
+
+ public boolean accept(File file) {
+ return file.isDirectory();
+ }
+ });
+ if (mDirs == null)
+ mDirs = new File[0];
+
+ mFiles = mDirectory.listFiles(new FileFilter() {
+
+ public boolean accept(File file) {
+ if (file.isDirectory())
+ return false;
+ String fname = file.getName().toLowerCase();
+ switch (mPurpose) {
+ case PickPDF:
+ if (fname.endsWith(".pdf"))
+ return true;
+ if (fname.endsWith(".xps"))
+ return true;
+ if (fname.endsWith(".cbz"))
+ return true;
+ if (fname.endsWith(".epub"))
+ return true;
+ if (fname.endsWith(".png"))
+ return true;
+ if (fname.endsWith(".jpe"))
+ return true;
+ if (fname.endsWith(".jpeg"))
+ return true;
+ if (fname.endsWith(".jpg"))
+ return true;
+ if (fname.endsWith(".jfif"))
+ return true;
+ if (fname.endsWith(".jfif-tbnl"))
+ return true;
+ if (fname.endsWith(".tif"))
+ return true;
+ if (fname.endsWith(".tiff"))
+ return true;
+ return false;
+ case PickKeyFile:
+ if (fname.endsWith(".pfx"))
+ return true;
+ return false;
+ default:
+ return false;
+ }
+ }
+ });
+ if (mFiles == null)
+ mFiles = new File[0];
+
+ Arrays.sort(mFiles, new Comparator<File>() {
+ public int compare(File arg0, File arg1) {
+ return arg0.getName().compareToIgnoreCase(arg1.getName());
+ }
+ });
+
+ Arrays.sort(mDirs, new Comparator<File>() {
+ public int compare(File arg0, File arg1) {
+ return arg0.getName().compareToIgnoreCase(arg1.getName());
+ }
+ });
+
+ adapter.clear();
+ if (mParent != null)
+ adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.PARENT, getString(R.string.parent_directory)));
+ for (File f : mDirs)
+ adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.DIR, f.getName()));
+ for (File f : mFiles)
+ adapter.add(new ChoosePDFItem(ChoosePDFItem.Type.DOC, f.getName()));
+
+ lastPosition();
+ }
+ };
+
+ // Start initial file scan...
+ mHandler.post(mUpdateFiles);
+
+ // ...and observe the directory and scan files upon changes.
+ FileObserver observer = new FileObserver(mDirectory.getPath(), FileObserver.CREATE | FileObserver.DELETE) {
+ public void onEvent(int event, String path) {
+ mHandler.post(mUpdateFiles);
+ }
+ };
+ observer.startWatching();
+ }
+
+ private void lastPosition() {
+ String p = mDirectory.getAbsolutePath();
+ if (mPositions.containsKey(p))
+ getListView().setSelection(mPositions.get(p));
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ mPositions.put(mDirectory.getAbsolutePath(), getListView().getFirstVisiblePosition());
+
+ if (position < (mParent == null ? 0 : 1)) {
+ mDirectory = mParent;
+ mHandler.post(mUpdateFiles);
+ return;
+ }
+
+ position -= (mParent == null ? 0 : 1);
+
+ if (position < mDirs.length) {
+ mDirectory = mDirs[position];
+ mHandler.post(mUpdateFiles);
+ return;
+ }
+
+ position -= mDirs.length;
+
+ Uri uri = Uri.fromFile(mFiles[position]);
+ Intent intent = new Intent(this,MuPDFActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ switch (mPurpose) {
+ case PickPDF:
+ // Start an activity to display the PDF file
+ startActivity(intent);
+ break;
+ case PickKeyFile:
+ // Return the uri to the caller
+ setResult(RESULT_OK, intent);
+ finish();
+ break;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mDirectory != null)
+ mPositions.put(mDirectory.getAbsolutePath(), getListView().getFirstVisiblePosition());
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java
new file mode 100644
index 00000000..0b3c6418
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java
@@ -0,0 +1,66 @@
+package com.artifex.mupdfdemo;
+
+import java.util.LinkedList;
+
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ChoosePDFAdapter extends BaseAdapter {
+ private final LinkedList<ChoosePDFItem> mItems;
+ private final LayoutInflater mInflater;
+
+ public ChoosePDFAdapter(LayoutInflater inflater) {
+ mInflater = inflater;
+ mItems = new LinkedList<ChoosePDFItem>();
+ }
+
+ public void clear() {
+ mItems.clear();
+ }
+
+ public void add(ChoosePDFItem item) {
+ mItems.add(item);
+ notifyDataSetChanged();
+ }
+
+ public int getCount() {
+ return mItems.size();
+ }
+
+ public Object getItem(int i) {
+ return null;
+ }
+
+ public long getItemId(int arg0) {
+ return 0;
+ }
+
+ private int iconForType(ChoosePDFItem.Type type) {
+ switch (type) {
+ case PARENT: return R.drawable.ic_arrow_up;
+ case DIR: return R.drawable.ic_dir;
+ case DOC: return R.drawable.ic_doc;
+ default: return 0;
+ }
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = mInflater.inflate(R.layout.picker_entry, null);
+ } else {
+ v = convertView;
+ }
+ ChoosePDFItem item = mItems.get(position);
+ ((TextView)v.findViewById(R.id.name)).setText(item.name);
+ ((ImageView)v.findViewById(R.id.icon)).setImageResource(iconForType(item.type));
+ ((ImageView)v.findViewById(R.id.icon)).setColorFilter(Color.argb(255, 0, 0, 0));
+ return v;
+ }
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFItem.java b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFItem.java
new file mode 100644
index 00000000..de6e1d52
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/ChoosePDFItem.java
@@ -0,0 +1,15 @@
+package com.artifex.mupdfdemo;
+
+public class ChoosePDFItem {
+ enum Type {
+ PARENT, DIR, DOC
+ }
+
+ final public Type type;
+ final public String name;
+
+ public ChoosePDFItem (Type t, String n) {
+ type = t;
+ name = n;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/Deque.java b/platform/android/viewer/src/com/artifex/mupdfdemo/Deque.java
new file mode 100644
index 00000000..4bb176b2
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/Deque.java
@@ -0,0 +1,554 @@
+/*
+ * Written by Doug Lea and Josh Bloch with assistance from members of
+ * JCP JSR-166 Expert Group and released to the public domain, as explained
+ * at http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+package com.artifex.mupdfdemo;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.Stack;
+
+// BEGIN android-note
+// removed link to collections framework docs
+// END android-note
+
+/**
+ * A linear collection that supports element insertion and removal at
+ * both ends. The name <i>deque</i> is short for "double ended queue"
+ * and is usually pronounced "deck". Most <tt>Deque</tt>
+ * implementations place no fixed limits on the number of elements
+ * they may contain, but this interface supports capacity-restricted
+ * deques as well as those with no fixed size limit.
+ *
+ * <p>This interface defines methods to access the elements at both
+ * ends of the deque. Methods are provided to insert, remove, and
+ * examine the element. Each of these methods exists in two forms:
+ * one throws an exception if the operation fails, the other returns a
+ * special value (either <tt>null</tt> or <tt>false</tt>, depending on
+ * the operation). The latter form of the insert operation is
+ * designed specifically for use with capacity-restricted
+ * <tt>Deque</tt> implementations; in most implementations, insert
+ * operations cannot fail.
+ *
+ * <p>The twelve methods described above are summarized in the
+ * following table:
+ *
+ * <p>
+ * <table BORDER CELLPADDING=3 CELLSPACING=1>
+ * <tr>
+ * <td></td>
+ * <td ALIGN=CENTER COLSPAN = 2> <b>First Element (Head)</b></td>
+ * <td ALIGN=CENTER COLSPAN = 2> <b>Last Element (Tail)</b></td>
+ * </tr>
+ * <tr>
+ * <td></td>
+ * <td ALIGN=CENTER><em>Throws exception</em></td>
+ * <td ALIGN=CENTER><em>Special value</em></td>
+ * <td ALIGN=CENTER><em>Throws exception</em></td>
+ * <td ALIGN=CENTER><em>Special value</em></td>
+ * </tr>
+ * <tr>
+ * <td><b>Insert</b></td>
+ * <td>{@link #addFirst addFirst(e)}</td>
+ * <td>{@link #offerFirst offerFirst(e)}</td>
+ * <td>{@link #addLast addLast(e)}</td>
+ * <td>{@link #offerLast offerLast(e)}</td>
+ * </tr>
+ * <tr>
+ * <td><b>Remove</b></td>
+ * <td>{@link #removeFirst removeFirst()}</td>
+ * <td>{@link #pollFirst pollFirst()}</td>
+ * <td>{@link #removeLast removeLast()}</td>
+ * <td>{@link #pollLast pollLast()}</td>
+ * </tr>
+ * <tr>
+ * <td><b>Examine</b></td>
+ * <td>{@link #getFirst getFirst()}</td>
+ * <td>{@link #peekFirst peekFirst()}</td>
+ * <td>{@link #getLast getLast()}</td>
+ * <td>{@link #peekLast peekLast()}</td>
+ * </tr>
+ * </table>
+ *
+ * <p>This interface extends the {@link Queue} interface. When a deque is
+ * used as a queue, FIFO (First-In-First-Out) behavior results. Elements are
+ * added at the end of the deque and removed from the beginning. The methods
+ * inherited from the <tt>Queue</tt> interface are precisely equivalent to
+ * <tt>Deque</tt> methods as indicated in the following table:
+ *
+ * <p>
+ * <table BORDER CELLPADDING=3 CELLSPACING=1>
+ * <tr>
+ * <td ALIGN=CENTER> <b><tt>Queue</tt> Method</b></td>
+ * <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#add add(e)}</td>
+ * <td>{@link #addLast addLast(e)}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#offer offer(e)}</td>
+ * <td>{@link #offerLast offerLast(e)}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#remove remove()}</td>
+ * <td>{@link #removeFirst removeFirst()}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#poll poll()}</td>
+ * <td>{@link #pollFirst pollFirst()}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#element element()}</td>
+ * <td>{@link #getFirst getFirst()}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link java.util.Queue#peek peek()}</td>
+ * <td>{@link #peek peekFirst()}</td>
+ * </tr>
+ * </table>
+ *
+ * <p>Deques can also be used as LIFO (Last-In-First-Out) stacks. This
+ * interface should be used in preference to the legacy {@link Stack} class.
+ * When a deque is used as a stack, elements are pushed and popped from the
+ * beginning of the deque. Stack methods are precisely equivalent to
+ * <tt>Deque</tt> methods as indicated in the table below:
+ *
+ * <p>
+ * <table BORDER CELLPADDING=3 CELLSPACING=1>
+ * <tr>
+ * <td ALIGN=CENTER> <b>Stack Method</b></td>
+ * <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>
+ * </tr>
+ * <tr>
+ * <td>{@link #push push(e)}</td>
+ * <td>{@link #addFirst addFirst(e)}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #pop pop()}</td>
+ * <td>{@link #removeFirst removeFirst()}</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #peek peek()}</td>
+ * <td>{@link #peekFirst peekFirst()}</td>
+ * </tr>
+ * </table>
+ *
+ * <p>Note that the {@link #peek peek} method works equally well when
+ * a deque is used as a queue or a stack; in either case, elements are
+ * drawn from the beginning of the deque.
+ *
+ * <p>This interface provides two methods to remove interior
+ * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and
+ * {@link #removeLastOccurrence removeLastOccurrence}.
+ *
+ * <p>Unlike the {@link List} interface, this interface does not
+ * provide support for indexed access to elements.
+ *
+ * <p>While <tt>Deque</tt> implementations are not strictly required
+ * to prohibit the insertion of null elements, they are strongly
+ * encouraged to do so. Users of any <tt>Deque</tt> implementations
+ * that do allow null elements are strongly encouraged <i>not</i> to
+ * take advantage of the ability to insert nulls. This is so because
+ * <tt>null</tt> is used as a special return value by various methods
+ * to indicated that the deque is empty.
+ *
+ * <p><tt>Deque</tt> implementations generally do not define
+ * element-based versions of the <tt>equals</tt> and <tt>hashCode</tt>
+ * methods, but instead inherit the identity-based versions from class
+ * <tt>Object</tt>.
+ *
+ * @author Doug Lea
+ * @author Josh Bloch
+ * @since 1.6
+ * @param <E> the type of elements held in this collection
+ */
+
+public interface Deque<E> extends Queue<E> {
+ /**
+ * Inserts the specified element at the front of this deque if it is
+ * possible to do so immediately without violating capacity restrictions.
+ * When using a capacity-restricted deque, it is generally preferable to
+ * use method {@link #offerFirst}.
+ *
+ * @param e the element to add
+ * @throws IllegalStateException if the element cannot be added at this
+ * time due to capacity restrictions
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ void addFirst(E e);
+
+ /**
+ * Inserts the specified element at the end of this deque if it is
+ * possible to do so immediately without violating capacity restrictions.
+ * When using a capacity-restricted deque, it is generally preferable to
+ * use method {@link #offerLast}.
+ *
+ * <p>This method is equivalent to {@link #add}.
+ *
+ * @param e the element to add
+ * @throws IllegalStateException if the element cannot be added at this
+ * time due to capacity restrictions
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ void addLast(E e);
+
+ /**
+ * Inserts the specified element at the front of this deque unless it would
+ * violate capacity restrictions. When using a capacity-restricted deque,
+ * this method is generally preferable to the {@link #addFirst} method,
+ * which can fail to insert an element only by throwing an exception.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> if the element was added to this deque, else
+ * <tt>false</tt>
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ boolean offerFirst(E e);
+
+ /**
+ * Inserts the specified element at the end of this deque unless it would
+ * violate capacity restrictions. When using a capacity-restricted deque,
+ * this method is generally preferable to the {@link #addLast} method,
+ * which can fail to insert an element only by throwing an exception.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> if the element was added to this deque, else
+ * <tt>false</tt>
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ boolean offerLast(E e);
+
+ /**
+ * Retrieves and removes the first element of this deque. This method
+ * differs from {@link #pollFirst pollFirst} only in that it throws an
+ * exception if this deque is empty.
+ *
+ * @return the head of this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E removeFirst();
+
+ /**
+ * Retrieves and removes the last element of this deque. This method
+ * differs from {@link #pollLast pollLast} only in that it throws an
+ * exception if this deque is empty.
+ *
+ * @return the tail of this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E removeLast();
+
+ /**
+ * Retrieves and removes the first element of this deque,
+ * or returns <tt>null</tt> if this deque is empty.
+ *
+ * @return the head of this deque, or <tt>null</tt> if this deque is empty
+ */
+ E pollFirst();
+
+ /**
+ * Retrieves and removes the last element of this deque,
+ * or returns <tt>null</tt> if this deque is empty.
+ *
+ * @return the tail of this deque, or <tt>null</tt> if this deque is empty
+ */
+ E pollLast();
+
+ /**
+ * Retrieves, but does not remove, the first element of this deque.
+ *
+ * This method differs from {@link #peekFirst peekFirst} only in that it
+ * throws an exception if this deque is empty.
+ *
+ * @return the head of this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E getFirst();
+
+ /**
+ * Retrieves, but does not remove, the last element of this deque.
+ * This method differs from {@link #peekLast peekLast} only in that it
+ * throws an exception if this deque is empty.
+ *
+ * @return the tail of this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E getLast();
+
+ /**
+ * Retrieves, but does not remove, the first element of this deque,
+ * or returns <tt>null</tt> if this deque is empty.
+ *
+ * @return the head of this deque, or <tt>null</tt> if this deque is empty
+ */
+ E peekFirst();
+
+ /**
+ * Retrieves, but does not remove, the last element of this deque,
+ * or returns <tt>null</tt> if this deque is empty.
+ *
+ * @return the tail of this deque, or <tt>null</tt> if this deque is empty
+ */
+ E peekLast();
+
+ /**
+ * Removes the first occurrence of the specified element from this deque.
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the first element <tt>e</tt> such that
+ * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+ * (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if an element was removed as a result of this call
+ * @throws ClassCastException if the class of the specified element
+ * is incompatible with this deque (optional)
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements (optional)
+ */
+ boolean removeFirstOccurrence(Object o);
+
+ /**
+ * Removes the last occurrence of the specified element from this deque.
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the last element <tt>e</tt> such that
+ * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+ * (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if an element was removed as a result of this call
+ * @throws ClassCastException if the class of the specified element
+ * is incompatible with this deque (optional)
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements (optional)
+ */
+ boolean removeLastOccurrence(Object o);
+
+ // *** Queue methods ***
+
+ /**
+ * Inserts the specified element into the queue represented by this deque
+ * (in other words, at the tail of this deque) if it is possible to do so
+ * immediately without violating capacity restrictions, returning
+ * <tt>true</tt> upon success and throwing an
+ * <tt>IllegalStateException</tt> if no space is currently available.
+ * When using a capacity-restricted deque, it is generally preferable to
+ * use {@link #offer(Object) offer}.
+ *
+ * <p>This method is equivalent to {@link #addLast}.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> (as specified by {@link Collection#add})
+ * @throws IllegalStateException if the element cannot be added at this
+ * time due to capacity restrictions
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ boolean add(E e);
+
+ /**
+ * Inserts the specified element into the queue represented by this deque
+ * (in other words, at the tail of this deque) if it is possible to do so
+ * immediately without violating capacity restrictions, returning
+ * <tt>true</tt> upon success and <tt>false</tt> if no space is currently
+ * available. When using a capacity-restricted deque, this method is
+ * generally preferable to the {@link #add} method, which can fail to
+ * insert an element only by throwing an exception.
+ *
+ * <p>This method is equivalent to {@link #offerLast}.
+ *
+ * @param e the element to add
+ * @return <tt>true</tt> if the element was added to this deque, else
+ * <tt>false</tt>
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ boolean offer(E e);
+
+ /**
+ * Retrieves and removes the head of the queue represented by this deque
+ * (in other words, the first element of this deque).
+ * This method differs from {@link #poll poll} only in that it throws an
+ * exception if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #removeFirst()}.
+ *
+ * @return the head of the queue represented by this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E remove();
+
+ /**
+ * Retrieves and removes the head of the queue represented by this deque
+ * (in other words, the first element of this deque), or returns
+ * <tt>null</tt> if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #pollFirst()}.
+ *
+ * @return the first element of this deque, or <tt>null</tt> if
+ * this deque is empty
+ */
+ E poll();
+
+ /**
+ * Retrieves, but does not remove, the head of the queue represented by
+ * this deque (in other words, the first element of this deque).
+ * This method differs from {@link #peek peek} only in that it throws an
+ * exception if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #getFirst()}.
+ *
+ * @return the head of the queue represented by this deque
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E element();
+
+ /**
+ * Retrieves, but does not remove, the head of the queue represented by
+ * this deque (in other words, the first element of this deque), or
+ * returns <tt>null</tt> if this deque is empty.
+ *
+ * <p>This method is equivalent to {@link #peekFirst()}.
+ *
+ * @return the head of the queue represented by this deque, or
+ * <tt>null</tt> if this deque is empty
+ */
+ E peek();
+
+
+ // *** Stack methods ***
+
+ /**
+ * Pushes an element onto the stack represented by this deque (in other
+ * words, at the head of this deque) if it is possible to do so
+ * immediately without violating capacity restrictions, returning
+ * <tt>true</tt> upon success and throwing an
+ * <tt>IllegalStateException</tt> if no space is currently available.
+ *
+ * <p>This method is equivalent to {@link #addFirst}.
+ *
+ * @param e the element to push
+ * @throws IllegalStateException if the element cannot be added at this
+ * time due to capacity restrictions
+ * @throws ClassCastException if the class of the specified element
+ * prevents it from being added to this deque
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements
+ * @throws IllegalArgumentException if some property of the specified
+ * element prevents it from being added to this deque
+ */
+ void push(E e);
+
+ /**
+ * Pops an element from the stack represented by this deque. In other
+ * words, removes and returns the first element of this deque.
+ *
+ * <p>This method is equivalent to {@link #removeFirst()}.
+ *
+ * @return the element at the front of this deque (which is the top
+ * of the stack represented by this deque)
+ * @throws NoSuchElementException if this deque is empty
+ */
+ E pop();
+
+
+ // *** Collection methods ***
+
+ /**
+ * Removes the first occurrence of the specified element from this deque.
+ * If the deque does not contain the element, it is unchanged.
+ * More formally, removes the first element <tt>e</tt> such that
+ * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+ * (if such an element exists).
+ * Returns <tt>true</tt> if this deque contained the specified element
+ * (or equivalently, if this deque changed as a result of the call).
+ *
+ * <p>This method is equivalent to {@link #removeFirstOccurrence}.
+ *
+ * @param o element to be removed from this deque, if present
+ * @return <tt>true</tt> if an element was removed as a result of this call
+ * @throws ClassCastException if the class of the specified element
+ * is incompatible with this deque (optional)
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements (optional)
+ */
+ boolean remove(Object o);
+
+ /**
+ * Returns <tt>true</tt> if this deque contains the specified element.
+ * More formally, returns <tt>true</tt> if and only if this deque contains
+ * at least one element <tt>e</tt> such that
+ * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
+ *
+ * @param o element whose presence in this deque is to be tested
+ * @return <tt>true</tt> if this deque contains the specified element
+ * @throws ClassCastException if the type of the specified element
+ * is incompatible with this deque (optional)
+ * @throws NullPointerException if the specified element is null and this
+ * deque does not permit null elements (optional)
+ */
+ boolean contains(Object o);
+
+ /**
+ * Returns the number of elements in this deque.
+ *
+ * @return the number of elements in this deque
+ */
+ public int size();
+
+ /**
+ * Returns an iterator over the elements in this deque in proper sequence.
+ * The elements will be returned in order from first (head) to last (tail).
+ *
+ * @return an iterator over the elements in this deque in proper sequence
+ */
+ Iterator<E> iterator();
+
+ /**
+ * Returns an iterator over the elements in this deque in reverse
+ * sequential order. The elements will be returned in order from
+ * last (tail) to first (head).
+ *
+ * @return an iterator over the elements in this deque in reverse
+ * sequence
+ */
+ Iterator<E> descendingIterator();
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/FilePicker.java b/platform/android/viewer/src/com/artifex/mupdfdemo/FilePicker.java
new file mode 100644
index 00000000..d1953531
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/FilePicker.java
@@ -0,0 +1,21 @@
+package com.artifex.mupdfdemo;
+
+import android.net.Uri;
+
+public abstract class FilePicker {
+ public interface FilePickerSupport {
+ void performPickFor(FilePicker picker);
+ }
+
+ private final FilePickerSupport support;
+
+ FilePicker(FilePickerSupport _support) {
+ support = _support;
+ }
+
+ void pick() {
+ support.performPickFor(this);
+ }
+
+ abstract void onPick(Uri uri);
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfo.java b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfo.java
new file mode 100644
index 00000000..5aeaccbe
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfo.java
@@ -0,0 +1,14 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.RectF;
+
+public class LinkInfo {
+ final public RectF rect;
+
+ public LinkInfo(float l, float t, float r, float b) {
+ rect = new RectF(l, t, r, b);
+ }
+
+ public void acceptVisitor(LinkInfoVisitor visitor) {
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoExternal.java b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoExternal.java
new file mode 100644
index 00000000..574b6264
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoExternal.java
@@ -0,0 +1,14 @@
+package com.artifex.mupdfdemo;
+
+public class LinkInfoExternal extends LinkInfo {
+ final public String url;
+
+ public LinkInfoExternal(float l, float t, float r, float b, String u) {
+ super(l, t, r, b);
+ url = u;
+ }
+
+ public void acceptVisitor(LinkInfoVisitor visitor) {
+ visitor.visitExternal(this);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoInternal.java b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoInternal.java
new file mode 100644
index 00000000..761bf87a
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoInternal.java
@@ -0,0 +1,14 @@
+package com.artifex.mupdfdemo;
+
+public class LinkInfoInternal extends LinkInfo {
+ final public int pageNumber;
+
+ public LinkInfoInternal(float l, float t, float r, float b, int p) {
+ super(l, t, r, b);
+ pageNumber = p;
+ }
+
+ public void acceptVisitor(LinkInfoVisitor visitor) {
+ visitor.visitInternal(this);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoRemote.java b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoRemote.java
new file mode 100644
index 00000000..731e6408
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoRemote.java
@@ -0,0 +1,18 @@
+package com.artifex.mupdfdemo;
+
+public class LinkInfoRemote extends LinkInfo {
+ final public String fileSpec;
+ final public int pageNumber;
+ final public boolean newWindow;
+
+ public LinkInfoRemote(float l, float t, float r, float b, String f, int p, boolean n) {
+ super(l, t, r, b);
+ fileSpec = f;
+ pageNumber = p;
+ newWindow = n;
+ }
+
+ public void acceptVisitor(LinkInfoVisitor visitor) {
+ visitor.visitRemote(this);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoVisitor.java b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoVisitor.java
new file mode 100644
index 00000000..ecd093e4
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/LinkInfoVisitor.java
@@ -0,0 +1,7 @@
+package com.artifex.mupdfdemo;
+
+abstract public class LinkInfoVisitor {
+ public abstract void visitInternal(LinkInfoInternal li);
+ public abstract void visitExternal(LinkInfoExternal li);
+ public abstract void visitRemote(LinkInfoRemote li);
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFActivity.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFActivity.java
new file mode 100644
index 00000000..91b1e8fe
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFActivity.java
@@ -0,0 +1,1417 @@
+package com.artifex.mupdfdemo;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.method.PasswordTransformationMethod;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.concurrent.Executor;
+
+class ThreadPerTaskExecutor implements Executor {
+ public void execute(Runnable r) {
+ new Thread(r).start();
+ }
+}
+
+public class MuPDFActivity extends Activity implements FilePicker.FilePickerSupport
+{
+ /* The core rendering instance */
+ enum TopBarMode {Main, Search, Annot, Delete, More, Accept};
+ enum AcceptMode {Highlight, Underline, StrikeOut, Ink, CopyText};
+
+ private final int OUTLINE_REQUEST=0;
+ private final int PRINT_REQUEST=1;
+ private final int FILEPICK_REQUEST=2;
+ private final int PROOF_REQUEST=3;
+ private MuPDFCore core;
+ private String mFileName;
+ private MuPDFReaderView mDocView;
+ private View mButtonsView;
+ private boolean mButtonsVisible;
+ private EditText mPasswordView;
+ private TextView mFilenameView;
+ private SeekBar mPageSlider;
+ private int mPageSliderRes;
+ private TextView mPageNumberView;
+ private TextView mInfoView;
+ private ImageButton mSearchButton;
+ private ImageButton mReflowButton;
+ private ImageButton mOutlineButton;
+ private ImageButton mMoreButton;
+ private TextView mAnnotTypeText;
+ private ImageButton mAnnotButton;
+ private ViewAnimator mTopBarSwitcher;
+ private ImageButton mLinkButton;
+ private TopBarMode mTopBarMode = TopBarMode.Main;
+ private AcceptMode mAcceptMode;
+ private ImageButton mSearchBack;
+ private ImageButton mSearchFwd;
+ private EditText mSearchText;
+ private SearchTask mSearchTask;
+ private ImageButton mProofButton;
+ private ImageButton mSepsButton;
+ private AlertDialog.Builder mAlertBuilder;
+ private boolean mLinkHighlight = false;
+ private final Handler mHandler = new Handler();
+ private boolean mAlertsActive= false;
+ private boolean mReflow = false;
+ private AsyncTask<Void,Void,MuPDFAlert> mAlertTask;
+ private AlertDialog mAlertDialog;
+ private FilePicker mFilePicker;
+ private String mProofFile;
+ private boolean mSepEnabled[][];
+
+ static private AlertDialog.Builder gAlertBuilder;
+ static public AlertDialog.Builder getAlertBuilder() {return gAlertBuilder;}
+
+ public void createAlertWaiter() {
+ mAlertsActive = true;
+ // All mupdf library calls are performed on asynchronous tasks to avoid stalling
+ // the UI. Some calls can lead to javascript-invoked requests to display an
+ // alert dialog and collect a reply from the user. The task has to be blocked
+ // until the user's reply is received. This method creates an asynchronous task,
+ // the purpose of which is to wait of these requests and produce the dialog
+ // in response, while leaving the core blocked. When the dialog receives the
+ // user's response, it is sent to the core via replyToAlert, unblocking it.
+ // Another alert-waiting task is then created to pick up the next alert.
+ if (mAlertTask != null) {
+ mAlertTask.cancel(true);
+ mAlertTask = null;
+ }
+ if (mAlertDialog != null) {
+ mAlertDialog.cancel();
+ mAlertDialog = null;
+ }
+ mAlertTask = new AsyncTask<Void,Void,MuPDFAlert>() {
+
+ @Override
+ protected MuPDFAlert doInBackground(Void... arg0) {
+ if (!mAlertsActive)
+ return null;
+
+ return core.waitForAlert();
+ }
+
+ @Override
+ protected void onPostExecute(final MuPDFAlert result) {
+ // core.waitForAlert may return null when shutting down
+ if (result == null)
+ return;
+ final MuPDFAlert.ButtonPressed pressed[] = new MuPDFAlert.ButtonPressed[3];
+ for(int i = 0; i < 3; i++)
+ pressed[i] = MuPDFAlert.ButtonPressed.None;
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mAlertDialog = null;
+ if (mAlertsActive) {
+ int index = 0;
+ switch (which) {
+ case AlertDialog.BUTTON1: index=0; break;
+ case AlertDialog.BUTTON2: index=1; break;
+ case AlertDialog.BUTTON3: index=2; break;
+ }
+ result.buttonPressed = pressed[index];
+ // Send the user's response to the core, so that it can
+ // continue processing.
+ core.replyToAlert(result);
+ // Create another alert-waiter to pick up the next alert.
+ createAlertWaiter();
+ }
+ }
+ };
+ mAlertDialog = mAlertBuilder.create();
+ mAlertDialog.setTitle(result.title);
+ mAlertDialog.setMessage(result.message);
+ switch (result.iconType)
+ {
+ case Error:
+ break;
+ case Warning:
+ break;
+ case Question:
+ break;
+ case Status:
+ break;
+ }
+ switch (result.buttonGroupType)
+ {
+ case OkCancel:
+ mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.cancel), listener);
+ pressed[1] = MuPDFAlert.ButtonPressed.Cancel;
+ case Ok:
+ mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.okay), listener);
+ pressed[0] = MuPDFAlert.ButtonPressed.Ok;
+ break;
+ case YesNoCancel:
+ mAlertDialog.setButton(AlertDialog.BUTTON3, getString(R.string.cancel), listener);
+ pressed[2] = MuPDFAlert.ButtonPressed.Cancel;
+ case YesNo:
+ mAlertDialog.setButton(AlertDialog.BUTTON1, getString(R.string.yes), listener);
+ pressed[0] = MuPDFAlert.ButtonPressed.Yes;
+ mAlertDialog.setButton(AlertDialog.BUTTON2, getString(R.string.no), listener);
+ pressed[1] = MuPDFAlert.ButtonPressed.No;
+ break;
+ }
+ mAlertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ mAlertDialog = null;
+ if (mAlertsActive) {
+ result.buttonPressed = MuPDFAlert.ButtonPressed.None;
+ core.replyToAlert(result);
+ createAlertWaiter();
+ }
+ }
+ });
+
+ mAlertDialog.show();
+ }
+ };
+
+ mAlertTask.executeOnExecutor(new ThreadPerTaskExecutor());
+ }
+
+ public void destroyAlertWaiter() {
+ mAlertsActive = false;
+ if (mAlertDialog != null) {
+ mAlertDialog.cancel();
+ mAlertDialog = null;
+ }
+ if (mAlertTask != null) {
+ mAlertTask.cancel(true);
+ mAlertTask = null;
+ }
+ }
+
+ private MuPDFCore openFile(String path)
+ {
+ int lastSlashPos = path.lastIndexOf('/');
+ mFileName = new String(lastSlashPos == -1
+ ? path
+ : path.substring(lastSlashPos+1));
+ System.out.println("Trying to open " + path);
+ try
+ {
+ core = new MuPDFCore(this, path);
+ // New file: drop the old outline data
+ OutlineActivityData.set(null);
+ }
+ catch (Exception e)
+ {
+ System.out.println(e);
+ return null;
+ }
+ catch (java.lang.OutOfMemoryError e)
+ {
+ // out of memory is not an Exception, so we catch it separately.
+ System.out.println(e);
+ return null;
+ }
+ return core;
+ }
+
+ private MuPDFCore openBuffer(byte buffer[], String magic)
+ {
+ System.out.println("Trying to open byte buffer");
+ try
+ {
+ core = new MuPDFCore(this, buffer, magic);
+ // New file: drop the old outline data
+ OutlineActivityData.set(null);
+ }
+ catch (Exception e)
+ {
+ System.out.println(e);
+ return null;
+ }
+ return core;
+ }
+
+ // determine whether the current activity is a proofing activity.
+ public boolean isProofing()
+ {
+ String format = core.fileFormat();
+ return (format.equals("GPROOF"));
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(final Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ mAlertBuilder = new AlertDialog.Builder(this);
+ gAlertBuilder = mAlertBuilder; // keep a static copy of this that other classes can use
+
+ if (core == null) {
+ core = (MuPDFCore)getLastNonConfigurationInstance();
+
+ if (savedInstanceState != null && savedInstanceState.containsKey("FileName")) {
+ mFileName = savedInstanceState.getString("FileName");
+ }
+ }
+ if (core == null) {
+ Intent intent = getIntent();
+ byte buffer[] = null;
+
+ if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ System.out.println("URI to open is: " + uri);
+ if (uri.toString().startsWith("content://")) {
+ String reason = null;
+ try {
+ InputStream is = getContentResolver().openInputStream(uri);
+ int len = is.available();
+ buffer = new byte[len];
+ is.read(buffer, 0, len);
+ is.close();
+ }
+ catch (java.lang.OutOfMemoryError e) {
+ System.out.println("Out of memory during buffer reading");
+ reason = e.toString();
+ }
+ catch (Exception e) {
+ System.out.println("Exception reading from stream: " + e);
+
+ // Handle view requests from the Transformer Prime's file manager
+ // Hopefully other file managers will use this same scheme, if not
+ // using explicit paths.
+ // I'm hoping that this case below is no longer needed...but it's
+ // hard to test as the file manager seems to have changed in 4.x.
+ try {
+ Cursor cursor = getContentResolver().query(uri, new String[]{"_data"}, null, null, null);
+ if (cursor.moveToFirst()) {
+ String str = cursor.getString(0);
+ if (str == null) {
+ reason = "Couldn't parse data in intent";
+ }
+ else {
+ uri = Uri.parse(str);
+ }
+ }
+ }
+ catch (Exception e2) {
+ System.out.println("Exception in Transformer Prime file manager code: " + e2);
+ reason = e2.toString();
+ }
+ }
+ if (reason != null) {
+ buffer = null;
+ Resources res = getResources();
+ AlertDialog alert = mAlertBuilder.create();
+ setTitle(String.format(res.getString(R.string.cannot_open_document_Reason), reason));
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
+ return;
+ }
+ }
+ if (buffer != null) {
+ core = openBuffer(buffer, intent.getType());
+ } else {
+ String path = Uri.decode(uri.getEncodedPath());
+ if (path == null) {
+ path = uri.toString();
+ }
+ core = openFile(path);
+ }
+ SearchTaskResult.set(null);
+ }
+ if (core != null && core.needsPassword()) {
+ requestPassword(savedInstanceState);
+ return;
+ }
+ if (core != null && core.countPages() == 0)
+ {
+ core = null;
+ }
+ }
+ if (core == null)
+ {
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setTitle(R.string.cannot_open_document);
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.setOnCancelListener(new OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ });
+ alert.show();
+ return;
+ }
+
+ createUI(savedInstanceState);
+
+ // hide the proof button if this file can't be proofed
+ if (!core.canProof()) {
+ mProofButton.setVisibility(View.INVISIBLE);
+ }
+
+ if (isProofing()) {
+
+ // start the activity with a new array
+ mSepEnabled = null;
+
+ // show the separations button
+ mSepsButton.setVisibility(View.VISIBLE);
+
+ // hide some other buttons
+ mLinkButton.setVisibility(View.INVISIBLE);
+ mReflowButton.setVisibility(View.INVISIBLE);
+ mOutlineButton.setVisibility(View.INVISIBLE);
+ mSearchButton.setVisibility(View.INVISIBLE);
+ mMoreButton.setVisibility(View.INVISIBLE);
+ }
+ else {
+ // hide the separations button
+ mSepsButton.setVisibility(View.INVISIBLE);
+ }
+
+ }
+
+ public void requestPassword(final Bundle savedInstanceState) {
+ mPasswordView = new EditText(this);
+ mPasswordView.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+ mPasswordView.setTransformationMethod(new PasswordTransformationMethod());
+
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setTitle(R.string.enter_password);
+ alert.setView(mPasswordView);
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.okay),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (core.authenticatePassword(mPasswordView.getText().toString())) {
+ createUI(savedInstanceState);
+ } else {
+ requestPassword(savedInstanceState);
+ }
+ }
+ });
+ alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel),
+ new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
+ }
+
+ public void createUI(Bundle savedInstanceState) {
+ if (core == null)
+ return;
+
+ // Now create the UI.
+ // First create the document view
+ mDocView = new MuPDFReaderView(this) {
+ @Override
+ protected void onMoveToChild(int i) {
+ if (core == null)
+ return;
+
+ mPageNumberView.setText(String.format("%d / %d", i + 1,
+ core.countPages()));
+ mPageSlider.setMax((core.countPages() - 1) * mPageSliderRes);
+ mPageSlider.setProgress(i * mPageSliderRes);
+ super.onMoveToChild(i);
+ }
+
+ @Override
+ protected void onTapMainDocArea() {
+ if (!mButtonsVisible) {
+ showButtons();
+ } else {
+ if (mTopBarMode == TopBarMode.Main)
+ hideButtons();
+ }
+ }
+
+ @Override
+ protected void onDocMotion() {
+ hideButtons();
+ }
+
+ @Override
+ protected void onHit(Hit item) {
+ switch (mTopBarMode) {
+ case Annot:
+ if (item == Hit.Annotation) {
+ showButtons();
+ mTopBarMode = TopBarMode.Delete;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+ break;
+ case Delete:
+ mTopBarMode = TopBarMode.Annot;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ // fall through
+ default:
+ // Not in annotation editing mode, but the pageview will
+ // still select and highlight hit annotations, so
+ // deselect just in case.
+ MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView();
+ if (pageView != null)
+ pageView.deselectAnnotation();
+ break;
+ }
+ }
+ };
+ mDocView.setAdapter(new MuPDFPageAdapter(this, this, core));
+
+ mSearchTask = new SearchTask(this, core) {
+ @Override
+ protected void onTextFound(SearchTaskResult result) {
+ SearchTaskResult.set(result);
+ // Ask the ReaderView to move to the resulting page
+ mDocView.setDisplayedViewIndex(result.pageNumber);
+ // Make the ReaderView act on the change to SearchTaskResult
+ // via overridden onChildSetup method.
+ mDocView.resetupChildren();
+ }
+ };
+
+ // Make the buttons overlay, and store all its
+ // controls in variables
+ makeButtonsView();
+
+ // Set up the page slider
+ int smax = Math.max(core.countPages()-1,1);
+ mPageSliderRes = ((10 + smax - 1)/smax) * 2;
+
+ // Set the file-name text
+ mFilenameView.setText(mFileName);
+
+ // Activate the seekbar
+ mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mDocView.setDisplayedViewIndex((seekBar.getProgress()+mPageSliderRes/2)/mPageSliderRes);
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser) {
+ updatePageNumView((progress+mPageSliderRes/2)/mPageSliderRes);
+ }
+ });
+
+ // Activate the search-preparing button
+ mSearchButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ searchModeOn();
+ }
+ });
+
+ // Activate the reflow button
+ mReflowButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ toggleReflow();
+ }
+ });
+
+ if (core.fileFormat().startsWith("PDF") && core.isUnencryptedPDF() && !core.wasOpenedFromBuffer())
+ {
+ mAnnotButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ mTopBarMode = TopBarMode.Annot;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+ });
+ }
+ else
+ {
+ mAnnotButton.setVisibility(View.GONE);
+ }
+
+ // Search invoking buttons are disabled while there is no text specified
+ mSearchBack.setEnabled(false);
+ mSearchFwd.setEnabled(false);
+ mSearchBack.setColorFilter(Color.argb(255, 128, 128, 128));
+ mSearchFwd.setColorFilter(Color.argb(255, 128, 128, 128));
+
+ // React to interaction with the text widget
+ mSearchText.addTextChangedListener(new TextWatcher() {
+
+ public void afterTextChanged(Editable s) {
+ boolean haveText = s.toString().length() > 0;
+ setButtonEnabled(mSearchBack, haveText);
+ setButtonEnabled(mSearchFwd, haveText);
+
+ // Remove any previous search results
+ if (SearchTaskResult.get() != null && !mSearchText.getText().toString().equals(SearchTaskResult.get().txt)) {
+ SearchTaskResult.set(null);
+ mDocView.resetupChildren();
+ }
+ }
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {}
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {}
+ });
+
+ //React to Done button on keyboard
+ mSearchText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE)
+ search(1);
+ return false;
+ }
+ });
+
+ mSearchText.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER)
+ search(1);
+ return false;
+ }
+ });
+
+ // Activate search invoking buttons
+ mSearchBack.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ search(-1);
+ }
+ });
+ mSearchFwd.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ search(1);
+ }
+ });
+
+ mLinkButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ setLinkHighlight(!mLinkHighlight);
+ }
+ });
+
+ if (core.hasOutline()) {
+ mOutlineButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ OutlineItem outline[] = core.getOutline();
+ if (outline != null) {
+ OutlineActivityData.get().items = outline;
+ Intent intent = new Intent(MuPDFActivity.this, OutlineActivity.class);
+ startActivityForResult(intent, OUTLINE_REQUEST);
+ }
+ }
+ });
+ } else {
+ mOutlineButton.setVisibility(View.GONE);
+ }
+
+ // Reenstate last state if it was recorded
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ mDocView.setDisplayedViewIndex(prefs.getInt("page"+mFileName, 0));
+
+ if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false))
+ showButtons();
+
+ if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false))
+ searchModeOn();
+
+ if(savedInstanceState != null && savedInstanceState.getBoolean("ReflowMode", false))
+ reflowModeSet(true);
+
+ // Stick the document view and the buttons overlay into a parent view
+ RelativeLayout layout = new RelativeLayout(this);
+ layout.addView(mDocView);
+ layout.addView(mButtonsView);
+ setContentView(layout);
+
+ if (isProofing()) {
+ // go to the current page
+ int currentPage = getIntent().getIntExtra("startingPage", 0);
+ mDocView.setDisplayedViewIndex(currentPage);
+ }
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case OUTLINE_REQUEST:
+ if (resultCode >= 0)
+ mDocView.setDisplayedViewIndex(resultCode);
+ break;
+ case PRINT_REQUEST:
+ if (resultCode == RESULT_CANCELED)
+ showInfo(getString(R.string.print_failed));
+ break;
+ case FILEPICK_REQUEST:
+ if (mFilePicker != null && resultCode == RESULT_OK)
+ mFilePicker.onPick(data.getData());
+ case PROOF_REQUEST:
+ // we're returning from a proofing activity
+
+ if (mProofFile != null)
+ {
+ core.endProof(mProofFile);
+ mProofFile = null;
+ }
+
+ // return the top bar to default
+ mTopBarMode = TopBarMode.Main;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public Object onRetainNonConfigurationInstance()
+ {
+ MuPDFCore mycore = core;
+ core = null;
+ return mycore;
+ }
+
+ private void reflowModeSet(boolean reflow)
+ {
+ mReflow = reflow;
+ mDocView.setAdapter(mReflow ? new MuPDFReflowAdapter(this, core) : new MuPDFPageAdapter(this, this, core));
+ mReflowButton.setColorFilter(mReflow ? Color.argb(0xFF, 172, 114, 37) : Color.argb(0xFF, 255, 255, 255));
+ setButtonEnabled(mAnnotButton, !reflow);
+ setButtonEnabled(mSearchButton, !reflow);
+ if (reflow) setLinkHighlight(false);
+ setButtonEnabled(mLinkButton, !reflow);
+ setButtonEnabled(mMoreButton, !reflow);
+ mDocView.refresh(mReflow);
+ }
+
+ private void toggleReflow() {
+ reflowModeSet(!mReflow);
+ showInfo(mReflow ? getString(R.string.entering_reflow_mode) : getString(R.string.leaving_reflow_mode));
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (mFileName != null && mDocView != null) {
+ outState.putString("FileName", mFileName);
+
+ // Store current page in the prefs against the file name,
+ // so that we can pick it up each time the file is loaded
+ // Other info is needed only for screen-orientation change,
+ // so it can go in the bundle
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex());
+ edit.commit();
+ }
+
+ if (!mButtonsVisible)
+ outState.putBoolean("ButtonsHidden", true);
+
+ if (mTopBarMode == TopBarMode.Search)
+ outState.putBoolean("SearchMode", true);
+
+ if (mReflow)
+ outState.putBoolean("ReflowMode", true);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (mSearchTask != null)
+ mSearchTask.stop();
+
+ if (mFileName != null && mDocView != null) {
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex());
+ edit.commit();
+ }
+ }
+
+ public void onDestroy()
+ {
+ if (mDocView != null) {
+ mDocView.applyToChildren(new ReaderView.ViewMapper() {
+ void applyToView(View view) {
+ ((MuPDFView)view).releaseBitmaps();
+ }
+ });
+ }
+ if (core != null)
+ core.onDestroy();
+ if (mAlertTask != null) {
+ mAlertTask.cancel(true);
+ mAlertTask = null;
+ }
+ core = null;
+ super.onDestroy();
+ }
+
+ private void setButtonEnabled(ImageButton button, boolean enabled) {
+ button.setEnabled(enabled);
+ button.setColorFilter(enabled ? Color.argb(255, 255, 255, 255) : Color.argb(255, 128, 128, 128));
+ }
+
+ private void setLinkHighlight(boolean highlight) {
+ mLinkHighlight = highlight;
+ // LINK_COLOR tint
+ mLinkButton.setColorFilter(highlight ? Color.argb(0xFF, 172, 114, 37) : Color.argb(0xFF, 255, 255, 255));
+ // Inform pages of the change.
+ mDocView.setLinksEnabled(highlight);
+ }
+
+ private void showButtons() {
+ if (core == null)
+ return;
+ if (!mButtonsVisible) {
+ mButtonsVisible = true;
+ // Update page number text and slider
+ int index = mDocView.getDisplayedViewIndex();
+ updatePageNumView(index);
+ mPageSlider.setMax((core.countPages()-1)*mPageSliderRes);
+ mPageSlider.setProgress(index * mPageSliderRes);
+ if (mTopBarMode == TopBarMode.Search) {
+ mSearchText.requestFocus();
+ showKeyboard();
+ }
+
+ Animation anim = new TranslateAnimation(0, 0, -mTopBarSwitcher.getHeight(), 0);
+ anim.setDuration(200);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ mTopBarSwitcher.setVisibility(View.VISIBLE);
+ }
+ public void onAnimationRepeat(Animation animation) {}
+ public void onAnimationEnd(Animation animation) {}
+ });
+ mTopBarSwitcher.startAnimation(anim);
+
+ anim = new TranslateAnimation(0, 0, mPageSlider.getHeight(), 0);
+ anim.setDuration(200);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ mPageSlider.setVisibility(View.VISIBLE);
+ }
+ public void onAnimationRepeat(Animation animation) {}
+ public void onAnimationEnd(Animation animation) {
+ mPageNumberView.setVisibility(View.VISIBLE);
+ }
+ });
+ mPageSlider.startAnimation(anim);
+ }
+ }
+
+ private void hideButtons() {
+ if (mButtonsVisible) {
+ mButtonsVisible = false;
+ hideKeyboard();
+
+ Animation anim = new TranslateAnimation(0, 0, 0, -mTopBarSwitcher.getHeight());
+ anim.setDuration(200);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ public void onAnimationStart(Animation animation) {}
+ public void onAnimationRepeat(Animation animation) {}
+ public void onAnimationEnd(Animation animation) {
+ mTopBarSwitcher.setVisibility(View.INVISIBLE);
+ }
+ });
+ mTopBarSwitcher.startAnimation(anim);
+
+ anim = new TranslateAnimation(0, 0, 0, mPageSlider.getHeight());
+ anim.setDuration(200);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ mPageNumberView.setVisibility(View.INVISIBLE);
+ }
+ public void onAnimationRepeat(Animation animation) {}
+ public void onAnimationEnd(Animation animation) {
+ mPageSlider.setVisibility(View.INVISIBLE);
+ }
+ });
+ mPageSlider.startAnimation(anim);
+ }
+ }
+
+ private void searchModeOn() {
+ if (mTopBarMode != TopBarMode.Search) {
+ mTopBarMode = TopBarMode.Search;
+ //Focus on EditTextWidget
+ mSearchText.requestFocus();
+ showKeyboard();
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+ }
+
+ private void searchModeOff() {
+ if (mTopBarMode == TopBarMode.Search) {
+ mTopBarMode = TopBarMode.Main;
+ hideKeyboard();
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ SearchTaskResult.set(null);
+ // Make the ReaderView act on the change to mSearchTaskResult
+ // via overridden onChildSetup method.
+ mDocView.resetupChildren();
+ }
+ }
+
+ private void updatePageNumView(int index) {
+ if (core == null)
+ return;
+ mPageNumberView.setText(String.format("%d / %d", index + 1, core.countPages()));
+ }
+
+ private void printDoc() {
+ if (!core.fileFormat().startsWith("PDF")) {
+ showInfo(getString(R.string.format_currently_not_supported));
+ return;
+ }
+
+ Intent myIntent = getIntent();
+ Uri docUri = myIntent != null ? myIntent.getData() : null;
+
+ if (docUri == null) {
+ showInfo(getString(R.string.print_failed));
+ }
+
+ if (docUri.getScheme() == null)
+ docUri = Uri.parse("file://"+docUri.toString());
+
+ Intent printIntent = new Intent(this, PrintDialogActivity.class);
+ printIntent.setDataAndType(docUri, "aplication/pdf");
+ printIntent.putExtra("title", mFileName);
+ startActivityForResult(printIntent, PRINT_REQUEST);
+ }
+
+ private void showInfo(String message) {
+ mInfoView.setText(message);
+
+ int currentApiVersion = android.os.Build.VERSION.SDK_INT;
+ if (currentApiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ SafeAnimatorInflater safe = new SafeAnimatorInflater((Activity)this, R.animator.info, (View)mInfoView);
+ } else {
+ mInfoView.setVisibility(View.VISIBLE);
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ mInfoView.setVisibility(View.INVISIBLE);
+ }
+ }, 500);
+ }
+ }
+
+ private void makeButtonsView() {
+ mButtonsView = getLayoutInflater().inflate(R.layout.buttons,null);
+ mFilenameView = (TextView)mButtonsView.findViewById(R.id.docNameText);
+ mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider);
+ mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber);
+ mInfoView = (TextView)mButtonsView.findViewById(R.id.info);
+ mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton);
+ mReflowButton = (ImageButton)mButtonsView.findViewById(R.id.reflowButton);
+ mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton);
+ mAnnotButton = (ImageButton)mButtonsView.findViewById(R.id.editAnnotButton);
+ mAnnotTypeText = (TextView)mButtonsView.findViewById(R.id.annotType);
+ mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher);
+ mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack);
+ mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward);
+ mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText);
+ mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton);
+ mMoreButton = (ImageButton)mButtonsView.findViewById(R.id.moreButton);
+ mProofButton = (ImageButton)mButtonsView.findViewById(R.id.proofButton);
+ mSepsButton = (ImageButton)mButtonsView.findViewById(R.id.sepsButton);
+ mTopBarSwitcher.setVisibility(View.INVISIBLE);
+ mPageNumberView.setVisibility(View.INVISIBLE);
+ mInfoView.setVisibility(View.INVISIBLE);
+
+ mPageSlider.setVisibility(View.INVISIBLE);
+ if (!core.gprfSupported()) {
+ mProofButton.setVisibility(View.INVISIBLE);
+ }
+ mSepsButton.setVisibility(View.INVISIBLE);
+ }
+
+ public void OnMoreButtonClick(View v) {
+ mTopBarMode = TopBarMode.More;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnCancelMoreButtonClick(View v) {
+ mTopBarMode = TopBarMode.Main;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnPrintButtonClick(View v) {
+ printDoc();
+ }
+
+ // start a proof activity with the given resolution.
+ public void proofWithResolution (int resolution)
+ {
+ mProofFile = core.startProof(resolution);
+ Uri uri = Uri.parse("file://"+mProofFile);
+ Intent intent = new Intent(this, MuPDFActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ // add the current page so it can be found when the activity is running
+ intent.putExtra("startingPage", mDocView.getDisplayedViewIndex());
+ startActivityForResult(intent, PROOF_REQUEST);
+ }
+
+ public void OnProofButtonClick(final View v)
+ {
+ // set up the menu or resolutions.
+ final PopupMenu popup = new PopupMenu(this, v);
+ popup.getMenu().add(0, 1, 0, "Select a resolution:");
+ popup.getMenu().add(0, 72, 0, "72");
+ popup.getMenu().add(0, 96, 0, "96");
+ popup.getMenu().add(0, 150, 0, "150");
+ popup.getMenu().add(0, 300, 0, "300");
+ popup.getMenu().add(0, 600, 0, "600");
+ popup.getMenu().add(0, 1200, 0, "1200");
+ popup.getMenu().add(0, 2400, 0, "2400");
+
+ // prevent the first item from being dismissed.
+ // is there not a better way to do this? It requires minimum API 14
+ MenuItem item = popup.getMenu().getItem(0);
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
+ item.setActionView(new View(v.getContext()));
+ item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ return false;
+ }
+ });
+
+ popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ int id = item.getItemId();
+ if (id != 1) {
+ // it's a resolution. The id is also the resolution value
+ proofWithResolution(id);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ popup.show();
+ }
+
+ public void OnSepsButtonClick(final View v)
+ {
+ if (isProofing()) {
+
+ // get the current page
+ final int currentPage = mDocView.getDisplayedViewIndex();
+
+ // buid a popup menu based on the given separations
+ final PopupMenu menu = new PopupMenu(this, v);
+
+ // This makes the popup menu display icons, which by default it does not do.
+ // I worry that this relies on the internals of PopupMenu, which could change.
+ try {
+ Field[] fields = menu.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ if ("mPopup".equals(field.getName())) {
+ field.setAccessible(true);
+ Object menuPopupHelper = field.get(menu);
+ Class<?> classPopupHelper = Class.forName(menuPopupHelper
+ .getClass().getName());
+ Method setForceIcons = classPopupHelper.getMethod(
+ "setForceShowIcon", boolean.class);
+ setForceIcons.invoke(menuPopupHelper, true);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ // get the maximum number of seps on any page.
+ // We use this to dimension an array further down
+ int maxSeps = 0;
+ int numPages = core.countPages();
+ for (int page=0; page<numPages; page++) {
+ int numSeps = core.getNumSepsOnPage(page);
+ if (numSeps>maxSeps)
+ maxSeps = numSeps;
+ }
+
+ // if this is the first time, create the "enabled" array
+ if (mSepEnabled==null) {
+ mSepEnabled = new boolean[numPages][maxSeps];
+ for (int page=0; page<numPages; page++) {
+ for (int i = 0; i < maxSeps; i++)
+ mSepEnabled[page][i] = true;
+ }
+ }
+
+ // count the seps on this page
+ int numSeps = core.getNumSepsOnPage(currentPage);
+
+ // for each sep,
+ for (int i = 0; i < numSeps; i++) {
+
+// // Robin use this to skip separations
+// if (i==12)
+// break;
+
+ // get the name
+ Separation sep = core.getSep(currentPage,i);
+ String name = sep.name;
+
+ // make a checkable menu item with that name
+ // and the separation index as the id
+ MenuItem item = menu.getMenu().add(0, i, 0, name+" ");
+ item.setCheckable(true);
+
+ // set an icon that's the right color
+ int iconSize = 48;
+ int alpha = (sep.rgba >> 24) & 0xFF;
+ int red = (sep.rgba >> 16) & 0xFF;
+ int green = (sep.rgba >> 8 ) & 0xFF;
+ int blue = (sep.rgba >> 0 ) & 0xFF;
+ int color = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
+
+ ShapeDrawable swatch = new ShapeDrawable (new RectShape());
+ swatch.setIntrinsicHeight(iconSize);
+ swatch.setIntrinsicWidth(iconSize);
+ swatch.setBounds(new Rect(0, 0, iconSize, iconSize));
+ swatch.getPaint().setColor(color);
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ item.setIcon(swatch);
+
+ // check it (or not)
+ item.setChecked(mSepEnabled[currentPage][i]);
+
+ // establishing a menu item listener
+ item.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ // someone tapped a menu item. get the ID
+ int sep = item.getItemId();
+
+ // toggle the sep
+ mSepEnabled[currentPage][sep] = !mSepEnabled[currentPage][sep];
+ item.setChecked(mSepEnabled[currentPage][sep]);
+ core.controlSepOnPage(currentPage, sep, !mSepEnabled[currentPage][sep]);
+
+ // prevent the menu from being dismissed by these items
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
+ item.setActionView(new View(v.getContext()));
+ item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ return false;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ return false;
+ }
+ });
+ return false;
+ }
+ });
+
+ // tell core to enable or disable each sep as appropriate
+ // but don't refresh the page yet.
+ core.controlSepOnPage(currentPage, i, !mSepEnabled[currentPage][i]);
+ }
+
+ // add one for done
+ MenuItem itemDone = menu.getMenu().add(0, 0, 0, "Done");
+ itemDone.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ // refresh the view
+ mDocView.refresh(false);
+ return true;
+ }
+ });
+
+ // show the menu
+ menu.show();
+ }
+
+ }
+
+ public void OnCopyTextButtonClick(View v) {
+ mTopBarMode = TopBarMode.Accept;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mAcceptMode = AcceptMode.CopyText;
+ mDocView.setMode(MuPDFReaderView.Mode.Selecting);
+ mAnnotTypeText.setText(getString(R.string.copy_text));
+ showInfo(getString(R.string.select_text));
+ }
+
+ public void OnEditAnnotButtonClick(View v) {
+ mTopBarMode = TopBarMode.Annot;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnCancelAnnotButtonClick(View v) {
+ mTopBarMode = TopBarMode.More;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnHighlightButtonClick(View v) {
+ mTopBarMode = TopBarMode.Accept;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mAcceptMode = AcceptMode.Highlight;
+ mDocView.setMode(MuPDFReaderView.Mode.Selecting);
+ mAnnotTypeText.setText(R.string.highlight);
+ showInfo(getString(R.string.select_text));
+ }
+
+ public void OnUnderlineButtonClick(View v) {
+ mTopBarMode = TopBarMode.Accept;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mAcceptMode = AcceptMode.Underline;
+ mDocView.setMode(MuPDFReaderView.Mode.Selecting);
+ mAnnotTypeText.setText(R.string.underline);
+ showInfo(getString(R.string.select_text));
+ }
+
+ public void OnStrikeOutButtonClick(View v) {
+ mTopBarMode = TopBarMode.Accept;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mAcceptMode = AcceptMode.StrikeOut;
+ mDocView.setMode(MuPDFReaderView.Mode.Selecting);
+ mAnnotTypeText.setText(R.string.strike_out);
+ showInfo(getString(R.string.select_text));
+ }
+
+ public void OnInkButtonClick(View v) {
+ mTopBarMode = TopBarMode.Accept;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mAcceptMode = AcceptMode.Ink;
+ mDocView.setMode(MuPDFReaderView.Mode.Drawing);
+ mAnnotTypeText.setText(R.string.ink);
+ showInfo(getString(R.string.draw_annotation));
+ }
+
+ public void OnCancelAcceptButtonClick(View v) {
+ MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView();
+ if (pageView != null) {
+ pageView.deselectText();
+ pageView.cancelDraw();
+ }
+ mDocView.setMode(MuPDFReaderView.Mode.Viewing);
+ switch (mAcceptMode) {
+ case CopyText:
+ mTopBarMode = TopBarMode.More;
+ break;
+ default:
+ mTopBarMode = TopBarMode.Annot;
+ break;
+ }
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnAcceptButtonClick(View v) {
+ MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView();
+ boolean success = false;
+ switch (mAcceptMode) {
+ case CopyText:
+ if (pageView != null)
+ success = pageView.copySelection();
+ mTopBarMode = TopBarMode.More;
+ showInfo(success?getString(R.string.copied_to_clipboard):getString(R.string.no_text_selected));
+ break;
+
+ case Highlight:
+ if (pageView != null)
+ success = pageView.markupSelection(Annotation.Type.HIGHLIGHT);
+ mTopBarMode = TopBarMode.Annot;
+ if (!success)
+ showInfo(getString(R.string.no_text_selected));
+ break;
+
+ case Underline:
+ if (pageView != null)
+ success = pageView.markupSelection(Annotation.Type.UNDERLINE);
+ mTopBarMode = TopBarMode.Annot;
+ if (!success)
+ showInfo(getString(R.string.no_text_selected));
+ break;
+
+ case StrikeOut:
+ if (pageView != null)
+ success = pageView.markupSelection(Annotation.Type.STRIKEOUT);
+ mTopBarMode = TopBarMode.Annot;
+ if (!success)
+ showInfo(getString(R.string.no_text_selected));
+ break;
+
+ case Ink:
+ if (pageView != null)
+ success = pageView.saveDraw();
+ mTopBarMode = TopBarMode.Annot;
+ if (!success)
+ showInfo(getString(R.string.nothing_to_save));
+ break;
+ }
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ mDocView.setMode(MuPDFReaderView.Mode.Viewing);
+ }
+
+ public void OnCancelSearchButtonClick(View v) {
+ searchModeOff();
+ }
+
+ public void OnDeleteButtonClick(View v) {
+ MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView();
+ if (pageView != null)
+ pageView.deleteSelectedAnnotation();
+ mTopBarMode = TopBarMode.Annot;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ public void OnCancelDeleteButtonClick(View v) {
+ MuPDFView pageView = (MuPDFView) mDocView.getDisplayedView();
+ if (pageView != null)
+ pageView.deselectAnnotation();
+ mTopBarMode = TopBarMode.Annot;
+ mTopBarSwitcher.setDisplayedChild(mTopBarMode.ordinal());
+ }
+
+ private void showKeyboard() {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null)
+ imm.showSoftInput(mSearchText, 0);
+ }
+
+ private void hideKeyboard() {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null)
+ imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0);
+ }
+
+ private void search(int direction) {
+ hideKeyboard();
+ int displayPage = mDocView.getDisplayedViewIndex();
+ SearchTaskResult r = SearchTaskResult.get();
+ int searchPage = r != null ? r.pageNumber : -1;
+ mSearchTask.go(mSearchText.getText().toString(), direction, displayPage, searchPage);
+ }
+
+ @Override
+ public boolean onSearchRequested() {
+ if (mButtonsVisible && mTopBarMode == TopBarMode.Search) {
+ hideButtons();
+ } else {
+ showButtons();
+ searchModeOn();
+ }
+ return super.onSearchRequested();
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ if (mButtonsVisible && mTopBarMode != TopBarMode.Search) {
+ hideButtons();
+ } else {
+ showButtons();
+ searchModeOff();
+ }
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ protected void onStart() {
+ if (core != null)
+ {
+ core.startAlerts();
+ createAlertWaiter();
+ }
+
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ if (core != null)
+ {
+ destroyAlertWaiter();
+ core.stopAlerts();
+ }
+
+ super.onStop();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (core != null && core.hasChanges()) {
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == AlertDialog.BUTTON_POSITIVE)
+ core.save();
+
+ finish();
+ }
+ };
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setTitle("MuPDF");
+ alert.setMessage(getString(R.string.document_has_changes_save_them_));
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.yes), listener);
+ alert.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.no), listener);
+ alert.show();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void performPickFor(FilePicker picker) {
+ mFilePicker = picker;
+ Intent intent = new Intent(this, ChoosePDFActivity.class);
+ intent.setAction(ChoosePDFActivity.PICK_KEY_FILE);
+ startActivityForResult(intent, FILEPICK_REQUEST);
+ }
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlert.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlert.java
new file mode 100644
index 00000000..76ed3a65
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlert.java
@@ -0,0 +1,21 @@
+package com.artifex.mupdfdemo;
+
+public class MuPDFAlert {
+ public enum IconType {Error,Warning,Question,Status};
+ public enum ButtonPressed {None,Ok,Cancel,No,Yes};
+ public enum ButtonGroupType {Ok,OkCancel,YesNo,YesNoCancel};
+
+ public final String message;
+ public final IconType iconType;
+ public final ButtonGroupType buttonGroupType;
+ public final String title;
+ public ButtonPressed buttonPressed;
+
+ MuPDFAlert(String aMessage, IconType aIconType, ButtonGroupType aButtonGroupType, String aTitle, ButtonPressed aButtonPressed) {
+ message = aMessage;
+ iconType = aIconType;
+ buttonGroupType = aButtonGroupType;
+ title = aTitle;
+ buttonPressed = aButtonPressed;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java
new file mode 100644
index 00000000..5d65768f
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java
@@ -0,0 +1,30 @@
+package com.artifex.mupdfdemo;
+
+// Version of MuPDFAlert without enums to simplify JNI
+public class MuPDFAlertInternal {
+ public final String message;
+ public final int iconType;
+ public final int buttonGroupType;
+ public final String title;
+ public int buttonPressed;
+
+ MuPDFAlertInternal(String aMessage, int aIconType, int aButtonGroupType, String aTitle, int aButtonPressed) {
+ message = aMessage;
+ iconType = aIconType;
+ buttonGroupType = aButtonGroupType;
+ title = aTitle;
+ buttonPressed = aButtonPressed;
+ }
+
+ MuPDFAlertInternal(MuPDFAlert alert) {
+ message = alert.message;
+ iconType = alert.iconType.ordinal();
+ buttonGroupType = alert.buttonGroupType.ordinal();
+ title = alert.message;
+ buttonPressed = alert.buttonPressed.ordinal();
+ }
+
+ MuPDFAlert toAlert() {
+ return new MuPDFAlert(message, MuPDFAlert.IconType.values()[iconType], MuPDFAlert.ButtonGroupType.values()[buttonGroupType], title, MuPDFAlert.ButtonPressed.values()[buttonPressed]);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCancellableTaskDefinition.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCancellableTaskDefinition.java
new file mode 100644
index 00000000..b95d8e93
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCancellableTaskDefinition.java
@@ -0,0 +1,38 @@
+package com.artifex.mupdfdemo;
+
+public abstract class MuPDFCancellableTaskDefinition<Params, Result> implements CancellableTaskDefinition<Params, Result>
+{
+ private MuPDFCore.Cookie cookie;
+
+ public MuPDFCancellableTaskDefinition(MuPDFCore core)
+ {
+ this.cookie = core.new Cookie();
+ }
+
+ @Override
+ public void doCancel()
+ {
+ if (cookie == null)
+ return;
+
+ cookie.abort();
+ }
+
+ @Override
+ public void doCleanup()
+ {
+ if (cookie == null)
+ return;
+
+ cookie.destroy();
+ cookie = null;
+ }
+
+ @Override
+ public final Result doInBackground(Params ... params)
+ {
+ return doInBackground(cookie, params);
+ }
+
+ public abstract Result doInBackground(MuPDFCore.Cookie cookie, Params ... params);
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCore.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCore.java
new file mode 100644
index 00000000..80174416
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFCore.java
@@ -0,0 +1,402 @@
+package com.artifex.mupdfdemo;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import java.util.ArrayList;
+
+public class MuPDFCore
+{
+ /* load our native library */
+ private static boolean gs_so_available = false;
+ static {
+ System.out.println("Loading dll");
+ System.loadLibrary("mupdf_java");
+ System.out.println("Loaded dll");
+ if (gprfSupportedInternal())
+ {
+ try {
+ System.loadLibrary("gs");
+ gs_so_available = true;
+ }
+ catch (UnsatisfiedLinkError e) {
+ gs_so_available = false;
+ }
+ }
+ }
+
+ /* Readable members */
+ private int numPages = -1;
+ private float pageWidth;
+ private float pageHeight;
+ private long globals;
+ private byte fileBuffer[];
+ private String file_format;
+ private boolean isUnencryptedPDF;
+ private final boolean wasOpenedFromBuffer;
+
+ /* The native functions */
+ private static native boolean gprfSupportedInternal();
+ private native long openFile(String filename);
+ private native long openBuffer(String magic);
+ private native String fileFormatInternal();
+ private native boolean isUnencryptedPDFInternal();
+ private native int countPagesInternal();
+ private native void gotoPageInternal(int localActionPageNum);
+ private native float getPageWidth();
+ private native float getPageHeight();
+ private native void drawPage(Bitmap bitmap,
+ int pageW, int pageH,
+ int patchX, int patchY,
+ int patchW, int patchH,
+ long cookiePtr);
+ private native void updatePageInternal(Bitmap bitmap,
+ int page,
+ int pageW, int pageH,
+ int patchX, int patchY,
+ int patchW, int patchH,
+ long cookiePtr);
+ private native RectF[] searchPage(String text);
+ private native TextChar[][][][] text();
+ private native byte[] textAsHtml();
+ private native void addMarkupAnnotationInternal(PointF[] quadPoints, int type);
+ private native void addInkAnnotationInternal(PointF[][] arcs);
+ private native void deleteAnnotationInternal(int annot_index);
+ private native int passClickEventInternal(int page, float x, float y);
+ private native void setFocusedWidgetChoiceSelectedInternal(String [] selected);
+ private native String [] getFocusedWidgetChoiceSelected();
+ private native String [] getFocusedWidgetChoiceOptions();
+ private native int getFocusedWidgetSignatureState();
+ private native String checkFocusedSignatureInternal();
+ private native boolean signFocusedSignatureInternal(String keyFile, String password);
+ private native int setFocusedWidgetTextInternal(String text);
+ private native String getFocusedWidgetTextInternal();
+ private native int getFocusedWidgetTypeInternal();
+ private native LinkInfo [] getPageLinksInternal(int page);
+ private native RectF[] getWidgetAreasInternal(int page);
+ private native Annotation[] getAnnotationsInternal(int page);
+ private native OutlineItem [] getOutlineInternal();
+ private native boolean hasOutlineInternal();
+ private native boolean needsPasswordInternal();
+ private native boolean authenticatePasswordInternal(String password);
+ private native MuPDFAlertInternal waitForAlertInternal();
+ private native void replyToAlertInternal(MuPDFAlertInternal alert);
+ private native void startAlertsInternal();
+ private native void stopAlertsInternal();
+ private native void destroying();
+ private native boolean hasChangesInternal();
+ private native void saveInternal();
+ private native long createCookie();
+ private native void destroyCookie(long cookie);
+ private native void abortCookie(long cookie);
+
+ private native String startProofInternal(int resolution);
+ private native void endProofInternal(String filename);
+ private native int getNumSepsOnPageInternal(int page);
+ private native int controlSepOnPageInternal(int page, int sep, boolean disable);
+ private native Separation getSepInternal(int page, int sep);
+
+ public native boolean javascriptSupported();
+
+ public class Cookie
+ {
+ private final long cookiePtr;
+
+ public Cookie()
+ {
+ cookiePtr = createCookie();
+ if (cookiePtr == 0)
+ throw new OutOfMemoryError();
+ }
+
+ public void abort()
+ {
+ abortCookie(cookiePtr);
+ }
+
+ public void destroy()
+ {
+ // We could do this in finalize, but there's no guarantee that
+ // a finalize will occur before the muPDF context occurs.
+ destroyCookie(cookiePtr);
+ }
+ }
+
+ public MuPDFCore(Context context, String filename) throws Exception
+ {
+ globals = openFile(filename);
+ if (globals == 0)
+ {
+ throw new Exception(String.format(context.getString(R.string.cannot_open_file_Path), filename));
+ }
+ file_format = fileFormatInternal();
+ isUnencryptedPDF = isUnencryptedPDFInternal();
+ wasOpenedFromBuffer = false;
+ }
+
+ public MuPDFCore(Context context, byte buffer[], String magic) throws Exception {
+ fileBuffer = buffer;
+ globals = openBuffer(magic != null ? magic : "");
+ if (globals == 0)
+ {
+ throw new Exception(context.getString(R.string.cannot_open_buffer));
+ }
+ file_format = fileFormatInternal();
+ isUnencryptedPDF = isUnencryptedPDFInternal();
+ wasOpenedFromBuffer = true;
+ }
+
+ public int countPages()
+ {
+ if (numPages < 0)
+ numPages = countPagesSynchronized();
+ return numPages;
+ }
+
+ public String fileFormat()
+ {
+ return file_format;
+ }
+
+ public boolean isUnencryptedPDF()
+ {
+ return isUnencryptedPDF;
+ }
+
+ public boolean wasOpenedFromBuffer()
+ {
+ return wasOpenedFromBuffer;
+ }
+
+ private synchronized int countPagesSynchronized() {
+ return countPagesInternal();
+ }
+
+ /* Shim function */
+ private void gotoPage(int page)
+ {
+ if (page > numPages-1)
+ page = numPages-1;
+ else if (page < 0)
+ page = 0;
+ gotoPageInternal(page);
+ this.pageWidth = getPageWidth();
+ this.pageHeight = getPageHeight();
+ }
+
+ public synchronized PointF getPageSize(int page) {
+ gotoPage(page);
+ return new PointF(pageWidth, pageHeight);
+ }
+
+ public MuPDFAlert waitForAlert() {
+ MuPDFAlertInternal alert = waitForAlertInternal();
+ return alert != null ? alert.toAlert() : null;
+ }
+
+ public void replyToAlert(MuPDFAlert alert) {
+ replyToAlertInternal(new MuPDFAlertInternal(alert));
+ }
+
+ public void stopAlerts() {
+ stopAlertsInternal();
+ }
+
+ public void startAlerts() {
+ startAlertsInternal();
+ }
+
+ public synchronized void onDestroy() {
+ destroying();
+ globals = 0;
+ }
+
+ public synchronized void drawPage(Bitmap bm, int page,
+ int pageW, int pageH,
+ int patchX, int patchY,
+ int patchW, int patchH,
+ MuPDFCore.Cookie cookie) {
+ gotoPage(page);
+ drawPage(bm, pageW, pageH, patchX, patchY, patchW, patchH, cookie.cookiePtr);
+ }
+
+ public synchronized void updatePage(Bitmap bm, int page,
+ int pageW, int pageH,
+ int patchX, int patchY,
+ int patchW, int patchH,
+ MuPDFCore.Cookie cookie) {
+ updatePageInternal(bm, page, pageW, pageH, patchX, patchY, patchW, patchH, cookie.cookiePtr);
+ }
+
+ public synchronized PassClickResult passClickEvent(int page, float x, float y) {
+ boolean changed = passClickEventInternal(page, x, y) != 0;
+
+ switch (WidgetType.values()[getFocusedWidgetTypeInternal()])
+ {
+ case TEXT:
+ return new PassClickResultText(changed, getFocusedWidgetTextInternal());
+ case LISTBOX:
+ case COMBOBOX:
+ return new PassClickResultChoice(changed, getFocusedWidgetChoiceOptions(), getFocusedWidgetChoiceSelected());
+ case SIGNATURE:
+ return new PassClickResultSignature(changed, getFocusedWidgetSignatureState());
+ default:
+ return new PassClickResult(changed);
+ }
+
+ }
+
+ public synchronized boolean setFocusedWidgetText(int page, String text) {
+ boolean success;
+ gotoPage(page);
+ success = setFocusedWidgetTextInternal(text) != 0 ? true : false;
+
+ return success;
+ }
+
+ public synchronized void setFocusedWidgetChoiceSelected(String [] selected) {
+ setFocusedWidgetChoiceSelectedInternal(selected);
+ }
+
+ public synchronized String checkFocusedSignature() {
+ return checkFocusedSignatureInternal();
+ }
+
+ public synchronized boolean signFocusedSignature(String keyFile, String password) {
+ return signFocusedSignatureInternal(keyFile, password);
+ }
+
+ public synchronized LinkInfo [] getPageLinks(int page) {
+ return getPageLinksInternal(page);
+ }
+
+ public synchronized RectF [] getWidgetAreas(int page) {
+ return getWidgetAreasInternal(page);
+ }
+
+ public synchronized Annotation [] getAnnoations(int page) {
+ return getAnnotationsInternal(page);
+ }
+
+ public synchronized RectF [] searchPage(int page, String text) {
+ gotoPage(page);
+ return searchPage(text);
+ }
+
+ public synchronized byte[] html(int page) {
+ gotoPage(page);
+ return textAsHtml();
+ }
+
+ public synchronized TextWord [][] textLines(int page) {
+ gotoPage(page);
+ TextChar[][][][] chars = text();
+
+ // The text of the page held in a hierarchy (blocks, lines, spans).
+ // Currently we don't need to distinguish the blocks level or
+ // the spans, and we need to collect the text into words.
+ ArrayList<TextWord[]> lns = new ArrayList<TextWord[]>();
+
+ for (TextChar[][][] bl: chars) {
+ if (bl == null)
+ continue;
+ for (TextChar[][] ln: bl) {
+ ArrayList<TextWord> wds = new ArrayList<TextWord>();
+ TextWord wd = new TextWord();
+
+ for (TextChar[] sp: ln) {
+ for (TextChar tc: sp) {
+ if (tc.c != ' ') {
+ wd.Add(tc);
+ } else if (wd.w.length() > 0) {
+ wds.add(wd);
+ wd = new TextWord();
+ }
+ }
+ }
+
+ if (wd.w.length() > 0)
+ wds.add(wd);
+
+ if (wds.size() > 0)
+ lns.add(wds.toArray(new TextWord[wds.size()]));
+ }
+ }
+
+ return lns.toArray(new TextWord[lns.size()][]);
+ }
+
+ public synchronized void addMarkupAnnotation(int page, PointF[] quadPoints, Annotation.Type type) {
+ gotoPage(page);
+ addMarkupAnnotationInternal(quadPoints, type.ordinal());
+ }
+
+ public synchronized void addInkAnnotation(int page, PointF[][] arcs) {
+ gotoPage(page);
+ addInkAnnotationInternal(arcs);
+ }
+
+ public synchronized void deleteAnnotation(int page, int annot_index) {
+ gotoPage(page);
+ deleteAnnotationInternal(annot_index);
+ }
+
+ public synchronized boolean hasOutline() {
+ return hasOutlineInternal();
+ }
+
+ public synchronized OutlineItem [] getOutline() {
+ return getOutlineInternal();
+ }
+
+ public synchronized boolean needsPassword() {
+ return needsPasswordInternal();
+ }
+
+ public synchronized boolean authenticatePassword(String password) {
+ return authenticatePasswordInternal(password);
+ }
+
+ public synchronized boolean hasChanges() {
+ return hasChangesInternal();
+ }
+
+ public synchronized void save() {
+ saveInternal();
+ }
+
+ public synchronized String startProof(int resolution) {
+ return startProofInternal(resolution);
+ }
+
+ public synchronized void endProof(String filename) {
+ endProofInternal(filename);
+ }
+
+ public static boolean gprfSupported() {
+ if (gs_so_available == false)
+ return false;
+ return gprfSupportedInternal();
+ }
+
+ public boolean canProof()
+ {
+ String format = fileFormat();
+ if (format.contains("PDF"))
+ return true;
+ return false;
+ }
+
+ public synchronized int getNumSepsOnPage(int page) {
+ return getNumSepsOnPageInternal(page);
+ }
+
+ public synchronized int controlSepOnPage(int page, int sep, boolean disable) {
+ return controlSepOnPageInternal(page, sep, disable);
+ }
+
+ public synchronized Separation getSep(int page, int sep) {
+ return getSepInternal(page, sep);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java
new file mode 100644
index 00000000..abdac845
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java
@@ -0,0 +1,87 @@
+package com.artifex.mupdfdemo;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+public class MuPDFPageAdapter extends BaseAdapter {
+ private final Context mContext;
+ private final FilePicker.FilePickerSupport mFilePickerSupport;
+ private final MuPDFCore mCore;
+ private final SparseArray<PointF> mPageSizes = new SparseArray<PointF>();
+ private Bitmap mSharedHqBm;
+
+ public MuPDFPageAdapter(Context c, FilePicker.FilePickerSupport filePickerSupport, MuPDFCore core) {
+ mContext = c;
+ mFilePickerSupport = filePickerSupport;
+ mCore = core;
+ }
+
+ public int getCount() {
+ return mCore.countPages();
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ public void releaseBitmaps()
+ {
+ // recycle and release the shared bitmap.
+ if (mSharedHqBm!=null)
+ mSharedHqBm.recycle();
+ mSharedHqBm = null;
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ final MuPDFPageView pageView;
+ if (convertView == null) {
+ if (mSharedHqBm == null || mSharedHqBm.getWidth() != parent.getWidth() || mSharedHqBm.getHeight() != parent.getHeight())
+ mSharedHqBm = Bitmap.createBitmap(parent.getWidth(), parent.getHeight(), Bitmap.Config.ARGB_8888);
+
+ pageView = new MuPDFPageView(mContext, mFilePickerSupport, mCore, new Point(parent.getWidth(), parent.getHeight()), mSharedHqBm);
+ } else {
+ pageView = (MuPDFPageView) convertView;
+ }
+
+ PointF pageSize = mPageSizes.get(position);
+ if (pageSize != null) {
+ // We already know the page size. Set it up
+ // immediately
+ pageView.setPage(position, pageSize);
+ } else {
+ // Page size as yet unknown. Blank it for now, and
+ // start a background task to find the size
+ pageView.blank(position);
+ AsyncTask<Void,Void,PointF> sizingTask = new AsyncTask<Void,Void,PointF>() {
+ @Override
+ protected PointF doInBackground(Void... arg0) {
+ return mCore.getPageSize(position);
+ }
+
+ @Override
+ protected void onPostExecute(PointF result) {
+ super.onPostExecute(result);
+ // We now know the page size
+ mPageSizes.put(position, result);
+ // Check that this view hasn't been reused for
+ // another page since we started
+ if (pageView.getPage() == position)
+ pageView.setPage(position, result);
+ }
+ };
+
+ sizingTask.execute((Void)null);
+ }
+ return pageView;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageView.java
new file mode 100644
index 00000000..ff6b6bbb
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFPageView.java
@@ -0,0 +1,692 @@
+package com.artifex.mupdfdemo;
+
+import java.util.ArrayList;
+
+import com.artifex.mupdfdemo.MuPDFCore.Cookie;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.text.method.PasswordTransformationMethod;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+
+/* This enum should be kept in line with the cooresponding C enum in mupdf.c */
+enum SignatureState {
+ NoSupport,
+ Unsigned,
+ Signed
+}
+
+abstract class PassClickResultVisitor {
+ public abstract void visitText(PassClickResultText result);
+ public abstract void visitChoice(PassClickResultChoice result);
+ public abstract void visitSignature(PassClickResultSignature result);
+}
+
+class PassClickResult {
+ public final boolean changed;
+
+ public PassClickResult(boolean _changed) {
+ changed = _changed;
+ }
+
+ public void acceptVisitor(PassClickResultVisitor visitor) {
+ }
+}
+
+class PassClickResultText extends PassClickResult {
+ public final String text;
+
+ public PassClickResultText(boolean _changed, String _text) {
+ super(_changed);
+ text = _text;
+ }
+
+ public void acceptVisitor(PassClickResultVisitor visitor) {
+ visitor.visitText(this);
+ }
+}
+
+class PassClickResultChoice extends PassClickResult {
+ public final String [] options;
+ public final String [] selected;
+
+ public PassClickResultChoice(boolean _changed, String [] _options, String [] _selected) {
+ super(_changed);
+ options = _options;
+ selected = _selected;
+ }
+
+ public void acceptVisitor(PassClickResultVisitor visitor) {
+ visitor.visitChoice(this);
+ }
+}
+
+class PassClickResultSignature extends PassClickResult {
+ public final SignatureState state;
+
+ public PassClickResultSignature(boolean _changed, int _state) {
+ super(_changed);
+ state = SignatureState.values()[_state];
+ }
+
+ public void acceptVisitor(PassClickResultVisitor visitor) {
+ visitor.visitSignature(this);
+ }
+}
+
+public class MuPDFPageView extends PageView implements MuPDFView {
+ final private FilePicker.FilePickerSupport mFilePickerSupport;
+ private final MuPDFCore mCore;
+ private AsyncTask<Void,Void,PassClickResult> mPassClick;
+ private RectF mWidgetAreas[];
+ private Annotation mAnnotations[];
+ private int mSelectedAnnotationIndex = -1;
+ private AsyncTask<Void,Void,RectF[]> mLoadWidgetAreas;
+ private AsyncTask<Void,Void,Annotation[]> mLoadAnnotations;
+ private AlertDialog.Builder mTextEntryBuilder;
+ private AlertDialog.Builder mChoiceEntryBuilder;
+ private AlertDialog.Builder mSigningDialogBuilder;
+ private AlertDialog.Builder mSignatureReportBuilder;
+ private AlertDialog.Builder mPasswordEntryBuilder;
+ private EditText mPasswordText;
+ private AlertDialog mTextEntry;
+ private AlertDialog mPasswordEntry;
+ private EditText mEditText;
+ private AsyncTask<String,Void,Boolean> mSetWidgetText;
+ private AsyncTask<String,Void,Void> mSetWidgetChoice;
+ private AsyncTask<PointF[],Void,Void> mAddStrikeOut;
+ private AsyncTask<PointF[][],Void,Void> mAddInk;
+ private AsyncTask<Integer,Void,Void> mDeleteAnnotation;
+ private AsyncTask<Void,Void,String> mCheckSignature;
+ private AsyncTask<Void,Void,Boolean> mSign;
+ private Runnable changeReporter;
+
+ public MuPDFPageView(Context c, FilePicker.FilePickerSupport filePickerSupport, MuPDFCore core, Point parentSize, Bitmap sharedHqBm) {
+ super(c, parentSize, sharedHqBm);
+ mFilePickerSupport = filePickerSupport;
+ mCore = core;
+ mTextEntryBuilder = new AlertDialog.Builder(c);
+ mTextEntryBuilder.setTitle(getContext().getString(R.string.fill_out_text_field));
+ LayoutInflater inflater = (LayoutInflater)c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mEditText = (EditText)inflater.inflate(R.layout.textentry, null);
+ mTextEntryBuilder.setView(mEditText);
+ mTextEntryBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ mTextEntryBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mSetWidgetText = new AsyncTask<String,Void,Boolean> () {
+ @Override
+ protected Boolean doInBackground(String... arg0) {
+ return mCore.setFocusedWidgetText(mPageNumber, arg0[0]);
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ changeReporter.run();
+ if (!result)
+ invokeTextDialog(mEditText.getText().toString());
+ }
+ };
+
+ mSetWidgetText.execute(mEditText.getText().toString());
+ }
+ });
+ mTextEntry = mTextEntryBuilder.create();
+
+ mChoiceEntryBuilder = new AlertDialog.Builder(c);
+ mChoiceEntryBuilder.setTitle(getContext().getString(R.string.choose_value));
+
+ mSigningDialogBuilder = new AlertDialog.Builder(c);
+ mSigningDialogBuilder.setTitle("Select certificate and sign?");
+ mSigningDialogBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ mSigningDialogBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ FilePicker picker = new FilePicker(mFilePickerSupport) {
+ @Override
+ void onPick(Uri uri) {
+ signWithKeyFile(uri);
+ }
+ };
+
+ picker.pick();
+ }
+ });
+
+ mSignatureReportBuilder = new AlertDialog.Builder(c);
+ mSignatureReportBuilder.setTitle("Signature checked");
+ mSignatureReportBuilder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ mPasswordText = new EditText(c);
+ mPasswordText.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+ mPasswordText.setTransformationMethod(new PasswordTransformationMethod());
+
+ mPasswordEntryBuilder = new AlertDialog.Builder(c);
+ mPasswordEntryBuilder.setTitle(R.string.enter_password);
+ mPasswordEntryBuilder.setView(mPasswordText);
+ mPasswordEntryBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ mPasswordEntry = mPasswordEntryBuilder.create();
+ }
+
+ private void signWithKeyFile(final Uri uri) {
+ mPasswordEntry.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ mPasswordEntry.setButton(AlertDialog.BUTTON_POSITIVE, "Sign", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ signWithKeyFileAndPassword(uri, mPasswordText.getText().toString());
+ }
+ });
+
+ mPasswordEntry.show();
+ }
+
+ private void signWithKeyFileAndPassword(final Uri uri, final String password) {
+ mSign = new AsyncTask<Void,Void,Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return mCore.signFocusedSignature(Uri.decode(uri.getEncodedPath()), password);
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result)
+ {
+ changeReporter.run();
+ }
+ else
+ {
+ mPasswordText.setText("");
+ signWithKeyFile(uri);
+ }
+ }
+
+ };
+
+ mSign.execute();
+ }
+
+ public LinkInfo hitLink(float x, float y) {
+ // Since link highlighting was implemented, the super class
+ // PageView has had sufficient information to be able to
+ // perform this method directly. Making that change would
+ // make MuPDFCore.hitLinkPage superfluous.
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ float docRelX = (x - getLeft())/scale;
+ float docRelY = (y - getTop())/scale;
+
+ for (LinkInfo l: mLinks)
+ if (l.rect.contains(docRelX, docRelY))
+ return l;
+
+ return null;
+ }
+
+ private void invokeTextDialog(String text) {
+ mEditText.setText(text);
+ mTextEntry.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ mTextEntry.show();
+ }
+
+ private void invokeChoiceDialog(final String [] options) {
+ mChoiceEntryBuilder.setItems(options, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mSetWidgetChoice = new AsyncTask<String,Void,Void>() {
+ @Override
+ protected Void doInBackground(String... params) {
+ String [] sel = {params[0]};
+ mCore.setFocusedWidgetChoiceSelected(sel);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ changeReporter.run();
+ }
+ };
+
+ mSetWidgetChoice.execute(options[which]);
+ }
+ });
+ AlertDialog dialog = mChoiceEntryBuilder.create();
+ dialog.show();
+ }
+
+ private void invokeSignatureCheckingDialog() {
+ mCheckSignature = new AsyncTask<Void,Void,String> () {
+ @Override
+ protected String doInBackground(Void... params) {
+ return mCore.checkFocusedSignature();
+ }
+ @Override
+ protected void onPostExecute(String result) {
+ AlertDialog report = mSignatureReportBuilder.create();
+ report.setMessage(result);
+ report.show();
+ }
+ };
+
+ mCheckSignature.execute();
+ }
+
+ private void invokeSigningDialog() {
+ AlertDialog dialog = mSigningDialogBuilder.create();
+ dialog.show();
+ }
+
+ private void warnNoSignatureSupport() {
+ AlertDialog dialog = mSignatureReportBuilder.create();
+ dialog.setTitle("App built with no signature support");
+ dialog.show();
+ }
+
+ public void setChangeReporter(Runnable reporter) {
+ changeReporter = reporter;
+ }
+
+ public Hit passClickEvent(float x, float y) {
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ final float docRelX = (x - getLeft())/scale;
+ final float docRelY = (y - getTop())/scale;
+ boolean hit = false;
+ int i;
+
+ if (mAnnotations != null) {
+ for (i = 0; i < mAnnotations.length; i++)
+ if (mAnnotations[i].contains(docRelX, docRelY)) {
+ hit = true;
+ break;
+ }
+
+ if (hit) {
+ switch (mAnnotations[i].type) {
+ case HIGHLIGHT:
+ case UNDERLINE:
+ case SQUIGGLY:
+ case STRIKEOUT:
+ case INK:
+ mSelectedAnnotationIndex = i;
+ setItemSelectBox(mAnnotations[i]);
+ return Hit.Annotation;
+ }
+ }
+ }
+
+ mSelectedAnnotationIndex = -1;
+ setItemSelectBox(null);
+
+ if (!mCore.javascriptSupported())
+ return Hit.Nothing;
+
+ if (mWidgetAreas != null) {
+ for (i = 0; i < mWidgetAreas.length && !hit; i++)
+ if (mWidgetAreas[i].contains(docRelX, docRelY))
+ hit = true;
+ }
+
+ if (hit) {
+ mPassClick = new AsyncTask<Void,Void,PassClickResult>() {
+ @Override
+ protected PassClickResult doInBackground(Void... arg0) {
+ return mCore.passClickEvent(mPageNumber, docRelX, docRelY);
+ }
+
+ @Override
+ protected void onPostExecute(PassClickResult result) {
+ if (result.changed) {
+ changeReporter.run();
+ }
+
+ result.acceptVisitor(new PassClickResultVisitor() {
+ @Override
+ public void visitText(PassClickResultText result) {
+ invokeTextDialog(result.text);
+ }
+
+ @Override
+ public void visitChoice(PassClickResultChoice result) {
+ invokeChoiceDialog(result.options);
+ }
+
+ @Override
+ public void visitSignature(PassClickResultSignature result) {
+ switch (result.state) {
+ case NoSupport:
+ warnNoSignatureSupport();
+ break;
+ case Unsigned:
+ invokeSigningDialog();
+ break;
+ case Signed:
+ invokeSignatureCheckingDialog();
+ break;
+ }
+ }
+ });
+ }
+ };
+
+ mPassClick.execute();
+ return Hit.Widget;
+ }
+
+ return Hit.Nothing;
+ }
+
+ @TargetApi(11)
+ public boolean copySelection() {
+ final StringBuilder text = new StringBuilder();
+
+ processSelectedText(new TextProcessor() {
+ StringBuilder line;
+
+ public void onStartLine() {
+ line = new StringBuilder();
+ }
+
+ public void onWord(TextWord word) {
+ if (line.length() > 0)
+ line.append(' ');
+ line.append(word.w);
+ }
+
+ public void onEndLine() {
+ if (text.length() > 0)
+ text.append('\n');
+ text.append(line);
+ }
+ });
+
+ if (text.length() == 0)
+ return false;
+
+ int currentApiVersion = android.os.Build.VERSION.SDK_INT;
+ if (currentApiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ android.content.ClipboardManager cm = (android.content.ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ cm.setPrimaryClip(ClipData.newPlainText("MuPDF", text));
+ } else {
+ android.text.ClipboardManager cm = (android.text.ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setText(text);
+ }
+
+ deselectText();
+
+ return true;
+ }
+
+ public boolean markupSelection(final Annotation.Type type) {
+ final ArrayList<PointF> quadPoints = new ArrayList<PointF>();
+ processSelectedText(new TextProcessor() {
+ RectF rect;
+
+ public void onStartLine() {
+ rect = new RectF();
+ }
+
+ public void onWord(TextWord word) {
+ rect.union(word);
+ }
+
+ public void onEndLine() {
+ if (!rect.isEmpty()) {
+ quadPoints.add(new PointF(rect.left, rect.bottom));
+ quadPoints.add(new PointF(rect.right, rect.bottom));
+ quadPoints.add(new PointF(rect.right, rect.top));
+ quadPoints.add(new PointF(rect.left, rect.top));
+ }
+ }
+ });
+
+ if (quadPoints.size() == 0)
+ return false;
+
+ mAddStrikeOut = new AsyncTask<PointF[],Void,Void>() {
+ @Override
+ protected Void doInBackground(PointF[]... params) {
+ addMarkup(params[0], type);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ loadAnnotations();
+ update();
+ }
+ };
+
+ mAddStrikeOut.execute(quadPoints.toArray(new PointF[quadPoints.size()]));
+
+ deselectText();
+
+ return true;
+ }
+
+ public void deleteSelectedAnnotation() {
+ if (mSelectedAnnotationIndex != -1) {
+ if (mDeleteAnnotation != null)
+ mDeleteAnnotation.cancel(true);
+
+ mDeleteAnnotation = new AsyncTask<Integer,Void,Void>() {
+ @Override
+ protected Void doInBackground(Integer... params) {
+ mCore.deleteAnnotation(mPageNumber, params[0]);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ loadAnnotations();
+ update();
+ }
+ };
+
+ mDeleteAnnotation.execute(mSelectedAnnotationIndex);
+
+ mSelectedAnnotationIndex = -1;
+ setItemSelectBox(null);
+ }
+ }
+
+ public void deselectAnnotation() {
+ mSelectedAnnotationIndex = -1;
+ setItemSelectBox(null);
+ }
+
+ public boolean saveDraw() {
+ PointF[][] path = getDraw();
+
+ if (path == null)
+ return false;
+
+ if (mAddInk != null) {
+ mAddInk.cancel(true);
+ mAddInk = null;
+ }
+ mAddInk = new AsyncTask<PointF[][],Void,Void>() {
+ @Override
+ protected Void doInBackground(PointF[][]... params) {
+ mCore.addInkAnnotation(mPageNumber, params[0]);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ loadAnnotations();
+ update();
+ }
+
+ };
+
+ mAddInk.execute(getDraw());
+ cancelDraw();
+
+ return true;
+ }
+
+
+ @Override
+ protected CancellableTaskDefinition<Void, Void> getDrawPageTask(final Bitmap bm, final int sizeX, final int sizeY,
+ final int patchX, final int patchY, final int patchWidth, final int patchHeight) {
+ return new MuPDFCancellableTaskDefinition<Void, Void>(mCore) {
+ @Override
+ public Void doInBackground(MuPDFCore.Cookie cookie, Void ... params) {
+ // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
+ // is not incremented when drawing.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ bm.eraseColor(0);
+ mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
+ return null;
+ }
+ };
+
+ }
+
+ protected CancellableTaskDefinition<Void, Void> getUpdatePageTask(final Bitmap bm, final int sizeX, final int sizeY,
+ final int patchX, final int patchY, final int patchWidth, final int patchHeight)
+ {
+ return new MuPDFCancellableTaskDefinition<Void, Void>(mCore) {
+
+ @Override
+ public Void doInBackground(MuPDFCore.Cookie cookie, Void ... params) {
+ // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
+ // is not incremented when drawing.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ bm.eraseColor(0);
+ mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
+ return null;
+ }
+ };
+ }
+
+ @Override
+ protected LinkInfo[] getLinkInfo() {
+ return mCore.getPageLinks(mPageNumber);
+ }
+
+ @Override
+ protected TextWord[][] getText() {
+ return mCore.textLines(mPageNumber);
+ }
+
+ @Override
+ protected void addMarkup(PointF[] quadPoints, Annotation.Type type) {
+ mCore.addMarkupAnnotation(mPageNumber, quadPoints, type);
+ }
+
+ private void loadAnnotations() {
+ mAnnotations = null;
+ if (mLoadAnnotations != null)
+ mLoadAnnotations.cancel(true);
+ mLoadAnnotations = new AsyncTask<Void,Void,Annotation[]> () {
+ @Override
+ protected Annotation[] doInBackground(Void... params) {
+ return mCore.getAnnoations(mPageNumber);
+ }
+
+ @Override
+ protected void onPostExecute(Annotation[] result) {
+ mAnnotations = result;
+ }
+ };
+
+ mLoadAnnotations.execute();
+ }
+
+ @Override
+ public void setPage(final int page, PointF size) {
+ loadAnnotations();
+
+ mLoadWidgetAreas = new AsyncTask<Void,Void,RectF[]> () {
+ @Override
+ protected RectF[] doInBackground(Void... arg0) {
+ return mCore.getWidgetAreas(page);
+ }
+
+ @Override
+ protected void onPostExecute(RectF[] result) {
+ mWidgetAreas = result;
+ }
+ };
+
+ mLoadWidgetAreas.execute();
+
+ super.setPage(page, size);
+ }
+
+ public void setScale(float scale) {
+ // This type of view scales automatically to fit the size
+ // determined by the parent view groups during layout
+ }
+
+ @Override
+ public void releaseResources() {
+ if (mPassClick != null) {
+ mPassClick.cancel(true);
+ mPassClick = null;
+ }
+
+ if (mLoadWidgetAreas != null) {
+ mLoadWidgetAreas.cancel(true);
+ mLoadWidgetAreas = null;
+ }
+
+ if (mLoadAnnotations != null) {
+ mLoadAnnotations.cancel(true);
+ mLoadAnnotations = null;
+ }
+
+ if (mSetWidgetText != null) {
+ mSetWidgetText.cancel(true);
+ mSetWidgetText = null;
+ }
+
+ if (mSetWidgetChoice != null) {
+ mSetWidgetChoice.cancel(true);
+ mSetWidgetChoice = null;
+ }
+
+ if (mAddStrikeOut != null) {
+ mAddStrikeOut.cancel(true);
+ mAddStrikeOut = null;
+ }
+
+ if (mDeleteAnnotation != null) {
+ mDeleteAnnotation.cancel(true);
+ mDeleteAnnotation = null;
+ }
+
+ super.releaseResources();
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReaderView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReaderView.java
new file mode 100644
index 00000000..f2d7f8fe
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReaderView.java
@@ -0,0 +1,276 @@
+package com.artifex.mupdfdemo;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.WindowManager;
+
+public class MuPDFReaderView extends ReaderView {
+ public enum Mode {Viewing, Selecting, Drawing}
+ private final Context mContext;
+ private boolean mLinksEnabled = false;
+ private Mode mMode = Mode.Viewing;
+ private boolean tapDisabled = false;
+ private int tapPageMargin;
+
+ protected void onTapMainDocArea() {}
+ protected void onDocMotion() {}
+ protected void onHit(Hit item) {};
+
+ public void setLinksEnabled(boolean b) {
+ mLinksEnabled = b;
+ resetupChildren();
+ }
+
+ public void setMode(Mode m) {
+ mMode = m;
+ }
+
+ private void setup()
+ {
+ // Get the screen size etc to customise tap margins.
+ // We calculate the size of 1 inch of the screen for tapping.
+ // On some devices the dpi values returned are wrong, so we
+ // sanity check it: we first restrict it so that we are never
+ // less than 100 pixels (the smallest Android device screen
+ // dimension I've seen is 480 pixels or so). Then we check
+ // to ensure we are never more than 1/5 of the screen width.
+ DisplayMetrics dm = new DisplayMetrics();
+ WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(dm);
+ tapPageMargin = (int)dm.xdpi;
+ if (tapPageMargin < 100)
+ tapPageMargin = 100;
+ if (tapPageMargin > dm.widthPixels/5)
+ tapPageMargin = dm.widthPixels/5;
+ }
+
+ public MuPDFReaderView(Context context) {
+ super(context);
+ mContext = context;
+ setup();
+ }
+
+ public MuPDFReaderView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ mContext = context;
+ setup();
+ }
+
+ public boolean onSingleTapUp(MotionEvent e) {
+ LinkInfo link = null;
+
+ if (mMode == Mode.Viewing && !tapDisabled) {
+ MuPDFView pageView = (MuPDFView) getDisplayedView();
+ Hit item = pageView.passClickEvent(e.getX(), e.getY());
+ onHit(item);
+ if (item == Hit.Nothing) {
+ if (mLinksEnabled && pageView != null
+ && (link = pageView.hitLink(e.getX(), e.getY())) != null) {
+ link.acceptVisitor(new LinkInfoVisitor() {
+ @Override
+ public void visitInternal(LinkInfoInternal li) {
+ // Clicked on an internal (GoTo) link
+ setDisplayedViewIndex(li.pageNumber);
+ }
+
+ @Override
+ public void visitExternal(LinkInfoExternal li) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri
+ .parse(li.url));
+ mContext.startActivity(intent);
+ }
+
+ @Override
+ public void visitRemote(LinkInfoRemote li) {
+ // Clicked on a remote (GoToR) link
+ }
+ });
+ } else if (e.getX() < tapPageMargin) {
+ super.smartMoveBackwards();
+ } else if (e.getX() > super.getWidth() - tapPageMargin) {
+ super.smartMoveForwards();
+ } else if (e.getY() < tapPageMargin) {
+ super.smartMoveBackwards();
+ } else if (e.getY() > super.getHeight() - tapPageMargin) {
+ super.smartMoveForwards();
+ } else {
+ onTapMainDocArea();
+ }
+ }
+ }
+ return super.onSingleTapUp(e);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+
+ return super.onDown(e);
+ }
+
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ MuPDFView pageView = (MuPDFView)getDisplayedView();
+ switch (mMode) {
+ case Viewing:
+ if (!tapDisabled)
+ onDocMotion();
+
+ return super.onScroll(e1, e2, distanceX, distanceY);
+ case Selecting:
+ if (pageView != null)
+ pageView.selectText(e1.getX(), e1.getY(), e2.getX(), e2.getY());
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ switch (mMode) {
+ case Viewing:
+ return super.onFling(e1, e2, velocityX, velocityY);
+ default:
+ return true;
+ }
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector d) {
+ // Disabled showing the buttons until next touch.
+ // Not sure why this is needed, but without it
+ // pinch zoom can make the buttons appear
+ tapDisabled = true;
+ return super.onScaleBegin(d);
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+
+ if ( mMode == Mode.Drawing )
+ {
+ float x = event.getX();
+ float y = event.getY();
+ switch (event.getAction())
+ {
+ case MotionEvent.ACTION_DOWN:
+ touch_start(x, y);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ touch_move(x, y);
+ break;
+ case MotionEvent.ACTION_UP:
+ touch_up();
+ break;
+ }
+ }
+
+ if ((event.getAction() & event.getActionMasked()) == MotionEvent.ACTION_DOWN)
+ {
+ tapDisabled = false;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ private float mX, mY;
+
+ private static final float TOUCH_TOLERANCE = 2;
+
+ private void touch_start(float x, float y) {
+
+ MuPDFView pageView = (MuPDFView)getDisplayedView();
+ if (pageView != null)
+ {
+ pageView.startDraw(x, y);
+ }
+ mX = x;
+ mY = y;
+ }
+
+ private void touch_move(float x, float y) {
+
+ float dx = Math.abs(x - mX);
+ float dy = Math.abs(y - mY);
+ if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
+ {
+ MuPDFView pageView = (MuPDFView)getDisplayedView();
+ if (pageView != null)
+ {
+ pageView.continueDraw(x, y);
+ }
+ mX = x;
+ mY = y;
+ }
+ }
+
+ private void touch_up() {
+
+ // NOOP
+ }
+
+ protected void onChildSetup(int i, View v) {
+ if (SearchTaskResult.get() != null
+ && SearchTaskResult.get().pageNumber == i)
+ ((MuPDFView) v).setSearchBoxes(SearchTaskResult.get().searchBoxes);
+ else
+ ((MuPDFView) v).setSearchBoxes(null);
+
+ ((MuPDFView) v).setLinkHighlighting(mLinksEnabled);
+
+ ((MuPDFView) v).setChangeReporter(new Runnable() {
+ public void run() {
+ applyToChildren(new ReaderView.ViewMapper() {
+ @Override
+ void applyToView(View view) {
+ ((MuPDFView) view).update();
+ }
+ });
+ }
+ });
+ }
+
+ protected void onMoveToChild(int i) {
+ if (SearchTaskResult.get() != null
+ && SearchTaskResult.get().pageNumber != i) {
+ SearchTaskResult.set(null);
+ resetupChildren();
+ }
+ }
+
+ @Override
+ protected void onMoveOffChild(int i) {
+ View v = getView(i);
+ if (v != null)
+ ((MuPDFView)v).deselectAnnotation();
+ }
+
+ protected void onSettle(View v) {
+ // When the layout has settled ask the page to render
+ // in HQ
+ ((MuPDFView) v).updateHq(false);
+ }
+
+ protected void onUnsettle(View v) {
+ // When something changes making the previous settled view
+ // no longer appropriate, tell the page to remove HQ
+ ((MuPDFView) v).removeHq();
+ }
+
+ @Override
+ protected void onNotInUse(View v) {
+ ((MuPDFView) v).releaseResources();
+ }
+
+ @Override
+ protected void onScaleChild(View v, Float scale) {
+ ((MuPDFView) v).setScale(scale);
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java
new file mode 100644
index 00000000..48625a7e
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java
@@ -0,0 +1,43 @@
+package com.artifex.mupdfdemo;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+public class MuPDFReflowAdapter extends BaseAdapter {
+ private final Context mContext;
+ private final MuPDFCore mCore;
+
+ public MuPDFReflowAdapter(Context c, MuPDFCore core) {
+ mContext = c;
+ mCore = core;
+ }
+
+ public int getCount() {
+ return mCore.countPages();
+ }
+
+ public Object getItem(int arg0) {
+ return null;
+ }
+
+ public long getItemId(int arg0) {
+ return 0;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final MuPDFReflowView reflowView;
+ if (convertView == null) {
+ reflowView = new MuPDFReflowView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight()));
+ } else {
+ reflowView = (MuPDFReflowView) convertView;
+ }
+
+ reflowView.setPage(position, new PointF());
+
+ return reflowView;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowView.java
new file mode 100644
index 00000000..7d41a9b9
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFReflowView.java
@@ -0,0 +1,182 @@
+package com.artifex.mupdfdemo;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.Base64;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+public class MuPDFReflowView extends WebView implements MuPDFView {
+ private final MuPDFCore mCore;
+ private final Handler mHandler;
+ private final Point mParentSize;
+ private int mPage;
+ private float mScale;
+ private int mContentHeight;
+ AsyncTask<Void,Void,byte[]> mLoadHTML;
+
+ public MuPDFReflowView(Context c, MuPDFCore core, Point parentSize) {
+ super(c);
+ mHandler = new Handler();
+ mCore = core;
+ mParentSize = parentSize;
+ mScale = 1.0f;
+ mContentHeight = parentSize.y;
+ getSettings().setJavaScriptEnabled(true);
+ addJavascriptInterface(new Object(){
+ public void reportContentHeight(String value) {
+ mContentHeight = (int)Float.parseFloat(value);
+ mHandler.post(new Runnable() {
+ public void run() {
+ requestLayout();
+ }
+ });
+ }
+ }, "HTMLOUT");
+ setWebViewClient(new WebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ setScale(mScale);
+ }
+ });
+ }
+
+ private void requestHeight() {
+ // Get the webview to report the content height via the interface setup
+ // above. Workaround for getContentHeight not working
+ loadUrl("javascript:elem=document.getElementById('content');window.HTMLOUT.reportContentHeight("+mParentSize.x+"*elem.offsetHeight/elem.offsetWidth)");
+ }
+
+ public void setPage(int page, PointF size) {
+ mPage = page;
+ if (mLoadHTML != null) {
+ mLoadHTML.cancel(true);
+ }
+ mLoadHTML = new AsyncTask<Void,Void,byte[]>() {
+ @Override
+ protected byte[] doInBackground(Void... params) {
+ return mCore.html(mPage);
+ }
+ @Override
+ protected void onPostExecute(byte[] result) {
+ String b64 = Base64.encodeToString(result, Base64.DEFAULT);
+ loadData(b64, "text/html; charset=utf-8", "base64");
+ }
+ };
+ mLoadHTML.execute();
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public void setScale(float scale) {
+ mScale = scale;
+ loadUrl("javascript:document.getElementById('content').style.zoom=\""+(int)(mScale*100)+"%\"");
+ requestHeight();
+ }
+
+ public void blank(int page) {
+ }
+
+ public Hit passClickEvent(float x, float y) {
+ return Hit.Nothing;
+ }
+
+ public LinkInfo hitLink(float x, float y) {
+ return null;
+ }
+
+ public void selectText(float x0, float y0, float x1, float y1) {
+ }
+
+ public void deselectText() {
+ }
+
+ public boolean copySelection() {
+ return false;
+ }
+
+ public boolean markupSelection(Annotation.Type type) {
+ return false;
+ }
+
+ public void startDraw(float x, float y) {
+ }
+
+ public void continueDraw(float x, float y) {
+ }
+
+ public void cancelDraw() {
+ }
+
+ public boolean saveDraw() {
+ return false;
+ }
+
+ public void setSearchBoxes(RectF[] searchBoxes) {
+ }
+
+ public void setLinkHighlighting(boolean f) {
+ }
+
+ public void deleteSelectedAnnotation() {
+ }
+
+ public void deselectAnnotation() {
+ }
+
+ public void setChangeReporter(Runnable reporter) {
+ }
+
+ public void update() {
+ }
+
+ public void updateHq(boolean update) {
+ }
+
+ public void removeHq() {
+ }
+
+ public void releaseResources() {
+ if (mLoadHTML != null) {
+ mLoadHTML.cancel(true);
+ mLoadHTML = null;
+ }
+ }
+
+ public void releaseBitmaps() {
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int x, y;
+ switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
+ case View.MeasureSpec.UNSPECIFIED:
+ x = mParentSize.x;
+ break;
+ default:
+ x = View.MeasureSpec.getSize(widthMeasureSpec);
+ }
+ switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
+ case View.MeasureSpec.UNSPECIFIED:
+ y = mContentHeight;
+ break;
+ default:
+ y = View.MeasureSpec.getSize(heightMeasureSpec);
+ }
+
+ setMeasuredDimension(x, y);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFView.java
new file mode 100644
index 00000000..ec3d3d9a
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/MuPDFView.java
@@ -0,0 +1,33 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+enum Hit {Nothing, Widget, Annotation};
+
+public interface MuPDFView {
+ public void setPage(int page, PointF size);
+ public void setScale(float scale);
+ public int getPage();
+ public void blank(int page);
+ public Hit passClickEvent(float x, float y);
+ public LinkInfo hitLink(float x, float y);
+ public void selectText(float x0, float y0, float x1, float y1);
+ public void deselectText();
+ public boolean copySelection();
+ public boolean markupSelection(Annotation.Type type);
+ public void deleteSelectedAnnotation();
+ public void setSearchBoxes(RectF searchBoxes[]);
+ public void setLinkHighlighting(boolean f);
+ public void deselectAnnotation();
+ public void startDraw(float x, float y);
+ public void continueDraw(float x, float y);
+ public void cancelDraw();
+ public boolean saveDraw();
+ public void setChangeReporter(Runnable reporter);
+ public void update();
+ public void updateHq(boolean update);
+ public void removeHq();
+ public void releaseResources();
+ public void releaseBitmaps();
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivity.java b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivity.java
new file mode 100644
index 00000000..52b0d410
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivity.java
@@ -0,0 +1,31 @@
+package com.artifex.mupdfdemo;
+
+import android.app.ListActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ListView;
+
+public class OutlineActivity extends ListActivity {
+ OutlineItem mItems[];
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mItems = OutlineActivityData.get().items;
+ setListAdapter(new OutlineAdapter(getLayoutInflater(),mItems));
+ // Restore the position within the list from last viewing
+ getListView().setSelection(OutlineActivityData.get().position);
+ getListView().setDividerHeight(0);
+ setResult(-1);
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ OutlineActivityData.get().position = getListView().getFirstVisiblePosition();
+ setResult(mItems[position].page);
+ finish();
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivityData.java b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivityData.java
new file mode 100644
index 00000000..a703e61e
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineActivityData.java
@@ -0,0 +1,17 @@
+package com.artifex.mupdfdemo;
+
+public class OutlineActivityData {
+ public OutlineItem items[];
+ public int position;
+ static private OutlineActivityData singleton;
+
+ static public void set(OutlineActivityData d) {
+ singleton = d;
+ }
+
+ static public OutlineActivityData get() {
+ if (singleton == null)
+ singleton = new OutlineActivityData();
+ return singleton;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineAdapter.java b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineAdapter.java
new file mode 100644
index 00000000..4251ed8e
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineAdapter.java
@@ -0,0 +1,46 @@
+package com.artifex.mupdfdemo;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+public class OutlineAdapter extends BaseAdapter {
+ private final OutlineItem mItems[];
+ private final LayoutInflater mInflater;
+ public OutlineAdapter(LayoutInflater inflater, OutlineItem items[]) {
+ mInflater = inflater;
+ mItems = items;
+ }
+
+ public int getCount() {
+ return mItems.length;
+ }
+
+ public Object getItem(int arg0) {
+ return null;
+ }
+
+ public long getItemId(int arg0) {
+ return 0;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = mInflater.inflate(R.layout.outline_entry, null);
+ } else {
+ v = convertView;
+ }
+ int level = mItems[position].level;
+ if (level > 8) level = 8;
+ String space = "";
+ for (int i=0; i<level;i++)
+ space += " ";
+ ((TextView)v.findViewById(R.id.title)).setText(space+mItems[position].title);
+ ((TextView)v.findViewById(R.id.page)).setText(String.valueOf(mItems[position].page+1));
+ return v;
+ }
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineItem.java b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineItem.java
new file mode 100644
index 00000000..7730991e
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/OutlineItem.java
@@ -0,0 +1,14 @@
+package com.artifex.mupdfdemo;
+
+public class OutlineItem {
+ public final int level;
+ public final String title;
+ public final int page;
+
+ OutlineItem(int _level, String _title, int _page) {
+ level = _level;
+ title = _title;
+ page = _page;
+ }
+
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/PageView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/PageView.java
new file mode 100644
index 00000000..2ee407cb
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/PageView.java
@@ -0,0 +1,698 @@
+package com.artifex.mupdfdemo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+
+// Make our ImageViews opaque to optimize redraw
+class OpaqueImageView extends ImageView {
+
+ public OpaqueImageView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+}
+
+interface TextProcessor {
+ void onStartLine();
+ void onWord(TextWord word);
+ void onEndLine();
+}
+
+class TextSelector {
+ final private TextWord[][] mText;
+ final private RectF mSelectBox;
+
+ public TextSelector(TextWord[][] text, RectF selectBox) {
+ mText = text;
+ mSelectBox = selectBox;
+ }
+
+ public void select(TextProcessor tp) {
+ if (mText == null || mSelectBox == null)
+ return;
+
+ ArrayList<TextWord[]> lines = new ArrayList<TextWord[]>();
+ for (TextWord[] line : mText)
+ if (line[0].bottom > mSelectBox.top && line[0].top < mSelectBox.bottom)
+ lines.add(line);
+
+ Iterator<TextWord[]> it = lines.iterator();
+ while (it.hasNext()) {
+ TextWord[] line = it.next();
+ boolean firstLine = line[0].top < mSelectBox.top;
+ boolean lastLine = line[0].bottom > mSelectBox.bottom;
+ float start = Float.NEGATIVE_INFINITY;
+ float end = Float.POSITIVE_INFINITY;
+
+ if (firstLine && lastLine) {
+ start = Math.min(mSelectBox.left, mSelectBox.right);
+ end = Math.max(mSelectBox.left, mSelectBox.right);
+ } else if (firstLine) {
+ start = mSelectBox.left;
+ } else if (lastLine) {
+ end = mSelectBox.right;
+ }
+
+ tp.onStartLine();
+
+ for (TextWord word : line)
+ if (word.right > start && word.left < end)
+ tp.onWord(word);
+
+ tp.onEndLine();
+ }
+ }
+}
+
+public abstract class PageView extends ViewGroup {
+ private static final int HIGHLIGHT_COLOR = 0x802572AC;
+ private static final int LINK_COLOR = 0x80AC7225;
+ private static final int BOX_COLOR = 0xFF4444FF;
+ private static final int INK_COLOR = 0xFFFF0000;
+ private static final float INK_THICKNESS = 10.0f;
+ private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
+ private static final int PROGRESS_DIALOG_DELAY = 200;
+ protected final Context mContext;
+ protected int mPageNumber;
+ private Point mParentSize;
+ protected Point mSize; // Size of page at minimum zoom
+ protected float mSourceScale;
+
+ private ImageView mEntire; // Image rendered at minimum zoom
+ private Bitmap mEntireBm;
+ private Matrix mEntireMat;
+ private AsyncTask<Void,Void,TextWord[][]> mGetText;
+ private AsyncTask<Void,Void,LinkInfo[]> mGetLinkInfo;
+ private CancellableAsyncTask<Void, Void> mDrawEntire;
+
+ private Point mPatchViewSize; // View size on the basis of which the patch was created
+ private Rect mPatchArea;
+ private ImageView mPatch;
+ private Bitmap mPatchBm;
+ private CancellableAsyncTask<Void,Void> mDrawPatch;
+ private RectF mSearchBoxes[];
+ protected LinkInfo mLinks[];
+ private RectF mSelectBox;
+ private TextWord mText[][];
+ private RectF mItemSelectBox;
+ protected ArrayList<ArrayList<PointF>> mDrawing;
+ private View mSearchView;
+ private boolean mIsBlank;
+ private boolean mHighlightLinks;
+
+ private ProgressBar mBusyIndicator;
+ private final Handler mHandler = new Handler();
+
+ public PageView(Context c, Point parentSize, Bitmap sharedHqBm) {
+ super(c);
+ mContext = c;
+ mParentSize = parentSize;
+ setBackgroundColor(BACKGROUND_COLOR);
+ mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
+ mPatchBm = sharedHqBm;
+ mEntireMat = new Matrix();
+ }
+
+ protected abstract CancellableTaskDefinition<Void, Void> getDrawPageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
+ protected abstract CancellableTaskDefinition<Void, Void> getUpdatePageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
+ protected abstract LinkInfo[] getLinkInfo();
+ protected abstract TextWord[][] getText();
+ protected abstract void addMarkup(PointF[] quadPoints, Annotation.Type type);
+
+ private void reinit() {
+ // Cancel pending render task
+ if (mDrawEntire != null) {
+ mDrawEntire.cancelAndWait();
+ mDrawEntire = null;
+ }
+
+ if (mDrawPatch != null) {
+ mDrawPatch.cancelAndWait();
+ mDrawPatch = null;
+ }
+
+ if (mGetLinkInfo != null) {
+ mGetLinkInfo.cancel(true);
+ mGetLinkInfo = null;
+ }
+
+ if (mGetText != null) {
+ mGetText.cancel(true);
+ mGetText = null;
+ }
+
+ mIsBlank = true;
+ mPageNumber = 0;
+
+ if (mSize == null)
+ mSize = mParentSize;
+
+ if (mEntire != null) {
+ mEntire.setImageBitmap(null);
+ mEntire.invalidate();
+ }
+
+ if (mPatch != null) {
+ mPatch.setImageBitmap(null);
+ mPatch.invalidate();
+ }
+
+ mPatchViewSize = null;
+ mPatchArea = null;
+
+ mSearchBoxes = null;
+ mLinks = null;
+ mSelectBox = null;
+ mText = null;
+ mItemSelectBox = null;
+ }
+
+ public void releaseResources() {
+ reinit();
+
+ if (mBusyIndicator != null) {
+ removeView(mBusyIndicator);
+ mBusyIndicator = null;
+ }
+ }
+
+ public void releaseBitmaps() {
+ reinit();
+
+ // recycle bitmaps before releasing them.
+
+ if (mEntireBm!=null)
+ mEntireBm.recycle();
+ mEntireBm = null;
+
+ if (mPatchBm!=null)
+ mPatchBm.recycle();
+ mPatchBm = null;
+ }
+
+ public void blank(int page) {
+ reinit();
+ mPageNumber = page;
+
+ if (mBusyIndicator == null) {
+ mBusyIndicator = new ProgressBar(mContext);
+ mBusyIndicator.setIndeterminate(true);
+ mBusyIndicator.setBackgroundResource(R.drawable.busy);
+ addView(mBusyIndicator);
+ }
+
+ setBackgroundColor(BACKGROUND_COLOR);
+ }
+
+ public void setPage(int page, PointF size) {
+ // Cancel pending render task
+ if (mDrawEntire != null) {
+ mDrawEntire.cancelAndWait();
+ mDrawEntire = null;
+ }
+
+ mIsBlank = false;
+ // Highlights may be missing because mIsBlank was true on last draw
+ if (mSearchView != null)
+ mSearchView.invalidate();
+
+ mPageNumber = page;
+ if (mEntire == null) {
+ mEntire = new OpaqueImageView(mContext);
+ mEntire.setScaleType(ImageView.ScaleType.MATRIX);
+ addView(mEntire);
+ }
+
+ // Calculate scaled size that fits within the screen limits
+ // This is the size at minimum zoom
+ mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
+ Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
+ mSize = newSize;
+
+ mEntire.setImageBitmap(null);
+ mEntire.invalidate();
+
+ // Get the link info in the background
+ mGetLinkInfo = new AsyncTask<Void,Void,LinkInfo[]>() {
+ protected LinkInfo[] doInBackground(Void... v) {
+ return getLinkInfo();
+ }
+
+ protected void onPostExecute(LinkInfo[] v) {
+ mLinks = v;
+ if (mSearchView != null)
+ mSearchView.invalidate();
+ }
+ };
+
+ mGetLinkInfo.execute();
+
+ // Render the page in the background
+ mDrawEntire = new CancellableAsyncTask<Void, Void>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
+
+ @Override
+ public void onPreExecute() {
+ setBackgroundColor(BACKGROUND_COLOR);
+ mEntire.setImageBitmap(null);
+ mEntire.invalidate();
+
+ if (mBusyIndicator == null) {
+ mBusyIndicator = new ProgressBar(mContext);
+ mBusyIndicator.setIndeterminate(true);
+ mBusyIndicator.setBackgroundResource(R.drawable.busy);
+ addView(mBusyIndicator);
+ mBusyIndicator.setVisibility(INVISIBLE);
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ if (mBusyIndicator != null)
+ mBusyIndicator.setVisibility(VISIBLE);
+ }
+ }, PROGRESS_DIALOG_DELAY);
+ }
+ }
+
+ @Override
+ public void onPostExecute(Void result) {
+ removeView(mBusyIndicator);
+ mBusyIndicator = null;
+ mEntire.setImageBitmap(mEntireBm);
+ mEntire.invalidate();
+ setBackgroundColor(Color.TRANSPARENT);
+
+ }
+ };
+
+ mDrawEntire.execute();
+
+ if (mSearchView == null) {
+ mSearchView = new View(mContext) {
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+ // Work out current total scale factor
+ // from source to view
+ final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ final Paint paint = new Paint();
+
+ if (!mIsBlank && mSearchBoxes != null) {
+ paint.setColor(HIGHLIGHT_COLOR);
+ for (RectF rect : mSearchBoxes)
+ canvas.drawRect(rect.left*scale, rect.top*scale,
+ rect.right*scale, rect.bottom*scale,
+ paint);
+ }
+
+ if (!mIsBlank && mLinks != null && mHighlightLinks) {
+ paint.setColor(LINK_COLOR);
+ for (LinkInfo link : mLinks)
+ canvas.drawRect(link.rect.left*scale, link.rect.top*scale,
+ link.rect.right*scale, link.rect.bottom*scale,
+ paint);
+ }
+
+ if (mSelectBox != null && mText != null) {
+ paint.setColor(HIGHLIGHT_COLOR);
+ processSelectedText(new TextProcessor() {
+ RectF rect;
+
+ public void onStartLine() {
+ rect = new RectF();
+ }
+
+ public void onWord(TextWord word) {
+ rect.union(word);
+ }
+
+ public void onEndLine() {
+ if (!rect.isEmpty())
+ canvas.drawRect(rect.left*scale, rect.top*scale, rect.right*scale, rect.bottom*scale, paint);
+ }
+ });
+ }
+
+ if (mItemSelectBox != null) {
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(BOX_COLOR);
+ canvas.drawRect(mItemSelectBox.left*scale, mItemSelectBox.top*scale, mItemSelectBox.right*scale, mItemSelectBox.bottom*scale, paint);
+ }
+
+ if (mDrawing != null) {
+ Path path = new Path();
+ PointF p;
+
+ paint.setAntiAlias(true);
+ paint.setDither(true);
+ paint.setStrokeJoin(Paint.Join.ROUND);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setStrokeWidth(INK_THICKNESS * scale);
+ paint.setColor(INK_COLOR);
+
+ Iterator<ArrayList<PointF>> it = mDrawing.iterator();
+ while (it.hasNext()) {
+ ArrayList<PointF> arc = it.next();
+ if (arc.size() >= 2) {
+ Iterator<PointF> iit = arc.iterator();
+ p = iit.next();
+ float mX = p.x * scale;
+ float mY = p.y * scale;
+ path.moveTo(mX, mY);
+ while (iit.hasNext()) {
+ p = iit.next();
+ float x = p.x * scale;
+ float y = p.y * scale;
+ path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
+ mX = x;
+ mY = y;
+ }
+ path.lineTo(mX, mY);
+ } else {
+ p = arc.get(0);
+ canvas.drawCircle(p.x * scale, p.y * scale, INK_THICKNESS * scale / 2, paint);
+ }
+ }
+
+ paint.setStyle(Paint.Style.STROKE);
+ canvas.drawPath(path, paint);
+ }
+ }
+ };
+
+ addView(mSearchView);
+ }
+ requestLayout();
+ }
+
+ public void setSearchBoxes(RectF searchBoxes[]) {
+ mSearchBoxes = searchBoxes;
+ if (mSearchView != null)
+ mSearchView.invalidate();
+ }
+
+ public void setLinkHighlighting(boolean f) {
+ mHighlightLinks = f;
+ if (mSearchView != null)
+ mSearchView.invalidate();
+ }
+
+ public void deselectText() {
+ mSelectBox = null;
+ mSearchView.invalidate();
+ }
+
+ public void selectText(float x0, float y0, float x1, float y1) {
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ float docRelX0 = (x0 - getLeft())/scale;
+ float docRelY0 = (y0 - getTop())/scale;
+ float docRelX1 = (x1 - getLeft())/scale;
+ float docRelY1 = (y1 - getTop())/scale;
+ // Order on Y but maintain the point grouping
+ if (docRelY0 <= docRelY1)
+ mSelectBox = new RectF(docRelX0, docRelY0, docRelX1, docRelY1);
+ else
+ mSelectBox = new RectF(docRelX1, docRelY1, docRelX0, docRelY0);
+
+ mSearchView.invalidate();
+
+ if (mGetText == null) {
+ mGetText = new AsyncTask<Void,Void,TextWord[][]>() {
+ @Override
+ protected TextWord[][] doInBackground(Void... params) {
+ return getText();
+ }
+ @Override
+ protected void onPostExecute(TextWord[][] result) {
+ mText = result;
+ mSearchView.invalidate();
+ }
+ };
+
+ mGetText.execute();
+ }
+ }
+
+ public void startDraw(float x, float y) {
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ float docRelX = (x - getLeft())/scale;
+ float docRelY = (y - getTop())/scale;
+ if (mDrawing == null)
+ mDrawing = new ArrayList<ArrayList<PointF>>();
+
+ ArrayList<PointF> arc = new ArrayList<PointF>();
+ arc.add(new PointF(docRelX, docRelY));
+ mDrawing.add(arc);
+ mSearchView.invalidate();
+ }
+
+ public void continueDraw(float x, float y) {
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ float docRelX = (x - getLeft())/scale;
+ float docRelY = (y - getTop())/scale;
+
+ if (mDrawing != null && mDrawing.size() > 0) {
+ ArrayList<PointF> arc = mDrawing.get(mDrawing.size() - 1);
+ arc.add(new PointF(docRelX, docRelY));
+ mSearchView.invalidate();
+ }
+ }
+
+ public void cancelDraw() {
+ mDrawing = null;
+ mSearchView.invalidate();
+ }
+
+ protected PointF[][] getDraw() {
+ if (mDrawing == null)
+ return null;
+
+ PointF[][] path = new PointF[mDrawing.size()][];
+
+ for (int i = 0; i < mDrawing.size(); i++) {
+ ArrayList<PointF> arc = mDrawing.get(i);
+ path[i] = arc.toArray(new PointF[arc.size()]);
+ }
+
+ return path;
+ }
+
+ protected void processSelectedText(TextProcessor tp) {
+ (new TextSelector(mText, mSelectBox)).select(tp);
+ }
+
+ public void setItemSelectBox(RectF rect) {
+ mItemSelectBox = rect;
+ if (mSearchView != null)
+ mSearchView.invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int x, y;
+ switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
+ case View.MeasureSpec.UNSPECIFIED:
+ x = mSize.x;
+ break;
+ default:
+ x = View.MeasureSpec.getSize(widthMeasureSpec);
+ }
+ switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
+ case View.MeasureSpec.UNSPECIFIED:
+ y = mSize.y;
+ break;
+ default:
+ y = View.MeasureSpec.getSize(heightMeasureSpec);
+ }
+
+ setMeasuredDimension(x, y);
+
+ if (mBusyIndicator != null) {
+ int limit = Math.min(mParentSize.x, mParentSize.y)/2;
+ mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int w = right-left;
+ int h = bottom-top;
+
+ if (mEntire != null) {
+ if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
+ mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
+ mEntire.setImageMatrix(mEntireMat);
+ mEntire.invalidate();
+ }
+ mEntire.layout(0, 0, w, h);
+ }
+
+ if (mSearchView != null) {
+ mSearchView.layout(0, 0, w, h);
+ }
+
+ if (mPatchViewSize != null) {
+ if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
+ // Zoomed since patch was created
+ mPatchViewSize = null;
+ mPatchArea = null;
+ if (mPatch != null) {
+ mPatch.setImageBitmap(null);
+ mPatch.invalidate();
+ }
+ } else {
+ mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
+ }
+ }
+
+ if (mBusyIndicator != null) {
+ int bw = mBusyIndicator.getMeasuredWidth();
+ int bh = mBusyIndicator.getMeasuredHeight();
+
+ mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
+ }
+ }
+
+ public void updateHq(boolean update) {
+ Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
+ if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
+ // If the viewArea's size matches the unzoomed size, there is no need for an hq patch
+ if (mPatch != null) {
+ mPatch.setImageBitmap(null);
+ mPatch.invalidate();
+ }
+ } else {
+ final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
+ final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
+
+ // Intersect and test that there is an intersection
+ if (!patchArea.intersect(viewArea))
+ return;
+
+ // Offset patch area to be relative to the view top left
+ patchArea.offset(-viewArea.left, -viewArea.top);
+
+ boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
+
+ // If being asked for the same area as last time and not because of an update then nothing to do
+ if (area_unchanged && !update)
+ return;
+
+ boolean completeRedraw = !(area_unchanged && update);
+
+ // Stop the drawing of previous patch if still going
+ if (mDrawPatch != null) {
+ mDrawPatch.cancelAndWait();
+ mDrawPatch = null;
+ }
+
+ // Create and add the image view if not already done
+ if (mPatch == null) {
+ mPatch = new OpaqueImageView(mContext);
+ mPatch.setScaleType(ImageView.ScaleType.MATRIX);
+ addView(mPatch);
+ mSearchView.bringToFront();
+ }
+
+ CancellableTaskDefinition<Void, Void> task;
+
+ if (completeRedraw)
+ task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
+ patchArea.left, patchArea.top,
+ patchArea.width(), patchArea.height());
+ else
+ task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
+ patchArea.left, patchArea.top,
+ patchArea.width(), patchArea.height());
+
+ mDrawPatch = new CancellableAsyncTask<Void,Void>(task) {
+
+ public void onPostExecute(Void result) {
+ mPatchViewSize = patchViewSize;
+ mPatchArea = patchArea;
+ mPatch.setImageBitmap(mPatchBm);
+ mPatch.invalidate();
+ //requestLayout();
+ // Calling requestLayout here doesn't lead to a later call to layout. No idea
+ // why, but apparently others have run into the problem.
+ mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
+ }
+ };
+
+ mDrawPatch.execute();
+ }
+ }
+
+ public void update() {
+ // Cancel pending render task
+ if (mDrawEntire != null) {
+ mDrawEntire.cancelAndWait();
+ mDrawEntire = null;
+ }
+
+ if (mDrawPatch != null) {
+ mDrawPatch.cancelAndWait();
+ mDrawPatch = null;
+ }
+
+
+ // Render the page in the background
+ mDrawEntire = new CancellableAsyncTask<Void, Void>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
+
+ public void onPostExecute(Void result) {
+ mEntire.setImageBitmap(mEntireBm);
+ mEntire.invalidate();
+ }
+ };
+
+ mDrawEntire.execute();
+
+ updateHq(true);
+ }
+
+ public void removeHq() {
+ // Stop the drawing of the patch if still going
+ if (mDrawPatch != null) {
+ mDrawPatch.cancelAndWait();
+ mDrawPatch = null;
+ }
+
+ // And get rid of it
+ mPatchViewSize = null;
+ mPatchArea = null;
+ if (mPatch != null) {
+ mPatch.setImageBitmap(null);
+ mPatch.invalidate();
+ }
+ }
+
+ public int getPage() {
+ return mPageNumber;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/PrintDialogActivity.java b/platform/android/viewer/src/com/artifex/mupdfdemo/PrintDialogActivity.java
new file mode 100644
index 00000000..d96322d5
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/PrintDialogActivity.java
@@ -0,0 +1,145 @@
+package com.artifex.mupdfdemo;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Base64;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+public class PrintDialogActivity extends Activity {
+ private static final String PRINT_DIALOG_URL = "https://www.google.com/cloudprint/dialog.html";
+ private static final String JS_INTERFACE = "AndroidPrintDialog";
+ private static final String CONTENT_TRANSFER_ENCODING = "base64";
+
+ private static final String ZXING_URL = "http://zxing.appspot.com";
+ private static final int ZXING_SCAN_REQUEST = 65743;
+
+ /**
+ * Post message that is sent by Print Dialog web page when the printing dialog
+ * needs to be closed.
+ */
+ private static final String CLOSE_POST_MESSAGE_NAME = "cp-dialog-on-close";
+
+ /**
+ * Web view element to show the printing dialog in.
+ */
+ private WebView dialogWebView;
+
+ /**
+ * Intent that started the action.
+ */
+ Intent cloudPrintIntent;
+
+ private int resultCode;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resultCode = RESULT_OK;
+ setContentView(R.layout.print_dialog);
+ dialogWebView = (WebView) findViewById(R.id.webview);
+ cloudPrintIntent = this.getIntent();
+
+ WebSettings settings = dialogWebView.getSettings();
+ settings.setJavaScriptEnabled(true);
+
+ dialogWebView.setWebViewClient(new PrintDialogWebClient());
+ dialogWebView.addJavascriptInterface(
+ new PrintDialogJavaScriptInterface(), JS_INTERFACE);
+
+ dialogWebView.loadUrl(PRINT_DIALOG_URL);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == ZXING_SCAN_REQUEST && resultCode == RESULT_OK) {
+ dialogWebView.loadUrl(intent.getStringExtra("SCAN_RESULT"));
+ }
+ }
+
+ final class PrintDialogJavaScriptInterface {
+ public String getType() {
+ return cloudPrintIntent.getType();
+ }
+
+ public String getTitle() {
+ return cloudPrintIntent.getExtras().getString("title");
+ }
+
+ public String getContent() {
+ try {
+ ContentResolver contentResolver = getContentResolver();
+ InputStream is = contentResolver.openInputStream(cloudPrintIntent.getData());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ byte[] buffer = new byte[4096];
+ int n = is.read(buffer);
+ while (n >= 0) {
+ baos.write(buffer, 0, n);
+ n = is.read(buffer);
+ }
+ is.close();
+ baos.flush();
+
+ return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT);
+ } catch (Throwable e) {
+ resultCode = RESULT_CANCELED;
+ setResult(resultCode);
+ finish();
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ public String getEncoding() {
+ return CONTENT_TRANSFER_ENCODING;
+ }
+
+ public void onPostMessage(String message) {
+ if (message.startsWith(CLOSE_POST_MESSAGE_NAME)) {
+ setResult(resultCode);
+ finish();
+ }
+ }
+ }
+
+ private final class PrintDialogWebClient extends WebViewClient {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url.startsWith(ZXING_URL)) {
+ Intent intentScan = new Intent("com.google.zxing.client.android.SCAN");
+ intentScan.putExtra("SCAN_MODE", "QR_CODE_MODE");
+ try {
+ startActivityForResult(intentScan, ZXING_SCAN_REQUEST);
+ } catch (ActivityNotFoundException error) {
+ view.loadUrl(url);
+ }
+ } else {
+ view.loadUrl(url);
+ }
+ return false;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ if (PRINT_DIALOG_URL.equals(url)) {
+ // Submit print document.
+ view.loadUrl("javascript:printDialog.setPrintDocument(printDialog.createPrintDocument("
+ + "window." + JS_INTERFACE + ".getType(),window." + JS_INTERFACE + ".getTitle(),"
+ + "window." + JS_INTERFACE + ".getContent(),window." + JS_INTERFACE + ".getEncoding()))");
+
+ // Add post messages listener.
+ view.loadUrl("javascript:window.addEventListener('message',"
+ + "function(evt){window." + JS_INTERFACE + ".onPostMessage(evt.data)}, false)");
+ }
+ }
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/ReaderView.java b/platform/android/viewer/src/com/artifex/mupdfdemo/ReaderView.java
new file mode 100644
index 00000000..65d8f665
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/ReaderView.java
@@ -0,0 +1,936 @@
+package com.artifex.mupdfdemo;
+
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.Scroller;
+
+public class ReaderView
+ extends AdapterView<Adapter>
+ implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener, Runnable {
+ private static final int MOVING_DIAGONALLY = 0;
+ private static final int MOVING_LEFT = 1;
+ private static final int MOVING_RIGHT = 2;
+ private static final int MOVING_UP = 3;
+ private static final int MOVING_DOWN = 4;
+
+ private static final int FLING_MARGIN = 100;
+ private static final int GAP = 20;
+
+ private static final float MIN_SCALE = 1.0f;
+ private static final float MAX_SCALE = 5.0f;
+ private static final float REFLOW_SCALE_FACTOR = 0.5f;
+
+ private static final boolean HORIZONTAL_SCROLLING = true;
+
+ private Adapter mAdapter;
+ private int mCurrent; // Adapter's index for the current view
+ private boolean mResetLayout;
+ private final SparseArray<View>
+ mChildViews = new SparseArray<View>(3);
+ // Shadows the children of the adapter view
+ // but with more sensible indexing
+ private final LinkedList<View>
+ mViewCache = new LinkedList<View>();
+ private boolean mUserInteracting; // Whether the user is interacting
+ private boolean mScaling; // Whether the user is currently pinch zooming
+ private float mScale = 1.0f;
+ private int mXScroll; // Scroll amounts recorded from events.
+ private int mYScroll; // and then accounted for in onLayout
+ private boolean mReflow = false;
+ private boolean mReflowChanged = false;
+ private final GestureDetector
+ mGestureDetector;
+ private final ScaleGestureDetector
+ mScaleGestureDetector;
+ private final Scroller mScroller;
+ private final Stepper mStepper;
+ private int mScrollerLastX;
+ private int mScrollerLastY;
+ private float mLastScaleFocusX;
+ private float mLastScaleFocusY;
+
+ static abstract class ViewMapper {
+ abstract void applyToView(View view);
+ }
+
+ public ReaderView(Context context) {
+ super(context);
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ mStepper = new Stepper(this, this);
+ }
+
+ public ReaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
+ // is instantiated in the IDE, so we need to be a bit careful what we do).
+ if (isInEditMode())
+ {
+ mGestureDetector = null;
+ mScaleGestureDetector = null;
+ mScroller = null;
+ mStepper = null;
+ }
+ else
+ {
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ mStepper = new Stepper(this, this);
+ }
+ }
+
+ public ReaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ mStepper = new Stepper(this, this);
+ }
+
+ public int getDisplayedViewIndex() {
+ return mCurrent;
+ }
+
+ public void setDisplayedViewIndex(int i) {
+ if (0 <= i && i < mAdapter.getCount()) {
+ onMoveOffChild(mCurrent);
+ mCurrent = i;
+ onMoveToChild(i);
+ mResetLayout = true;
+ requestLayout();
+ }
+ }
+
+ public void moveToNext() {
+ View v = mChildViews.get(mCurrent+1);
+ if (v != null)
+ slideViewOntoScreen(v);
+ }
+
+ public void moveToPrevious() {
+ View v = mChildViews.get(mCurrent-1);
+ if (v != null)
+ slideViewOntoScreen(v);
+ }
+
+ // When advancing down the page, we want to advance by about
+ // 90% of a screenful. But we'd be happy to advance by between
+ // 80% and 95% if it means we hit the bottom in a whole number
+ // of steps.
+ private int smartAdvanceAmount(int screenHeight, int max) {
+ int advance = (int)(screenHeight * 0.9 + 0.5);
+ int leftOver = max % advance;
+ int steps = max / advance;
+ if (leftOver == 0) {
+ // We'll make it exactly. No adjustment
+ } else if ((float)leftOver / steps <= screenHeight * 0.05) {
+ // We can adjust up by less than 5% to make it exact.
+ advance += (int)((float)leftOver/steps + 0.5);
+ } else {
+ int overshoot = advance - leftOver;
+ if ((float)overshoot / steps <= screenHeight * 0.1) {
+ // We can adjust down by less than 10% to make it exact.
+ advance -= (int)((float)overshoot/steps + 0.5);
+ }
+ }
+ if (advance > max)
+ advance = max;
+ return advance;
+ }
+
+ public void smartMoveForwards() {
+ View v = mChildViews.get(mCurrent);
+ if (v == null)
+ return;
+
+ // The following code works in terms of where the screen is on the views;
+ // so for example, if the currentView is at (-100,-100), the visible
+ // region would be at (100,100). If the previous page was (2000, 3000) in
+ // size, the visible region of the previous page might be (2100 + GAP, 100)
+ // (i.e. off the previous page). This is different to the way the rest of
+ // the code in this file is written, but it's easier for me to think about.
+ // At some point we may refactor this to fit better with the rest of the
+ // code.
+
+ // screenWidth/Height are the actual width/height of the screen. e.g. 480/800
+ int screenWidth = getWidth();
+ int screenHeight = getHeight();
+ // We might be mid scroll; we want to calculate where we scroll to based on
+ // where this scroll would end, not where we are now (to allow for people
+ // bashing 'forwards' very fast.
+ int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
+ int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
+ // right/bottom is in terms of pixels within the scaled document; e.g. 1000
+ int top = -(v.getTop() + mYScroll + remainingY);
+ int right = screenWidth -(v.getLeft() + mXScroll + remainingX);
+ int bottom = screenHeight+top;
+ // docWidth/Height are the width/height of the scaled document e.g. 2000x3000
+ int docWidth = v.getMeasuredWidth();
+ int docHeight = v.getMeasuredHeight();
+
+ int xOffset, yOffset;
+ if (bottom >= docHeight) {
+ // We are flush with the bottom. Advance to next column.
+ if (right + screenWidth > docWidth) {
+ // No room for another column - go to next page
+ View nv = mChildViews.get(mCurrent+1);
+ if (nv == null) // No page to advance to
+ return;
+ int nextTop = -(nv.getTop() + mYScroll + remainingY);
+ int nextLeft = -(nv.getLeft() + mXScroll + remainingX);
+ int nextDocWidth = nv.getMeasuredWidth();
+ int nextDocHeight = nv.getMeasuredHeight();
+
+ // Allow for the next page maybe being shorter than the screen is high
+ yOffset = (nextDocHeight < screenHeight ? ((nextDocHeight - screenHeight)>>1) : 0);
+
+ if (nextDocWidth < screenWidth) {
+ // Next page is too narrow to fill the screen. Scroll to the top, centred.
+ xOffset = (nextDocWidth - screenWidth)>>1;
+ } else {
+ // Reset X back to the left hand column
+ xOffset = right % screenWidth;
+ // Adjust in case the previous page is less wide
+ if (xOffset + screenWidth > nextDocWidth)
+ xOffset = nextDocWidth - screenWidth;
+ }
+ xOffset -= nextLeft;
+ yOffset -= nextTop;
+ } else {
+ // Move to top of next column
+ xOffset = screenWidth;
+ yOffset = screenHeight - bottom;
+ }
+ } else {
+ // Advance by 90% of the screen height downwards (in case lines are partially cut off)
+ xOffset = 0;
+ yOffset = smartAdvanceAmount(screenHeight, docHeight - bottom);
+ }
+ mScrollerLastX = mScrollerLastY = 0;
+ mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
+ mStepper.prod();
+ }
+
+ public void smartMoveBackwards() {
+ View v = mChildViews.get(mCurrent);
+ if (v == null)
+ return;
+
+ // The following code works in terms of where the screen is on the views;
+ // so for example, if the currentView is at (-100,-100), the visible
+ // region would be at (100,100). If the previous page was (2000, 3000) in
+ // size, the visible region of the previous page might be (2100 + GAP, 100)
+ // (i.e. off the previous page). This is different to the way the rest of
+ // the code in this file is written, but it's easier for me to think about.
+ // At some point we may refactor this to fit better with the rest of the
+ // code.
+
+ // screenWidth/Height are the actual width/height of the screen. e.g. 480/800
+ int screenWidth = getWidth();
+ int screenHeight = getHeight();
+ // We might be mid scroll; we want to calculate where we scroll to based on
+ // where this scroll would end, not where we are now (to allow for people
+ // bashing 'forwards' very fast.
+ int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
+ int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
+ // left/top is in terms of pixels within the scaled document; e.g. 1000
+ int left = -(v.getLeft() + mXScroll + remainingX);
+ int top = -(v.getTop() + mYScroll + remainingY);
+ // docWidth/Height are the width/height of the scaled document e.g. 2000x3000
+ int docHeight = v.getMeasuredHeight();
+
+ int xOffset, yOffset;
+ if (top <= 0) {
+ // We are flush with the top. Step back to previous column.
+ if (left < screenWidth) {
+ /* No room for previous column - go to previous page */
+ View pv = mChildViews.get(mCurrent-1);
+ if (pv == null) /* No page to advance to */
+ return;
+ int prevDocWidth = pv.getMeasuredWidth();
+ int prevDocHeight = pv.getMeasuredHeight();
+
+ // Allow for the next page maybe being shorter than the screen is high
+ yOffset = (prevDocHeight < screenHeight ? ((prevDocHeight - screenHeight)>>1) : 0);
+
+ int prevLeft = -(pv.getLeft() + mXScroll);
+ int prevTop = -(pv.getTop() + mYScroll);
+ if (prevDocWidth < screenWidth) {
+ // Previous page is too narrow to fill the screen. Scroll to the bottom, centred.
+ xOffset = (prevDocWidth - screenWidth)>>1;
+ } else {
+ // Reset X back to the right hand column
+ xOffset = (left > 0 ? left % screenWidth : 0);
+ if (xOffset + screenWidth > prevDocWidth)
+ xOffset = prevDocWidth - screenWidth;
+ while (xOffset + screenWidth*2 < prevDocWidth)
+ xOffset += screenWidth;
+ }
+ xOffset -= prevLeft;
+ yOffset -= prevTop-prevDocHeight+screenHeight;
+ } else {
+ // Move to bottom of previous column
+ xOffset = -screenWidth;
+ yOffset = docHeight - screenHeight + top;
+ }
+ } else {
+ // Retreat by 90% of the screen height downwards (in case lines are partially cut off)
+ xOffset = 0;
+ yOffset = -smartAdvanceAmount(screenHeight, top);
+ }
+ mScrollerLastX = mScrollerLastY = 0;
+ mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
+ mStepper.prod();
+ }
+
+ public void resetupChildren() {
+ for (int i = 0; i < mChildViews.size(); i++)
+ onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
+ }
+
+ public void applyToChildren(ViewMapper mapper) {
+ for (int i = 0; i < mChildViews.size(); i++)
+ mapper.applyToView(mChildViews.valueAt(i));
+ }
+
+ public void refresh(boolean reflow) {
+ mReflow = reflow;
+ mReflowChanged = true;
+ mResetLayout = true;
+
+ mScale = 1.0f;
+ mXScroll = mYScroll = 0;
+
+ requestLayout();
+ }
+
+ protected void onChildSetup(int i, View v) {}
+
+ protected void onMoveToChild(int i) {}
+
+ protected void onMoveOffChild(int i) {}
+
+ protected void onSettle(View v) {};
+
+ protected void onUnsettle(View v) {};
+
+ protected void onNotInUse(View v) {};
+
+ protected void onScaleChild(View v, Float scale) {};
+
+ public View getView(int i) {
+ return mChildViews.get(i);
+ }
+
+ public View getDisplayedView() {
+ return mChildViews.get(mCurrent);
+ }
+
+ public void run() {
+ if (!mScroller.isFinished()) {
+ mScroller.computeScrollOffset();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ mXScroll += x - mScrollerLastX;
+ mYScroll += y - mScrollerLastY;
+ mScrollerLastX = x;
+ mScrollerLastY = y;
+ requestLayout();
+ mStepper.prod();
+ }
+ else if (!mUserInteracting) {
+ // End of an inertial scroll and the user is not interacting.
+ // The layout is stable
+ View v = mChildViews.get(mCurrent);
+ if (v != null)
+ postSettle(v);
+ }
+ }
+
+ public boolean onDown(MotionEvent arg0) {
+ mScroller.forceFinished(true);
+ return true;
+ }
+
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (mScaling)
+ return true;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ Rect bounds = getScrollBounds(v);
+ switch(directionOfTravel(velocityX, velocityY)) {
+ case MOVING_LEFT:
+ if (HORIZONTAL_SCROLLING && bounds.left >= 0) {
+ // Fling off to the left bring next view onto screen
+ View vl = mChildViews.get(mCurrent+1);
+
+ if (vl != null) {
+ slideViewOntoScreen(vl);
+ return true;
+ }
+ }
+ break;
+ case MOVING_UP:
+ if (!HORIZONTAL_SCROLLING && bounds.top >= 0) {
+ // Fling off to the top bring next view onto screen
+ View vl = mChildViews.get(mCurrent+1);
+
+ if (vl != null) {
+ slideViewOntoScreen(vl);
+ return true;
+ }
+ }
+ break;
+ case MOVING_RIGHT:
+ if (HORIZONTAL_SCROLLING && bounds.right <= 0) {
+ // Fling off to the right bring previous view onto screen
+ View vr = mChildViews.get(mCurrent-1);
+
+ if (vr != null) {
+ slideViewOntoScreen(vr);
+ return true;
+ }
+ }
+ break;
+ case MOVING_DOWN:
+ if (!HORIZONTAL_SCROLLING && bounds.bottom <= 0) {
+ // Fling off to the bottom bring previous view onto screen
+ View vr = mChildViews.get(mCurrent-1);
+
+ if (vr != null) {
+ slideViewOntoScreen(vr);
+ return true;
+ }
+ }
+ break;
+ }
+ mScrollerLastX = mScrollerLastY = 0;
+ // If the page has been dragged out of bounds then we want to spring back
+ // nicely. fling jumps back into bounds instantly, so we don't want to use
+ // fling in that case. On the other hand, we don't want to forgo a fling
+ // just because of a slightly off-angle drag taking us out of bounds other
+ // than in the direction of the drag, so we test for out of bounds only
+ // in the direction of travel.
+ //
+ // Also don't fling if out of bounds in any direction by more than fling
+ // margin
+ Rect expandedBounds = new Rect(bounds);
+ expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
+
+ if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
+ && expandedBounds.contains(0, 0)) {
+ mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
+ mStepper.prod();
+ }
+ }
+
+ return true;
+ }
+
+ public void onLongPress(MotionEvent e) {
+ }
+
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ if (!mScaling) {
+ mXScroll -= distanceX;
+ mYScroll -= distanceY;
+ requestLayout();
+ }
+ return true;
+ }
+
+ public void onShowPress(MotionEvent e) {
+ }
+
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ float previousScale = mScale;
+ float scale_factor = mReflow ? REFLOW_SCALE_FACTOR : 1.0f;
+ float min_scale = MIN_SCALE * scale_factor;
+ float max_scale = MAX_SCALE * scale_factor;
+ mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), min_scale), max_scale);
+
+ if (mReflow) {
+ View v = mChildViews.get(mCurrent);
+ if (v != null)
+ onScaleChild(v, mScale);
+ } else {
+ float factor = mScale/previousScale;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ float currentFocusX = detector.getFocusX();
+ float currentFocusY = detector.getFocusY();
+ // Work out the focus point relative to the view top left
+ int viewFocusX = (int)currentFocusX - (v.getLeft() + mXScroll);
+ int viewFocusY = (int)currentFocusY - (v.getTop() + mYScroll);
+ // Scroll to maintain the focus point
+ mXScroll += viewFocusX - viewFocusX * factor;
+ mYScroll += viewFocusY - viewFocusY * factor;
+
+ if (mLastScaleFocusX>=0)
+ mXScroll+=currentFocusX-mLastScaleFocusX;
+ if (mLastScaleFocusY>=0)
+ mYScroll+=currentFocusY-mLastScaleFocusY;
+
+ mLastScaleFocusX=currentFocusX;
+ mLastScaleFocusY=currentFocusY;
+ requestLayout();
+ }
+ }
+ return true;
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ mScaling = true;
+ // Ignore any scroll amounts yet to be accounted for: the
+ // screen is not showing the effect of them, so they can
+ // only confuse the user
+ mXScroll = mYScroll = 0;
+ mLastScaleFocusX = mLastScaleFocusY = -1;
+ return true;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ if (mReflow) {
+ applyToChildren(new ViewMapper() {
+ @Override
+ void applyToView(View view) {
+ onScaleChild(view, mScale);
+ }
+ });
+ }
+ mScaling = false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ mScaleGestureDetector.onTouchEvent(event);
+ mGestureDetector.onTouchEvent(event);
+
+ if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+ mUserInteracting = true;
+ }
+ if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+ mUserInteracting = false;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ if (mScroller.isFinished()) {
+ // If, at the end of user interaction, there is no
+ // current inertial scroll in operation then animate
+ // the view onto screen if necessary
+ slideViewOntoScreen(v);
+ }
+
+ if (mScroller.isFinished()) {
+ // If still there is no inertial scroll in operation
+ // then the layout is stable
+ postSettle(v);
+ }
+ }
+ }
+
+ requestLayout();
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int n = getChildCount();
+ for (int i = 0; i < n; i++)
+ measureView(getChildAt(i));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ try {
+ onLayout2(changed, left, top, right, bottom);
+ }
+ catch (java.lang.OutOfMemoryError e) {
+ System.out.println("Out of memory during layout");
+
+ // we might get an out of memory error.
+ // so let's display an alert.
+ // TODO: a better message, in resources.
+
+ if (!memAlert) {
+ memAlert = true;
+ AlertDialog alertDialog = MuPDFActivity.getAlertBuilder().create();
+ alertDialog.setMessage("Out of memory during layout");
+ alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ memAlert = false;
+ }
+ });
+ alertDialog.show();
+ }
+ }
+ }
+
+ private boolean memAlert = false;
+
+ private void onLayout2(boolean changed, int left, int top, int right,
+ int bottom) {
+
+ // "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
+ // is instantiated in the IDE, so we need to be a bit careful what we do).
+ if (isInEditMode())
+ return;
+
+ View cv = mChildViews.get(mCurrent);
+ Point cvOffset;
+
+ if (!mResetLayout) {
+ // Move to next or previous if current is sufficiently off center
+ if (cv != null) {
+ boolean move;
+ cvOffset = subScreenSizeOffset(cv);
+ // cv.getRight() may be out of date with the current scale
+ // so add left to the measured width for the correct position
+ if (HORIZONTAL_SCROLLING)
+ move = cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2;
+ else
+ move = cv.getTop() + cv.getMeasuredHeight() + cvOffset.y + GAP/2 + mYScroll < getHeight()/2;
+ if (move && mCurrent + 1 < mAdapter.getCount()) {
+ postUnsettle(cv);
+ // post to invoke test for end of animation
+ // where we must set hq area for the new current view
+ mStepper.prod();
+
+ onMoveOffChild(mCurrent);
+ mCurrent++;
+ onMoveToChild(mCurrent);
+ }
+
+ if (HORIZONTAL_SCROLLING)
+ move = cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2;
+ else
+ move = cv.getTop() - cvOffset.y - GAP/2 + mYScroll >= getHeight()/2;
+ if (move && mCurrent > 0) {
+ postUnsettle(cv);
+ // post to invoke test for end of animation
+ // where we must set hq area for the new current view
+ mStepper.prod();
+
+ onMoveOffChild(mCurrent);
+ mCurrent--;
+ onMoveToChild(mCurrent);
+ }
+ }
+
+ // Remove not needed children and hold them for reuse
+ int numChildren = mChildViews.size();
+ int childIndices[] = new int[numChildren];
+ for (int i = 0; i < numChildren; i++)
+ childIndices[i] = mChildViews.keyAt(i);
+
+ for (int i = 0; i < numChildren; i++) {
+ int ai = childIndices[i];
+ if (ai < mCurrent - 1 || ai > mCurrent + 1) {
+ View v = mChildViews.get(ai);
+ onNotInUse(v);
+ mViewCache.add(v);
+ removeViewInLayout(v);
+ mChildViews.remove(ai);
+ }
+ }
+ } else {
+ mResetLayout = false;
+ mXScroll = mYScroll = 0;
+
+ // Remove all children and hold them for reuse
+ int numChildren = mChildViews.size();
+ for (int i = 0; i < numChildren; i++) {
+ View v = mChildViews.valueAt(i);
+ onNotInUse(v);
+ mViewCache.add(v);
+ removeViewInLayout(v);
+ }
+ mChildViews.clear();
+
+ // Don't reuse cached views if the adapter has changed
+ if (mReflowChanged) {
+ mReflowChanged = false;
+ mViewCache.clear();
+ }
+
+ // post to ensure generation of hq area
+ mStepper.prod();
+ }
+
+ // Ensure current view is present
+ int cvLeft, cvRight, cvTop, cvBottom;
+ boolean notPresent = (mChildViews.get(mCurrent) == null);
+ cv = getOrCreateChild(mCurrent);
+ // When the view is sub-screen-size in either dimension we
+ // offset it to center within the screen area, and to keep
+ // the views spaced out
+ cvOffset = subScreenSizeOffset(cv);
+ if (notPresent) {
+ //Main item not already present. Just place it top left
+ cvLeft = cvOffset.x;
+ cvTop = cvOffset.y;
+ } else {
+ // Main item already present. Adjust by scroll offsets
+ cvLeft = cv.getLeft() + mXScroll;
+ cvTop = cv.getTop() + mYScroll;
+ }
+ // Scroll values have been accounted for
+ mXScroll = mYScroll = 0;
+ cvRight = cvLeft + cv.getMeasuredWidth();
+ cvBottom = cvTop + cv.getMeasuredHeight();
+
+ if (!mUserInteracting && mScroller.isFinished()) {
+ Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+ cvRight += corr.x;
+ cvLeft += corr.x;
+ cvTop += corr.y;
+ cvBottom += corr.y;
+ } else if (HORIZONTAL_SCROLLING && cv.getMeasuredHeight() <= getHeight()) {
+ // When the current view is as small as the screen in height, clamp
+ // it vertically
+ Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+ cvTop += corr.y;
+ cvBottom += corr.y;
+ } else if (!HORIZONTAL_SCROLLING && cv.getMeasuredWidth() <= getWidth()) {
+ // When the current view is as small as the screen in width, clamp
+ // it horizontally
+ Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+ cvRight += corr.x;
+ cvLeft += corr.x;
+ }
+
+ cv.layout(cvLeft, cvTop, cvRight, cvBottom);
+
+ if (mCurrent > 0) {
+ View lv = getOrCreateChild(mCurrent - 1);
+ Point leftOffset = subScreenSizeOffset(lv);
+ if (HORIZONTAL_SCROLLING)
+ {
+ int gap = leftOffset.x + GAP + cvOffset.x;
+ lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
+ (cvBottom + cvTop - lv.getMeasuredHeight())/2,
+ cvLeft - gap,
+ (cvBottom + cvTop + lv.getMeasuredHeight())/2);
+ } else {
+ int gap = leftOffset.y + GAP + cvOffset.y;
+ lv.layout((cvLeft + cvRight - lv.getMeasuredWidth())/2,
+ cvTop - lv.getMeasuredHeight() - gap,
+ (cvLeft + cvRight + lv.getMeasuredWidth())/2,
+ cvTop - gap);
+ }
+ }
+
+ if (mCurrent + 1 < mAdapter.getCount()) {
+ View rv = getOrCreateChild(mCurrent + 1);
+ Point rightOffset = subScreenSizeOffset(rv);
+ if (HORIZONTAL_SCROLLING)
+ {
+ int gap = cvOffset.x + GAP + rightOffset.x;
+ rv.layout(cvRight + gap,
+ (cvBottom + cvTop - rv.getMeasuredHeight())/2,
+ cvRight + rv.getMeasuredWidth() + gap,
+ (cvBottom + cvTop + rv.getMeasuredHeight())/2);
+ } else {
+ int gap = cvOffset.y + GAP + rightOffset.y;
+ rv.layout((cvLeft + cvRight - rv.getMeasuredWidth())/2,
+ cvBottom + gap,
+ (cvLeft + cvRight + rv.getMeasuredWidth())/2,
+ cvBottom + gap + rv.getMeasuredHeight());
+ }
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getSelectedView() {
+ return null;
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+
+ // release previous adapter's bitmaps
+ if (null!=mAdapter && adapter!=mAdapter) {
+ if (adapter instanceof MuPDFPageAdapter){
+ ((MuPDFPageAdapter) adapter).releaseBitmaps();
+ }
+ }
+
+ mAdapter = adapter;
+
+ requestLayout();
+ }
+
+ @Override
+ public void setSelection(int arg0) {
+ throw new UnsupportedOperationException(getContext().getString(R.string.not_supported));
+ }
+
+ private View getCached() {
+ if (mViewCache.size() == 0)
+ return null;
+ else
+ return mViewCache.removeFirst();
+ }
+
+ private View getOrCreateChild(int i) {
+ View v = mChildViews.get(i);
+ if (v == null) {
+ v = mAdapter.getView(i, getCached(), this);
+ addAndMeasureChild(i, v);
+ onChildSetup(i, v);
+ onScaleChild(v, mScale);
+ }
+
+ return v;
+ }
+
+ private void addAndMeasureChild(int i, View v) {
+ LayoutParams params = v.getLayoutParams();
+ if (params == null) {
+ params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+ addViewInLayout(v, 0, params, true);
+ mChildViews.append(i, v); // Record the view against it's adapter index
+ measureView(v);
+ }
+
+ private void measureView(View v) {
+ // See what size the view wants to be
+ v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+
+ if (!mReflow) {
+ // Work out a scale that will fit it to this view
+ float scale = Math.min((float)getWidth()/(float)v.getMeasuredWidth(),
+ (float)getHeight()/(float)v.getMeasuredHeight());
+ // Use the fitting values scaled by our current scale factor
+ v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale),
+ View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale));
+ } else {
+ v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()),
+ View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()));
+ }
+ }
+
+ private Rect getScrollBounds(int left, int top, int right, int bottom) {
+ int xmin = getWidth() - right;
+ int xmax = -left;
+ int ymin = getHeight() - bottom;
+ int ymax = -top;
+
+ // In either dimension, if view smaller than screen then
+ // constrain it to be central
+ if (xmin > xmax) xmin = xmax = (xmin + xmax)/2;
+ if (ymin > ymax) ymin = ymax = (ymin + ymax)/2;
+
+ return new Rect(xmin, ymin, xmax, ymax);
+ }
+
+ private Rect getScrollBounds(View v) {
+ // There can be scroll amounts not yet accounted for in
+ // onLayout, so add mXScroll and mYScroll to the current
+ // positions when calculating the bounds.
+ return getScrollBounds(v.getLeft() + mXScroll,
+ v.getTop() + mYScroll,
+ v.getLeft() + v.getMeasuredWidth() + mXScroll,
+ v.getTop() + v.getMeasuredHeight() + mYScroll);
+ }
+
+ private Point getCorrection(Rect bounds) {
+ return new Point(Math.min(Math.max(0,bounds.left),bounds.right),
+ Math.min(Math.max(0,bounds.top),bounds.bottom));
+ }
+
+ private void postSettle(final View v) {
+ // onSettle and onUnsettle are posted so that the calls
+ // wont be executed until after the system has performed
+ // layout.
+ post (new Runnable() {
+ public void run () {
+ onSettle(v);
+ }
+ });
+ }
+
+ private void postUnsettle(final View v) {
+ post (new Runnable() {
+ public void run () {
+ onUnsettle(v);
+ }
+ });
+ }
+
+ private void slideViewOntoScreen(View v) {
+ Point corr = getCorrection(getScrollBounds(v));
+ if (corr.x != 0 || corr.y != 0) {
+ mScrollerLastX = mScrollerLastY = 0;
+ mScroller.startScroll(0, 0, corr.x, corr.y, 400);
+ mStepper.prod();
+ }
+ }
+
+ private Point subScreenSizeOffset(View v) {
+ return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
+ Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
+ }
+
+ private static int directionOfTravel(float vx, float vy) {
+ if (Math.abs(vx) > 2 * Math.abs(vy))
+ return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
+ else if (Math.abs(vy) > 2 * Math.abs(vx))
+ return (vy > 0) ? MOVING_DOWN : MOVING_UP;
+ else
+ return MOVING_DIAGONALLY;
+ }
+
+ private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
+ switch (directionOfTravel(vx, vy)) {
+ case MOVING_DIAGONALLY: return bounds.contains(0, 0);
+ case MOVING_LEFT: return bounds.left <= 0;
+ case MOVING_RIGHT: return bounds.right >= 0;
+ case MOVING_UP: return bounds.top <= 0;
+ case MOVING_DOWN: return bounds.bottom >= 0;
+ default: throw new NoSuchElementException();
+ }
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java b/platform/android/viewer/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java
new file mode 100644
index 00000000..7f715bb4
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java
@@ -0,0 +1,35 @@
+package com.artifex.mupdfdemo;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.view.View;
+
+public class SafeAnimatorInflater
+{
+ private View mView;
+
+ public SafeAnimatorInflater(Activity activity, int animation, View view)
+ {
+ AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(activity, R.animator.info);
+ mView = view;
+ set.setTarget(view);
+ set.addListener(new Animator.AnimatorListener() {
+ public void onAnimationStart(Animator animation) {
+ mView.setVisibility(View.VISIBLE);
+ }
+
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ public void onAnimationEnd(Animator animation) {
+ mView.setVisibility(View.INVISIBLE);
+ }
+
+ public void onAnimationCancel(Animator animation) {
+ }
+ });
+ set.start();
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTask.java b/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTask.java
new file mode 100644
index 00000000..d3969f10
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTask.java
@@ -0,0 +1,128 @@
+package com.artifex.mupdfdemo;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.RectF;
+import android.os.Handler;
+
+class ProgressDialogX extends ProgressDialog {
+ public ProgressDialogX(Context context) {
+ super(context);
+ }
+
+ private boolean mCancelled = false;
+
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+
+ @Override
+ public void cancel() {
+ mCancelled = true;
+ super.cancel();
+ }
+}
+
+public abstract class SearchTask {
+ private static final int SEARCH_PROGRESS_DELAY = 200;
+ private final Context mContext;
+ private final MuPDFCore mCore;
+ private final Handler mHandler;
+ private final AlertDialog.Builder mAlertBuilder;
+ private AsyncTask<Void,Integer,SearchTaskResult> mSearchTask;
+
+ public SearchTask(Context context, MuPDFCore core) {
+ mContext = context;
+ mCore = core;
+ mHandler = new Handler();
+ mAlertBuilder = new AlertDialog.Builder(context);
+ }
+
+ protected abstract void onTextFound(SearchTaskResult result);
+
+ public void stop() {
+ if (mSearchTask != null) {
+ mSearchTask.cancel(true);
+ mSearchTask = null;
+ }
+ }
+
+ public void go(final String text, int direction, int displayPage, int searchPage) {
+ if (mCore == null)
+ return;
+ stop();
+
+ final int increment = direction;
+ final int startIndex = searchPage == -1 ? displayPage : searchPage + increment;
+
+ final ProgressDialogX progressDialog = new ProgressDialogX(mContext);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.setTitle(mContext.getString(R.string.searching_));
+ progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ stop();
+ }
+ });
+ progressDialog.setMax(mCore.countPages());
+
+ mSearchTask = new AsyncTask<Void,Integer,SearchTaskResult>() {
+ @Override
+ protected SearchTaskResult doInBackground(Void... params) {
+ int index = startIndex;
+
+ while (0 <= index && index < mCore.countPages() && !isCancelled()) {
+ publishProgress(index);
+ RectF searchHits[] = mCore.searchPage(index, text);
+
+ if (searchHits != null && searchHits.length > 0)
+ return new SearchTaskResult(text, index, searchHits);
+
+ index += increment;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(SearchTaskResult result) {
+ progressDialog.cancel();
+ if (result != null) {
+ onTextFound(result);
+ } else {
+ mAlertBuilder.setTitle(SearchTaskResult.get() == null ? R.string.text_not_found : R.string.no_further_occurrences_found);
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, mContext.getString(R.string.dismiss),
+ (DialogInterface.OnClickListener)null);
+ alert.show();
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ progressDialog.cancel();
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... values) {
+ progressDialog.setProgress(values[0].intValue());
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ if (!progressDialog.isCancelled())
+ {
+ progressDialog.show();
+ progressDialog.setProgress(startIndex);
+ }
+ }
+ }, SEARCH_PROGRESS_DELAY);
+ }
+ };
+
+ mSearchTask.execute();
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTaskResult.java b/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTaskResult.java
new file mode 100644
index 00000000..8fa3c3a2
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/SearchTaskResult.java
@@ -0,0 +1,24 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.RectF;
+
+public class SearchTaskResult {
+ public final String txt;
+ public final int pageNumber;
+ public final RectF searchBoxes[];
+ static private SearchTaskResult singleton;
+
+ SearchTaskResult(String _txt, int _pageNumber, RectF _searchBoxes[]) {
+ txt = _txt;
+ pageNumber = _pageNumber;
+ searchBoxes = _searchBoxes;
+ }
+
+ static public SearchTaskResult get() {
+ return singleton;
+ }
+
+ static public void set(SearchTaskResult r) {
+ singleton = r;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/Separation.java b/platform/android/viewer/src/com/artifex/mupdfdemo/Separation.java
new file mode 100644
index 00000000..eadda4ba
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/Separation.java
@@ -0,0 +1,15 @@
+package com.artifex.mupdfdemo;
+
+public class Separation
+{
+ String name;
+ int rgba;
+ int cmyk;
+
+ public Separation(String name, int rgba, int cmyk)
+ {
+ this.name = name;
+ this.rgba = rgba;
+ this.cmyk = cmyk;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/Stepper.java b/platform/android/viewer/src/com/artifex/mupdfdemo/Stepper.java
new file mode 100644
index 00000000..d22240ef
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/Stepper.java
@@ -0,0 +1,42 @@
+package com.artifex.mupdfdemo;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.view.View;
+
+public class Stepper {
+ protected final View mPoster;
+ protected final Runnable mTask;
+ protected boolean mPending;
+
+ public Stepper(View v, Runnable r) {
+ mPoster = v;
+ mTask = r;
+ mPending = false;
+ }
+
+ @SuppressLint("NewApi")
+ public void prod() {
+ if (!mPending) {
+ mPending = true;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mPoster.postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mPending = false;
+ mTask.run();
+ }
+ });
+ } else {
+ mPoster.post(new Runnable() {
+ @Override
+ public void run() {
+ mPending = false;
+ mTask.run();
+ }
+ });
+
+ }
+ }
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/TextChar.java b/platform/android/viewer/src/com/artifex/mupdfdemo/TextChar.java
new file mode 100644
index 00000000..aebf519f
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/TextChar.java
@@ -0,0 +1,12 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.RectF;
+
+public class TextChar extends RectF {
+ public char c;
+
+ public TextChar(float x0, float y0, float x1, float y1, char _c) {
+ super(x0, y0, x1, y1);
+ c = _c;
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/TextWord.java b/platform/android/viewer/src/com/artifex/mupdfdemo/TextWord.java
new file mode 100644
index 00000000..d9672573
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/TextWord.java
@@ -0,0 +1,17 @@
+package com.artifex.mupdfdemo;
+
+import android.graphics.RectF;
+
+public class TextWord extends RectF {
+ public String w;
+
+ public TextWord() {
+ super();
+ w = new String();
+ }
+
+ public void Add(TextChar tc) {
+ super.union(tc);
+ w = w.concat(new String(new char[]{tc.c}));
+ }
+}
diff --git a/platform/android/viewer/src/com/artifex/mupdfdemo/WidgetType.java b/platform/android/viewer/src/com/artifex/mupdfdemo/WidgetType.java
new file mode 100644
index 00000000..882a38f2
--- /dev/null
+++ b/platform/android/viewer/src/com/artifex/mupdfdemo/WidgetType.java
@@ -0,0 +1,9 @@
+package com.artifex.mupdfdemo;
+
+public enum WidgetType {
+ NONE,
+ TEXT,
+ LISTBOX,
+ COMBOBOX,
+ SIGNATURE
+}