diff options
author | Tor Andersson <tor.andersson@artifex.com> | 2013-06-19 15:29:44 +0200 |
---|---|---|
committer | Tor Andersson <tor.andersson@artifex.com> | 2013-06-20 16:45:35 +0200 |
commit | 0a927854a10e1e6b9770a81e2e1d9f3093631757 (patch) | |
tree | 3d65d820d9fdba2d0d394d99c36290c851b78ca0 /platform/android/src | |
parent | 1ae8f19179c5f0f8c6352b3c7855465325d5449a (diff) | |
download | mupdf-0a927854a10e1e6b9770a81e2e1d9f3093631757.tar.xz |
Rearrange source files.
Diffstat (limited to 'platform/android/src')
36 files changed, 6978 insertions, 0 deletions
diff --git a/platform/android/src/com/artifex/mupdfdemo/Annotation.java b/platform/android/src/com/artifex/mupdfdemo/Annotation.java new file mode 100644 index 00000000..cf915524 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/ArrayDeque.java b/platform/android/src/com/artifex/mupdfdemo/ArrayDeque.java new file mode 100644 index 00000000..4f06ea41 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/AsyncTask.java b/platform/android/src/com/artifex/mupdfdemo/AsyncTask.java new file mode 100644 index 00000000..b370794c --- /dev/null +++ b/platform/android/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<URL, Integer, Long> { + * 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<Void, Void, Void> { ... } + * </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/src/com/artifex/mupdfdemo/BitmapHolder.java b/platform/android/src/com/artifex/mupdfdemo/BitmapHolder.java new file mode 100644 index 00000000..5816e7bb --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/BitmapHolder.java @@ -0,0 +1,25 @@ +package com.artifex.mupdfdemo; + +import android.graphics.Bitmap; + +public class BitmapHolder { + private Bitmap bm; + + public BitmapHolder() { + bm = null; + } + + public synchronized void setBm(Bitmap abm) { + if (bm != null && bm != abm) + bm.recycle(); + bm = abm; + } + + public synchronized void drop() { + bm = null; + } + + public synchronized Bitmap getBm() { + return bm; + } +} diff --git a/platform/android/src/com/artifex/mupdfdemo/ChoosePDFActivity.java b/platform/android/src/com/artifex/mupdfdemo/ChoosePDFActivity.java new file mode 100644 index 00000000..c1c9142c --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/ChoosePDFActivity.java @@ -0,0 +1,195 @@ +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; + +public class ChoosePDFActivity extends ListActivity { + 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; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + 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(); + if (fname.endsWith(".pdf")) + return true; + if (fname.endsWith(".xps")) + return true; + if (fname.endsWith(".cbz")) + 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; + } + }); + 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.parse(mFiles[position].getAbsolutePath()); + Intent intent = new Intent(this,MuPDFActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(uri); + startActivity(intent); + } + + @Override + protected void onPause() { + super.onPause(); + mPositions.put(mDirectory.getAbsolutePath(), getListView().getFirstVisiblePosition()); + } +} diff --git a/platform/android/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java b/platform/android/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java new file mode 100644 index 00000000..0b3c6418 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/ChoosePDFItem.java b/platform/android/src/com/artifex/mupdfdemo/ChoosePDFItem.java new file mode 100644 index 00000000..de6e1d52 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/Deque.java b/platform/android/src/com/artifex/mupdfdemo/Deque.java new file mode 100644 index 00000000..4bb176b2 --- /dev/null +++ b/platform/android/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 ? e==null : 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 ? e==null : 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 ? e==null : 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 ? e==null : 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/src/com/artifex/mupdfdemo/LinkInfo.java b/platform/android/src/com/artifex/mupdfdemo/LinkInfo.java new file mode 100644 index 00000000..5aeaccbe --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/LinkInfoExternal.java b/platform/android/src/com/artifex/mupdfdemo/LinkInfoExternal.java new file mode 100644 index 00000000..574b6264 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/LinkInfoInternal.java b/platform/android/src/com/artifex/mupdfdemo/LinkInfoInternal.java new file mode 100644 index 00000000..761bf87a --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/LinkInfoRemote.java b/platform/android/src/com/artifex/mupdfdemo/LinkInfoRemote.java new file mode 100644 index 00000000..731e6408 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/LinkInfoVisitor.java b/platform/android/src/com/artifex/mupdfdemo/LinkInfoVisitor.java new file mode 100644 index 00000000..ecd093e4 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/MuPDFActivity.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFActivity.java new file mode 100644 index 00000000..ff38b22a --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFActivity.java @@ -0,0 +1,1090 @@ +package com.artifex.mupdfdemo; + +import java.io.InputStream; +import java.util.concurrent.Executor; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Color; +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.View; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.ViewAnimator; + +class ThreadPerTaskExecutor implements Executor { + public void execute(Runnable r) { + new Thread(r).start(); + } +} + +public class MuPDFActivity extends Activity +{ + /* 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 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 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; + + 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; + } + return core; + } + + private MuPDFCore openBuffer(byte buffer[]) + { + System.out.println("Trying to open byte buffer"); + try + { + core = new MuPDFCore(this, buffer); + // New file: drop the old outline data + OutlineActivityData.set(null); + } + catch (Exception e) + { + System.out.println(e); + return null; + } + return core; + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + mAlertBuilder = new AlertDialog.Builder(this); + + 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(); + if (uri.toString().startsWith("content://")) { + // Handle view requests from the Transformer Prime's file manager + // Hopefully other file managers will use this same scheme, if not + // using explicit paths. + Cursor cursor = getContentResolver().query(uri, new String[]{"_data"}, null, null, null); + if (cursor.moveToFirst()) { + String str = cursor.getString(0); + String reason = null; + if (str == 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) { + reason = e.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; + } + } else { + uri = Uri.parse(str); + } + } + } + if (buffer != null) { + core = openBuffer(buffer); + } else { + core = openFile(Uri.decode(uri.getEncodedPath())); + } + SearchTaskResult.set(null); + if (core.countPages() == 0) + core = null; + } + if (core != null && core.needsPassword()) { + requestPassword(savedInstanceState); + return; + } + } + 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.show(); + return; + } + + createUI(savedInstanceState); + } + + 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, 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")) + { + 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); + } + + @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; + } + 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, 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 (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); + mTopBarSwitcher.setVisibility(View.INVISIBLE); + mPageNumberView.setVisibility(View.INVISIBLE); + mInfoView.setVisibility(View.INVISIBLE); + mPageSlider.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(); + } + + 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.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(); + } + } +} diff --git a/platform/android/src/com/artifex/mupdfdemo/MuPDFAlert.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFAlert.java new file mode 100644 index 00000000..76ed3a65 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java new file mode 100644 index 00000000..5d65768f --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/MuPDFCore.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFCore.java new file mode 100644 index 00000000..bc1fea71 --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFCore.java @@ -0,0 +1,300 @@ +package com.artifex.mupdfdemo; +import java.util.ArrayList; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.PointF; +import android.graphics.RectF; + +public class MuPDFCore +{ + /* load our native library */ + static { + System.loadLibrary("mupdf"); + } + + /* Readable members */ + private int numPages = -1; + private float pageWidth; + private float pageHeight; + private long globals; + private byte fileBuffer[]; + private String file_format; + + /* The native functions */ + private native long openFile(String filename); + private native long openBuffer(); + private native String fileFormatInternal(); + 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); + private native void updatePageInternal(Bitmap bitmap, + int page, + int pageW, int pageH, + int patchX, int patchY, + int patchW, int patchH); + 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 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(); + + public static native boolean javascriptSupported(); + + 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(); + } + + public MuPDFCore(Context context, byte buffer[]) throws Exception + { + fileBuffer = buffer; + globals = openBuffer(); + if (globals == 0) + { + throw new Exception(context.getString(R.string.cannot_open_buffer)); + } + file_format = fileFormatInternal(); + } + + public int countPages() + { + if (numPages < 0) + numPages = countPagesSynchronized(); + + return numPages; + } + + public String fileFormat() + { + return file_format; + } + + 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 Bitmap drawPage(int page, + int pageW, int pageH, + int patchX, int patchY, + int patchW, int patchH) { + gotoPage(page); + Bitmap bm = Bitmap.createBitmap(patchW, patchH, Config.ARGB_8888); + drawPage(bm, pageW, pageH, patchX, patchY, patchW, patchH); + return bm; + } + + public synchronized Bitmap updatePage(BitmapHolder h, int page, + int pageW, int pageH, + int patchX, int patchY, + int patchW, int patchH) { + Bitmap bm = null; + Bitmap old_bm = h.getBm(); + + if (old_bm == null) + return null; + + bm = old_bm.copy(Bitmap.Config.ARGB_8888, false); + old_bm = null; + + updatePageInternal(bm, page, pageW, pageH, patchX, patchY, patchW, patchH); + return bm; + } + + 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()); + 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 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) { + 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(); + } +} diff --git a/platform/android/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java new file mode 100644 index 00000000..806d0830 --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java @@ -0,0 +1,72 @@ +package com.artifex.mupdfdemo; + +import android.content.Context; +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 MuPDFCore mCore; + private final SparseArray<PointF> mPageSizes = new SparseArray<PointF>(); + + public MuPDFPageAdapter(Context c, MuPDFCore core) { + mContext = c; + mCore = core; + } + + public int getCount() { + return mCore.countPages(); + } + + public Object getItem(int position) { + return null; + } + + public long getItemId(int position) { + return 0; + } + + public View getView(final int position, View convertView, ViewGroup parent) { + final MuPDFPageView pageView; + if (convertView == null) { + pageView = new MuPDFPageView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight())); + } 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/src/com/artifex/mupdfdemo/MuPDFPageView.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFPageView.java new file mode 100644 index 00000000..043b90ab --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFPageView.java @@ -0,0 +1,502 @@ +package com.artifex.mupdfdemo; + +import java.util.ArrayList; + +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.view.LayoutInflater; +import android.view.WindowManager; +import android.widget.EditText; + +abstract class PassClickResultVisitor { + public abstract void visitText(PassClickResultText result); + public abstract void visitChoice(PassClickResultChoice 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); + } +} + +public class MuPDFPageView extends PageView implements MuPDFView { + 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 mTextEntry; + 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 Runnable changeReporter; + + public MuPDFPageView(Context c, MuPDFCore core, Point parentSize) { + super(c, parentSize); + 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)); + } + + 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(); + } + + 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 (!MuPDFCore.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); + } + }); + } + }; + + mPassClick.execute(); + return Hit.Widget; + } + + return Hit.Nothing; + } + + 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 Bitmap drawPage(int sizeX, int sizeY, + int patchX, int patchY, int patchWidth, int patchHeight) { + return mCore.drawPage(mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight); + } + + @Override + protected Bitmap updatePage(BitmapHolder h, int sizeX, int sizeY, + int patchX, int patchY, int patchWidth, int patchHeight) { + return mCore.updatePage(h, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight); + } + + @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/src/com/artifex/mupdfdemo/MuPDFReaderView.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFReaderView.java new file mode 100644 index 00000000..9cab70b7 --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFReaderView.java @@ -0,0 +1,261 @@ +package com.artifex.mupdfdemo; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +public class MuPDFReaderView extends ReaderView { + 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; + } + + public MuPDFReaderView(Activity act) { + super(act); + mContext = act; + // 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(); + act.getWindowManager().getDefaultDisplay().getMetrics(dm); + tapPageMargin = (int)dm.xdpi; + if (tapPageMargin < 100) + tapPageMargin = 100; + if (tapPageMargin > dm.widthPixels/5) + tapPageMargin = dm.widthPixels/5; + } + + 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).addHq(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/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java new file mode 100644 index 00000000..48625a7e --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/MuPDFReflowView.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFReflowView.java new file mode 100644 index 00000000..0c7074bc --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFReflowView.java @@ -0,0 +1,176 @@ +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 int mContentHeight; + AsyncTask<Void,Void,byte[]> mLoadHTML; + + public MuPDFReflowView(Context c, MuPDFCore core, Point parentSize) { + super(c); + mHandler = new Handler(); + mCore = core; + mParentSize = parentSize; + 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) { + requestHeight(); + } + }); + } + + 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) { + loadUrl("javascript:document.getElementById('content').style.zoom=\""+(int)(scale*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 addHq(boolean update) { + } + + public void removeHq() { + } + + public void releaseResources() { + if (mLoadHTML != null) { + mLoadHTML.cancel(true); + mLoadHTML = null; + } + } + + @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/src/com/artifex/mupdfdemo/MuPDFView.java b/platform/android/src/com/artifex/mupdfdemo/MuPDFView.java new file mode 100644 index 00000000..cc0405d1 --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/MuPDFView.java @@ -0,0 +1,32 @@ +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 addHq(boolean update); + public void removeHq(); + public void releaseResources(); +} diff --git a/platform/android/src/com/artifex/mupdfdemo/OutlineActivity.java b/platform/android/src/com/artifex/mupdfdemo/OutlineActivity.java new file mode 100644 index 00000000..52b0d410 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/OutlineActivityData.java b/platform/android/src/com/artifex/mupdfdemo/OutlineActivityData.java new file mode 100644 index 00000000..a703e61e --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/OutlineAdapter.java b/platform/android/src/com/artifex/mupdfdemo/OutlineAdapter.java new file mode 100644 index 00000000..4251ed8e --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/OutlineItem.java b/platform/android/src/com/artifex/mupdfdemo/OutlineItem.java new file mode 100644 index 00000000..7730991e --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/PageView.java b/platform/android/src/com/artifex/mupdfdemo/PageView.java new file mode 100644 index 00000000..200821dd --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/PageView.java @@ -0,0 +1,704 @@ +package com.artifex.mupdfdemo; + +import java.util.ArrayList; +import java.util.Iterator; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +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; + +class PatchInfo { + public BitmapHolder bmh; + public Bitmap bm; + public Point patchViewSize; + public Rect patchArea; + public boolean completeRedraw; + + public PatchInfo(Point aPatchViewSize, Rect aPatchArea, BitmapHolder aBmh, boolean aCompleteRedraw) { + bmh = aBmh; + bm = null; + patchViewSize = aPatchViewSize; + patchArea = aPatchArea; + completeRedraw = aCompleteRedraw; + } +} + +// 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 BitmapHolder mEntireBmh; + private AsyncTask<Void,Void,TextWord[][]> mGetText; + private AsyncTask<Void,Void,LinkInfo[]> mGetLinkInfo; + private AsyncTask<Void,Void,Bitmap> mDrawEntire; + + private Point mPatchViewSize; // View size on the basis of which the patch was created + private Rect mPatchArea; + private ImageView mPatch; + private BitmapHolder mPatchBmh; + private AsyncTask<PatchInfo,Void,PatchInfo> 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) { + super(c); + mContext = c; + mParentSize = parentSize; + setBackgroundColor(BACKGROUND_COLOR); + mEntireBmh = new BitmapHolder(); + mPatchBmh = new BitmapHolder(); + } + + protected abstract Bitmap drawPage(int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight); + protected abstract Bitmap updatePage(BitmapHolder h, 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.cancel(true); + mDrawEntire = null; + } + + if (mDrawPatch != null) { + mDrawPatch.cancel(true); + 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); + mEntireBmh.setBm(null); + } + + if (mPatch != null) { + mPatch.setImageBitmap(null); + mPatchBmh.setBm(null); + } + + 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 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.cancel(true); + 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.FIT_CENTER); + 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); + mEntireBmh.setBm(null); + + // 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; + invalidate(); + } + }; + + mGetLinkInfo.execute(); + + // Render the page in the background + mDrawEntire = new AsyncTask<Void,Void,Bitmap>() { + protected Bitmap doInBackground(Void... v) { + return drawPage(mSize.x, mSize.y, 0, 0, mSize.x, mSize.y); + } + + protected void onPreExecute() { + setBackgroundColor(BACKGROUND_COLOR); + mEntire.setImageBitmap(null); + mEntireBmh.setBm(null); + + 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); + } + } + + protected void onPostExecute(Bitmap bm) { + removeView(mBusyIndicator); + mBusyIndicator = null; + mEntire.setImageBitmap(bm); + mEntireBmh.setBm(bm); + 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; + 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); + } + } + + paint.setAntiAlias(true); + paint.setDither(true); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(INK_THICKNESS * scale); + paint.setColor(INK_COLOR); + + 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); + } + + 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) { + 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); + mPatchBmh.setBm(null); + } + } 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 addHq(boolean update) { + Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom()); + // If the viewArea's size matches the unzoomed size, there is no need for an hq patch + if (viewArea.width() != mSize.x || viewArea.height() != mSize.y) { + Point patchViewSize = new Point(viewArea.width(), viewArea.height()); + 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.cancel(true); + mDrawPatch = null; + } + + if (completeRedraw) { + // The bitmap holder mPatchBm may still be rendered to by a + // previously invoked task, and possibly for a different + // area, so we cannot risk the bitmap generated by this task + // being passed to it + mPatchBmh.drop(); + mPatchBmh = new BitmapHolder(); + } + + // Create and add the image view if not already done + if (mPatch == null) { + mPatch = new OpaqueImageView(mContext); + mPatch.setScaleType(ImageView.ScaleType.FIT_CENTER); + addView(mPatch); + mSearchView.bringToFront(); + } + + mDrawPatch = new AsyncTask<PatchInfo,Void,PatchInfo>() { + protected PatchInfo doInBackground(PatchInfo... v) { + if (v[0].completeRedraw) { + v[0].bm = drawPage(v[0].patchViewSize.x, v[0].patchViewSize.y, + v[0].patchArea.left, v[0].patchArea.top, + v[0].patchArea.width(), v[0].patchArea.height()); + } else { + v[0].bm = updatePage(v[0].bmh, v[0].patchViewSize.x, v[0].patchViewSize.y, + v[0].patchArea.left, v[0].patchArea.top, + v[0].patchArea.width(), v[0].patchArea.height()); + } + + return v[0]; + } + + protected void onPostExecute(PatchInfo v) { + if (mPatchBmh == v.bmh) { + mPatchViewSize = v.patchViewSize; + mPatchArea = v.patchArea; + if (v.bm != null) { + mPatch.setImageBitmap(v.bm); + v.bmh.setBm(v.bm); + v.bm = null; + } + //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); + invalidate(); + } + } + }; + + mDrawPatch.execute(new PatchInfo(patchViewSize, patchArea, mPatchBmh, completeRedraw)); + } + } + + public void update() { + // Cancel pending render task + if (mDrawEntire != null) { + mDrawEntire.cancel(true); + mDrawEntire = null; + } + + if (mDrawPatch != null) { + mDrawPatch.cancel(true); + mDrawPatch = null; + } + + // Render the page in the background + mDrawEntire = new AsyncTask<Void,Void,Bitmap>() { + protected Bitmap doInBackground(Void... v) { + // Pass the current bitmap as a basis for the update, but use a bitmap + // holder so that the held bitmap will be nulled and not hold on to + // memory, should this view become redundant. + return updatePage(mEntireBmh, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y); + } + + protected void onPostExecute(Bitmap bm) { + if (bm != null) { + mEntire.setImageBitmap(bm); + mEntireBmh.setBm(bm); + } + invalidate(); + } + }; + + mDrawEntire.execute(); + + addHq(true); + } + + public void removeHq() { + // Stop the drawing of the patch if still going + if (mDrawPatch != null) { + mDrawPatch.cancel(true); + mDrawPatch = null; + } + + // And get rid of it + mPatchViewSize = null; + mPatchArea = null; + if (mPatch != null) { + mPatch.setImageBitmap(null); + mPatchBmh.setBm(null); + } + } + + public int getPage() { + return mPageNumber; + } + + @Override + public boolean isOpaque() { + return true; + } +} diff --git a/platform/android/src/com/artifex/mupdfdemo/PrintDialogActivity.java b/platform/android/src/com/artifex/mupdfdemo/PrintDialogActivity.java new file mode 100644 index 00000000..d96322d5 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/ReaderView.java b/platform/android/src/com/artifex/mupdfdemo/ReaderView.java new file mode 100644 index 00000000..f4f54722 --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/ReaderView.java @@ -0,0 +1,803 @@ +package com.artifex.mupdfdemo; + +import java.util.LinkedList; +import java.util.NoSuchElementException; + +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 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 final GestureDetector + mGestureDetector; + private final ScaleGestureDetector + mScaleGestureDetector; + private final Scroller mScroller; + private int mScrollerLastX; + private int mScrollerLastY; + private boolean mScrollDisabled; + + 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); + } + + public ReaderView(Context context, AttributeSet attrs) { + super(context, attrs); + mGestureDetector = new GestureDetector(this); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mScroller = new Scroller(context); + } + + 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); + } + + 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); + post(this); + } + + 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); + post(this); + } + + 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; + + mScale = 1.0f; + mXScroll = mYScroll = 0; + + int numChildren = mChildViews.size(); + for (int i = 0; i < numChildren; i++) { + View v = mChildViews.valueAt(i); + onNotInUse(v); + removeViewInLayout(v); + } + mChildViews.clear(); + mViewCache.clear(); + + 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(); + post(this); + } + 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 (mScrollDisabled) + return true; + + View v = mChildViews.get(mCurrent); + if (v != null) { + Rect bounds = getScrollBounds(v); + switch(directionOfTravel(velocityX, velocityY)) { + case MOVING_LEFT: + if (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_RIGHT: + if (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; + } + 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); + post(this); + } + } + + return true; + } + + public void onLongPress(MotionEvent e) { + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + if (!mScrollDisabled) { + 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) { + applyToChildren(new ViewMapper() { + @Override + void applyToView(View view) { + onScaleChild(view, mScale); + } + }); + } else { + float factor = mScale/previousScale; + + View v = mChildViews.get(mCurrent); + if (v != null) { + // Work out the focus point relative to the view top left + int viewFocusX = (int)detector.getFocusX() - (v.getLeft() + mXScroll); + int viewFocusY = (int)detector.getFocusY() - (v.getTop() + mYScroll); + // Scroll to maintain the focus point + mXScroll += viewFocusX - viewFocusX * factor; + mYScroll += viewFocusY - viewFocusY * factor; + 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; + // Avoid jump at end of scaling by disabling scrolling + // until the next start of gesture + mScrollDisabled = true; + return true; + } + + public void onScaleEnd(ScaleGestureDetector detector) { + mScaling = false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mScaleGestureDetector.onTouchEvent(event); + + if (!mScaling) + mGestureDetector.onTouchEvent(event); + + if ((event.getAction() & event.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + mUserInteracting = true; + } + if ((event.getAction() & event.ACTION_MASK) == MotionEvent.ACTION_UP) { + mScrollDisabled = false; + 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); + + View cv = mChildViews.get(mCurrent); + Point cvOffset; + + if (!mResetLayout) { + // Move to next or previous if current is sufficiently off center + if (cv != null) { + 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 (cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2 && 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 + post(this); + + onMoveOffChild(mCurrent); + mCurrent++; + onMoveToChild(mCurrent); + } + + if (cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2 && mCurrent > 0) { + postUnsettle(cv); + // post to invoke test for end of animation + // where we must set hq area for the new current view + post(this); + + 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(); + // post to ensure generation of hq area + post(this); + } + + // 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 (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; + } + + cv.layout(cvLeft, cvTop, cvRight, cvBottom); + + if (mCurrent > 0) { + View lv = getOrCreateChild(mCurrent - 1); + Point leftOffset = subScreenSizeOffset(lv); + 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); + } + + if (mCurrent + 1 < mAdapter.getCount()) { + View rv = getOrCreateChild(mCurrent + 1); + Point rightOffset = subScreenSizeOffset(rv); + 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); + } + + invalidate(); + } + + @Override + public Adapter getAdapter() { + return mAdapter; + } + + @Override + public View getSelectedView() { + throw new UnsupportedOperationException(getContext().getString(R.string.not_supported)); + } + + @Override + public void setAdapter(Adapter adapter) { + mAdapter = adapter; + mChildViews.clear(); + removeAllViewsInLayout(); + 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); + post(this); + } + } + + 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/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java b/platform/android/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java new file mode 100644 index 00000000..7c6a7ebc --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java @@ -0,0 +1,37 @@ +package com.artifex.mupdfdemo; + +import android.animation.Animator; +import android.view.View; +import android.view.animation.Animation; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.view.View; +import android.app.Activity; + +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/src/com/artifex/mupdfdemo/SearchTask.java b/platform/android/src/com/artifex/mupdfdemo/SearchTask.java new file mode 100644 index 00000000..d3969f10 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/SearchTaskResult.java b/platform/android/src/com/artifex/mupdfdemo/SearchTaskResult.java new file mode 100644 index 00000000..8fa3c3a2 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/TextChar.java b/platform/android/src/com/artifex/mupdfdemo/TextChar.java new file mode 100644 index 00000000..aebf519f --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/TextWord.java b/platform/android/src/com/artifex/mupdfdemo/TextWord.java new file mode 100644 index 00000000..d9672573 --- /dev/null +++ b/platform/android/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/src/com/artifex/mupdfdemo/WidgetType.java b/platform/android/src/com/artifex/mupdfdemo/WidgetType.java new file mode 100644 index 00000000..5a22975d --- /dev/null +++ b/platform/android/src/com/artifex/mupdfdemo/WidgetType.java @@ -0,0 +1,8 @@ +package com.artifex.mupdfdemo; + +public enum WidgetType { + NONE, + TEXT, + LISTBOX, + COMBOBOX +} |