From 0a927854a10e1e6b9770a81e2e1d9f3093631757 Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor.andersson@artifex.com>
Date: Wed, 19 Jun 2013 15:29:44 +0200
Subject: Rearrange source files.

---
 .../src/com/artifex/mupdfdemo/Annotation.java      |   18 +
 .../src/com/artifex/mupdfdemo/ArrayDeque.java      |  855 +++++++++++++++
 .../src/com/artifex/mupdfdemo/AsyncTask.java       |  670 ++++++++++++
 .../src/com/artifex/mupdfdemo/BitmapHolder.java    |   25 +
 .../com/artifex/mupdfdemo/ChoosePDFActivity.java   |  195 ++++
 .../com/artifex/mupdfdemo/ChoosePDFAdapter.java    |   66 ++
 .../src/com/artifex/mupdfdemo/ChoosePDFItem.java   |   15 +
 .../android/src/com/artifex/mupdfdemo/Deque.java   |  554 ++++++++++
 .../src/com/artifex/mupdfdemo/LinkInfo.java        |   14 +
 .../com/artifex/mupdfdemo/LinkInfoExternal.java    |   14 +
 .../com/artifex/mupdfdemo/LinkInfoInternal.java    |   14 +
 .../src/com/artifex/mupdfdemo/LinkInfoRemote.java  |   18 +
 .../src/com/artifex/mupdfdemo/LinkInfoVisitor.java |    7 +
 .../src/com/artifex/mupdfdemo/MuPDFActivity.java   | 1090 ++++++++++++++++++++
 .../src/com/artifex/mupdfdemo/MuPDFAlert.java      |   21 +
 .../com/artifex/mupdfdemo/MuPDFAlertInternal.java  |   30 +
 .../src/com/artifex/mupdfdemo/MuPDFCore.java       |  300 ++++++
 .../com/artifex/mupdfdemo/MuPDFPageAdapter.java    |   72 ++
 .../src/com/artifex/mupdfdemo/MuPDFPageView.java   |  502 +++++++++
 .../src/com/artifex/mupdfdemo/MuPDFReaderView.java |  261 +++++
 .../com/artifex/mupdfdemo/MuPDFReflowAdapter.java  |   43 +
 .../src/com/artifex/mupdfdemo/MuPDFReflowView.java |  176 ++++
 .../src/com/artifex/mupdfdemo/MuPDFView.java       |   32 +
 .../src/com/artifex/mupdfdemo/OutlineActivity.java |   31 +
 .../com/artifex/mupdfdemo/OutlineActivityData.java |   17 +
 .../src/com/artifex/mupdfdemo/OutlineAdapter.java  |   46 +
 .../src/com/artifex/mupdfdemo/OutlineItem.java     |   14 +
 .../src/com/artifex/mupdfdemo/PageView.java        |  704 +++++++++++++
 .../com/artifex/mupdfdemo/PrintDialogActivity.java |  145 +++
 .../src/com/artifex/mupdfdemo/ReaderView.java      |  803 ++++++++++++++
 .../artifex/mupdfdemo/SafeAnimatorInflater.java    |   37 +
 .../src/com/artifex/mupdfdemo/SearchTask.java      |  128 +++
 .../com/artifex/mupdfdemo/SearchTaskResult.java    |   24 +
 .../src/com/artifex/mupdfdemo/TextChar.java        |   12 +
 .../src/com/artifex/mupdfdemo/TextWord.java        |   17 +
 .../src/com/artifex/mupdfdemo/WidgetType.java      |    8 +
 36 files changed, 6978 insertions(+)
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/Annotation.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/ArrayDeque.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/AsyncTask.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/BitmapHolder.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/ChoosePDFActivity.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/ChoosePDFAdapter.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/ChoosePDFItem.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/Deque.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/LinkInfo.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/LinkInfoExternal.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/LinkInfoInternal.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/LinkInfoRemote.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/LinkInfoVisitor.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFActivity.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFAlert.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFAlertInternal.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFCore.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFPageAdapter.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFPageView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFReaderView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFReflowAdapter.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFReflowView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/MuPDFView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/OutlineActivity.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/OutlineActivityData.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/OutlineAdapter.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/OutlineItem.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/PageView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/PrintDialogActivity.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/ReaderView.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/SafeAnimatorInflater.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/SearchTask.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/SearchTaskResult.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/TextChar.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/TextWord.java
 create mode 100644 platform/android/src/com/artifex/mupdfdemo/WidgetType.java

(limited to 'platform/android/src')

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&lt;URL, Integer, Long&gt; {
+ *     protected Long doInBackground(URL... urls) {
+ *         int count = urls.length;
+ *         long totalSize = 0;
+ *         for (int i = 0; i < count; i++) {
+ *             totalSize += Downloader.downloadFile(urls[i]);
+ *             publishProgress((int) ((i / (float) count) * 100));
+ *             // Escape early if cancel() is called
+ *             if (isCancelled()) break;
+ *         }
+ *         return totalSize;
+ *     }
+ *
+ *     protected void onProgressUpdate(Integer... progress) {
+ *         setProgressPercent(progress[0]);
+ *     }
+ *
+ *     protected void onPostExecute(Long result) {
+ *         showDialog("Downloaded " + result + " bytes");
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>Once created, a task is executed very simply:</p>
+ * <pre class="prettyprint">
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * </pre>
+ *
+ * <h2>AsyncTask's generic types</h2>
+ * <p>The three types used by an asynchronous task are the following:</p>
+ * <ol>
+ *     <li><code>Params</code>, the type of the parameters sent to the task upon
+ *     execution.</li>
+ *     <li><code>Progress</code>, the type of the progress units published during
+ *     the background computation.</li>
+ *     <li><code>Result</code>, the type of the result of the background
+ *     computation.</li>
+ * </ol>
+ * <p>Not all types are always used by an asynchronous task. To mark a type as unused,
+ * simply use the type {@link Void}:</p>
+ * <pre>
+ * private class MyTask extends AsyncTask&lt;Void, Void, Void&gt; { ... }
+ * </pre>
+ *
+ * <h2>The 4 steps</h2>
+ * <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
+ * <ol>
+ *     <li>{@link #onPreExecute()}, invoked on the UI thread before the task
+ *     is executed. This step is normally used to setup the task, for instance by
+ *     showing a progress bar in the user interface.</li>
+ *     <li>{@link #doInBackground}, invoked on the background thread
+ *     immediately after {@link #onPreExecute()} finishes executing. This step is used
+ *     to perform background computation that can take a long time. The parameters
+ *     of the asynchronous task are passed to this step. The result of the computation must
+ *     be returned by this step and will be passed back to the last step. This step
+ *     can also use {@link #publishProgress} to publish one or more units
+ *     of progress. These values are published on the UI thread, in the
+ *     {@link #onProgressUpdate} step.</li>
+ *     <li>{@link #onProgressUpdate}, invoked on the UI thread after a
+ *     call to {@link #publishProgress}. The timing of the execution is
+ *     undefined. This method is used to display any form of progress in the user
+ *     interface while the background computation is still executing. For instance,
+ *     it can be used to animate a progress bar or show logs in a text field.</li>
+ *     <li>{@link #onPostExecute}, invoked on the UI thread after the background
+ *     computation finishes. The result of the background computation is passed to
+ *     this step as a parameter.</li>
+ * </ol>
+ *
+ * <h2>Cancelling a task</h2>
+ * <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
+ * this method will cause subsequent calls to {@link #isCancelled()} to return true.
+ * After invoking this method, {@link #onCancelled(Object)}, instead of
+ * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
+ * returns. To ensure that a task is cancelled as quickly as possible, you should always
+ * check the return value of {@link #isCancelled()} periodically from
+ * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
+ *
+ * <h2>Threading rules</h2>
+ * <p>There are a few threading rules that must be followed for this class to
+ * work properly:</p>
+ * <ul>
+ *     <li>The AsyncTask class must be loaded on the UI thread. This is done
+ *     automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
+ *     <li>The task instance must be created on the UI thread.</li>
+ *     <li>{@link #execute} must be invoked on the UI thread.</li>
+ *     <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
+ *     {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
+ *     <li>The task can be executed only once (an exception will be thrown if
+ *     a second execution is attempted.)</li>
+ * </ul>
+ *
+ * <h2>Memory observability</h2>
+ * <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
+ * operations are safe without explicit synchronizations.</p>
+ * <ul>
+ *     <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
+ *     in {@link #doInBackground}.
+ *     <li>Set member fields in {@link #doInBackground}, and refer to them in
+ *     {@link #onProgressUpdate} and {@link #onPostExecute}.
+ * </ul>
+ *
+ * <h2>Order of execution</h2>
+ * <p>When first introduced, AsyncTasks were executed serially on a single background
+ * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+ * to a pool of threads allowing multiple tasks to operate in parallel. Starting with
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
+ * thread to avoid common application errors caused by parallel execution.</p>
+ * <p>If you truly want parallel execution, you can invoke
+ * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
+ * {@link #THREAD_POOL_EXECUTOR}.</p>
+ */
+public abstract class AsyncTask<Params, Progress, Result> {
+    private static final String LOG_TAG = "AsyncTask";
+
+    private static final int CORE_POOL_SIZE = 5;
+    private static final int MAXIMUM_POOL_SIZE = 128;
+    private static final int KEEP_ALIVE = 1;
+
+    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
+        private final AtomicInteger mCount = new AtomicInteger(1);
+
+        public Thread newThread(Runnable r) {
+            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
+        }
+    };
+
+    private static final BlockingQueue<Runnable> sPoolWorkQueue =
+            new LinkedBlockingQueue<Runnable>(10);
+
+    /**
+     * An {@link Executor} that can be used to execute tasks in parallel.
+     */
+    public static final Executor THREAD_POOL_EXECUTOR
+            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
+                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
+
+    /**
+     * An {@link Executor} that executes tasks one at a time in serial
+     * order.  This serialization is global to a particular process.
+     */
+    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
+
+    private static final int MESSAGE_POST_RESULT = 0x1;
+    private static final int MESSAGE_POST_PROGRESS = 0x2;
+
+    private static final InternalHandler sHandler = new InternalHandler();
+
+    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
+    private final WorkerRunnable<Params, Result> mWorker;
+    private final FutureTask<Result> mFuture;
+
+    private volatile Status mStatus = Status.PENDING;
+
+    private final AtomicBoolean mCancelled = new AtomicBoolean();
+    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
+
+    private static class SerialExecutor implements Executor {
+        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
+        Runnable mActive;
+
+        public synchronized void execute(final Runnable r) {
+            mTasks.offer(new Runnable() {
+                public void run() {
+                    try {
+                        r.run();
+                    } finally {
+                        scheduleNext();
+                    }
+                }
+            });
+            if (mActive == null) {
+                scheduleNext();
+            }
+        }
+
+        protected synchronized void scheduleNext() {
+            if ((mActive = mTasks.poll()) != null) {
+                THREAD_POOL_EXECUTOR.execute(mActive);
+            }
+        }
+    }
+
+    /**
+     * Indicates the current status of the task. Each status will be set only once
+     * during the lifetime of a task.
+     */
+    public enum Status {
+        /**
+         * Indicates that the task has not been executed yet.
+         */
+        PENDING,
+        /**
+         * Indicates that the task is running.
+         */
+        RUNNING,
+        /**
+         * Indicates that {@link AsyncTask#onPostExecute} has finished.
+         */
+        FINISHED,
+    }
+
+    /** @hide Used to force static handler to be created. */
+    public static void init() {
+        sHandler.getLooper();
+    }
+
+    /** @hide */
+    public static void setDefaultExecutor(Executor exec) {
+        sDefaultExecutor = exec;
+    }
+
+    /**
+     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
+     */
+    public AsyncTask() {
+        mWorker = new WorkerRunnable<Params, Result>() {
+            public Result call() throws Exception {
+                mTaskInvoked.set(true);
+
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                //noinspection unchecked
+                return postResult(doInBackground(mParams));
+            }
+        };
+
+        mFuture = new FutureTask<Result>(mWorker) {
+            @Override
+            protected void done() {
+                try {
+                    postResultIfNotInvoked(get());
+                } catch (InterruptedException e) {
+                    android.util.Log.w(LOG_TAG, e);
+                } catch (ExecutionException e) {
+                    throw new RuntimeException("An error occured while executing doInBackground()",
+                            e.getCause());
+                } catch (CancellationException e) {
+                    postResultIfNotInvoked(null);
+                }
+            }
+        };
+    }
+
+    private void postResultIfNotInvoked(Result result) {
+        final boolean wasTaskInvoked = mTaskInvoked.get();
+        if (!wasTaskInvoked) {
+            postResult(result);
+        }
+    }
+
+    private Result postResult(Result result) {
+        @SuppressWarnings("unchecked")
+        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
+                new AsyncTaskResult<Result>(this, result));
+        message.sendToTarget();
+        return result;
+    }
+
+    /**
+     * Returns the current status of this task.
+     *
+     * @return The current status.
+     */
+    public final Status getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Override this method to perform a computation on a background thread. The
+     * specified parameters are the parameters passed to {@link #execute}
+     * by the caller of this task.
+     *
+     * This method can call {@link #publishProgress} to publish updates
+     * on the UI thread.
+     *
+     * @param params The parameters of the task.
+     *
+     * @return A result, defined by the subclass of this task.
+     *
+     * @see #onPreExecute()
+     * @see #onPostExecute
+     * @see #publishProgress
+     */
+    protected abstract Result doInBackground(Params... params);
+
+    /**
+     * Runs on the UI thread before {@link #doInBackground}.
+     *
+     * @see #onPostExecute
+     * @see #doInBackground
+     */
+    protected void onPreExecute() {
+    }
+
+    /**
+     * <p>Runs on the UI thread after {@link #doInBackground}. The
+     * specified result is the value returned by {@link #doInBackground}.</p>
+     *
+     * <p>This method won't be invoked if the task was cancelled.</p>
+     *
+     * @param result The result of the operation computed by {@link #doInBackground}.
+     *
+     * @see #onPreExecute
+     * @see #doInBackground
+     * @see #onCancelled(Object)
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected void onPostExecute(Result result) {
+    }
+
+    /**
+     * Runs on the UI thread after {@link #publishProgress} is invoked.
+     * The specified values are the values passed to {@link #publishProgress}.
+     *
+     * @param values The values indicating progress.
+     *
+     * @see #publishProgress
+     * @see #doInBackground
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected void onProgressUpdate(Progress... values) {
+    }
+
+    /**
+     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+     * {@link #doInBackground(Object[])} has finished.</p>
+     *
+     * <p>The default implementation simply invokes {@link #onCancelled()} and
+     * ignores the result. If you write your own implementation, do not call
+     * <code>super.onCancelled(result)</code>.</p>
+     *
+     * @param result The result, if any, computed in
+     *               {@link #doInBackground(Object[])}, can be null
+     *
+     * @see #cancel(boolean)
+     * @see #isCancelled()
+     */
+    @SuppressWarnings({"UnusedParameters"})
+    protected void onCancelled(Result result) {
+        onCancelled();
+    }
+
+    /**
+     * <p>Applications should preferably override {@link #onCancelled(Object)}.
+     * This method is invoked by the default implementation of
+     * {@link #onCancelled(Object)}.</p>
+     *
+     * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
+     * {@link #doInBackground(Object[])} has finished.</p>
+     *
+     * @see #onCancelled(Object)
+     * @see #cancel(boolean)
+     * @see #isCancelled()
+     */
+    protected void onCancelled() {
+    }
+
+    /**
+     * Returns <tt>true</tt> if this task was cancelled before it completed
+     * normally. If you are calling {@link #cancel(boolean)} on the task,
+     * the value returned by this method should be checked periodically from
+     * {@link #doInBackground(Object[])} to end the task as soon as possible.
+     *
+     * @return <tt>true</tt> if task was cancelled before it completed
+     *
+     * @see #cancel(boolean)
+     */
+    public final boolean isCancelled() {
+        return mCancelled.get();
+    }
+
+    /**
+     * <p>Attempts to cancel execution of this task.  This attempt will
+     * fail if the task has already completed, already been cancelled,
+     * or could not be cancelled for some other reason. If successful,
+     * and this task has not started when <tt>cancel</tt> is called,
+     * this task should never run. If the task has already started,
+     * then the <tt>mayInterruptIfRunning</tt> parameter determines
+     * whether the thread executing this task should be interrupted in
+     * an attempt to stop the task.</p>
+     *
+     * <p>Calling this method will result in {@link #onCancelled(Object)} being
+     * invoked on the UI thread after {@link #doInBackground(Object[])}
+     * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
+     * is never invoked. After invoking this method, you should check the
+     * value returned by {@link #isCancelled()} periodically from
+     * {@link #doInBackground(Object[])} to finish the task as early as
+     * possible.</p>
+     *
+     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
+     *        task should be interrupted; otherwise, in-progress tasks are allowed
+     *        to complete.
+     *
+     * @return <tt>false</tt> if the task could not be cancelled,
+     *         typically because it has already completed normally;
+     *         <tt>true</tt> otherwise
+     *
+     * @see #isCancelled()
+     * @see #onCancelled(Object)
+     */
+    public final boolean cancel(boolean mayInterruptIfRunning) {
+        mCancelled.set(true);
+        return mFuture.cancel(mayInterruptIfRunning);
+    }
+
+    /**
+     * Waits if necessary for the computation to complete, and then
+     * retrieves its result.
+     *
+     * @return The computed result.
+     *
+     * @throws CancellationException If the computation was cancelled.
+     * @throws ExecutionException If the computation threw an exception.
+     * @throws InterruptedException If the current thread was interrupted
+     *         while waiting.
+     */
+    public final Result get() throws InterruptedException, ExecutionException {
+        return mFuture.get();
+    }
+
+    /**
+     * Waits if necessary for at most the given time for the computation
+     * to complete, and then retrieves its result.
+     *
+     * @param timeout Time to wait before cancelling the operation.
+     * @param unit The time unit for the timeout.
+     *
+     * @return The computed result.
+     *
+     * @throws CancellationException If the computation was cancelled.
+     * @throws ExecutionException If the computation threw an exception.
+     * @throws InterruptedException If the current thread was interrupted
+     *         while waiting.
+     * @throws TimeoutException If the wait timed out.
+     */
+    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
+            ExecutionException, TimeoutException {
+        return mFuture.get(timeout, unit);
+    }
+
+    /**
+     * Executes the task with the specified parameters. The task returns
+     * itself (this) so that the caller can keep a reference to it.
+     *
+     * <p>Note: this function schedules the task on a queue for a single background
+     * thread or pool of threads depending on the platform version.  When first
+     * introduced, AsyncTasks were executed serially on a single background thread.
+     * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
+     * to a pool of threads allowing multiple tasks to operate in parallel. Starting
+     * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
+     * executed on a single thread to avoid common application errors caused
+     * by parallel execution.  If you truly want parallel execution, you can use
+     * the {@link #executeOnExecutor} version of this method
+     * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
+     * on its use.
+     *
+     * <p>This method must be invoked on the UI thread.
+     *
+     * @param params The parameters of the task.
+     *
+     * @return This instance of AsyncTask.
+     *
+     * @throws IllegalStateException If {@link #getStatus()} returns either
+     *         {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+     *
+     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+     * @see #execute(Runnable)
+     */
+    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
+        return executeOnExecutor(sDefaultExecutor, params);
+    }
+
+    /**
+     * Executes the task with the specified parameters. The task returns
+     * itself (this) so that the caller can keep a reference to it.
+     *
+     * <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
+     * allow multiple tasks to run in parallel on a pool of threads managed by
+     * AsyncTask, however you can also use your own {@link Executor} for custom
+     * behavior.
+     *
+     * <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
+     * a thread pool is generally <em>not</em> what one wants, because the order
+     * of their operation is not defined.  For example, if these tasks are used
+     * to modify any state in common (such as writing a file due to a button click),
+     * there are no guarantees on the order of the modifications.
+     * Without careful work it is possible in rare cases for the newer version
+     * of the data to be over-written by an older one, leading to obscure data
+     * loss and stability issues.  Such changes are best
+     * executed in serial; to guarantee such work is serialized regardless of
+     * platform version you can use this function with {@link #SERIAL_EXECUTOR}.
+     *
+     * <p>This method must be invoked on the UI thread.
+     *
+     * @param exec The executor to use.  {@link #THREAD_POOL_EXECUTOR} is available as a
+     *              convenient process-wide thread pool for tasks that are loosely coupled.
+     * @param params The parameters of the task.
+     *
+     * @return This instance of AsyncTask.
+     *
+     * @throws IllegalStateException If {@link #getStatus()} returns either
+     *         {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
+     *
+     * @see #execute(Object[])
+     */
+    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
+            Params... params) {
+        if (mStatus != Status.PENDING) {
+            switch (mStatus) {
+                case RUNNING:
+                    throw new IllegalStateException("Cannot execute task:"
+                            + " the task is already running.");
+                case FINISHED:
+                    throw new IllegalStateException("Cannot execute task:"
+                            + " the task has already been executed "
+                            + "(a task can be executed only once)");
+            }
+        }
+
+        mStatus = Status.RUNNING;
+
+        onPreExecute();
+
+        mWorker.mParams = params;
+        exec.execute(mFuture);
+
+        return this;
+    }
+
+    /**
+     * Convenience version of {@link #execute(Object...)} for use with
+     * a simple Runnable object. See {@link #execute(Object[])} for more
+     * information on the order of execution.
+     *
+     * @see #execute(Object[])
+     * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
+     */
+    public static void execute(Runnable runnable) {
+        sDefaultExecutor.execute(runnable);
+    }
+
+    /**
+     * This method can be invoked from {@link #doInBackground} to
+     * publish updates on the UI thread while the background computation is
+     * still running. Each call to this method will trigger the execution of
+     * {@link #onProgressUpdate} on the UI thread.
+     *
+     * {@link #onProgressUpdate} will note be called if the task has been
+     * canceled.
+     *
+     * @param values The progress values to update the UI with.
+     *
+     * @see #onProgressUpdate
+     * @see #doInBackground
+     */
+    protected final void publishProgress(Progress... values) {
+        if (!isCancelled()) {
+            sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
+                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
+        }
+    }
+
+    private void finish(Result result) {
+        if (isCancelled()) {
+            onCancelled(result);
+        } else {
+            onPostExecute(result);
+        }
+        mStatus = Status.FINISHED;
+    }
+
+    private static class InternalHandler extends Handler {
+        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
+        @Override
+        public void handleMessage(Message msg) {
+            AsyncTaskResult result = (AsyncTaskResult) msg.obj;
+            switch (msg.what) {
+                case MESSAGE_POST_RESULT:
+                    // There is only one result
+                    result.mTask.finish(result.mData[0]);
+                    break;
+                case MESSAGE_POST_PROGRESS:
+                    result.mTask.onProgressUpdate(result.mData);
+                    break;
+            }
+        }
+    }
+
+    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
+        Params[] mParams;
+    }
+
+    @SuppressWarnings({"RawUseOfParameterizedType"})
+    private static class AsyncTaskResult<Data> {
+        final AsyncTask mTask;
+        final Data[] mData;
+
+        AsyncTaskResult(AsyncTask task, Data... data) {
+            mTask = task;
+            mData = data;
+        }
+    }
+}
diff --git a/platform/android/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&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+     * (if such an element exists).
+     * Returns <tt>true</tt> if this deque contained the specified element
+     * (or equivalently, if this deque changed as a result of the call).
+     *
+     * @param o element to be removed from this deque, if present
+     * @return <tt>true</tt> if an element was removed as a result of this call
+     * @throws ClassCastException if the class of the specified element
+     *         is incompatible with this deque (optional)
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements (optional)
+     */
+    boolean removeFirstOccurrence(Object o);
+
+    /**
+     * Removes the last occurrence of the specified element from this deque.
+     * If the deque does not contain the element, it is unchanged.
+     * More formally, removes the last element <tt>e</tt> such that
+     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+     * (if such an element exists).
+     * Returns <tt>true</tt> if this deque contained the specified element
+     * (or equivalently, if this deque changed as a result of the call).
+     *
+     * @param o element to be removed from this deque, if present
+     * @return <tt>true</tt> if an element was removed as a result of this call
+     * @throws ClassCastException if the class of the specified element
+     *         is incompatible with this deque (optional)
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements (optional)
+     */
+    boolean removeLastOccurrence(Object o);
+
+    // *** Queue methods ***
+
+    /**
+     * Inserts the specified element into the queue represented by this deque
+     * (in other words, at the tail of this deque) if it is possible to do so
+     * immediately without violating capacity restrictions, returning
+     * <tt>true</tt> upon success and throwing an
+     * <tt>IllegalStateException</tt> if no space is currently available.
+     * When using a capacity-restricted deque, it is generally preferable to
+     * use {@link #offer(Object) offer}.
+     *
+     * <p>This method is equivalent to {@link #addLast}.
+     *
+     * @param e the element to add
+     * @return <tt>true</tt> (as specified by {@link Collection#add})
+     * @throws IllegalStateException if the element cannot be added at this
+     *         time due to capacity restrictions
+     * @throws ClassCastException if the class of the specified element
+     *         prevents it from being added to this deque
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements
+     * @throws IllegalArgumentException if some property of the specified
+     *         element prevents it from being added to this deque
+     */
+    boolean add(E e);
+
+    /**
+     * Inserts the specified element into the queue represented by this deque
+     * (in other words, at the tail of this deque) if it is possible to do so
+     * immediately without violating capacity restrictions, returning
+     * <tt>true</tt> upon success and <tt>false</tt> if no space is currently
+     * available.  When using a capacity-restricted deque, this method is
+     * generally preferable to the {@link #add} method, which can fail to
+     * insert an element only by throwing an exception.
+     *
+     * <p>This method is equivalent to {@link #offerLast}.
+     *
+     * @param e the element to add
+     * @return <tt>true</tt> if the element was added to this deque, else
+     *         <tt>false</tt>
+     * @throws ClassCastException if the class of the specified element
+     *         prevents it from being added to this deque
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements
+     * @throws IllegalArgumentException if some property of the specified
+     *         element prevents it from being added to this deque
+     */
+    boolean offer(E e);
+
+    /**
+     * Retrieves and removes the head of the queue represented by this deque
+     * (in other words, the first element of this deque).
+     * This method differs from {@link #poll poll} only in that it throws an
+     * exception if this deque is empty.
+     *
+     * <p>This method is equivalent to {@link #removeFirst()}.
+     *
+     * @return the head of the queue represented by this deque
+     * @throws NoSuchElementException if this deque is empty
+     */
+    E remove();
+
+    /**
+     * Retrieves and removes the head of the queue represented by this deque
+     * (in other words, the first element of this deque), or returns
+     * <tt>null</tt> if this deque is empty.
+     *
+     * <p>This method is equivalent to {@link #pollFirst()}.
+     *
+     * @return the first element of this deque, or <tt>null</tt> if
+     *         this deque is empty
+     */
+    E poll();
+
+    /**
+     * Retrieves, but does not remove, the head of the queue represented by
+     * this deque (in other words, the first element of this deque).
+     * This method differs from {@link #peek peek} only in that it throws an
+     * exception if this deque is empty.
+     *
+     * <p>This method is equivalent to {@link #getFirst()}.
+     *
+     * @return the head of the queue represented by this deque
+     * @throws NoSuchElementException if this deque is empty
+     */
+    E element();
+
+    /**
+     * Retrieves, but does not remove, the head of the queue represented by
+     * this deque (in other words, the first element of this deque), or
+     * returns <tt>null</tt> if this deque is empty.
+     *
+     * <p>This method is equivalent to {@link #peekFirst()}.
+     *
+     * @return the head of the queue represented by this deque, or
+     *         <tt>null</tt> if this deque is empty
+     */
+    E peek();
+
+
+    // *** Stack methods ***
+
+    /**
+     * Pushes an element onto the stack represented by this deque (in other
+     * words, at the head of this deque) if it is possible to do so
+     * immediately without violating capacity restrictions, returning
+     * <tt>true</tt> upon success and throwing an
+     * <tt>IllegalStateException</tt> if no space is currently available.
+     *
+     * <p>This method is equivalent to {@link #addFirst}.
+     *
+     * @param e the element to push
+     * @throws IllegalStateException if the element cannot be added at this
+     *         time due to capacity restrictions
+     * @throws ClassCastException if the class of the specified element
+     *         prevents it from being added to this deque
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements
+     * @throws IllegalArgumentException if some property of the specified
+     *         element prevents it from being added to this deque
+     */
+    void push(E e);
+
+    /**
+     * Pops an element from the stack represented by this deque.  In other
+     * words, removes and returns the first element of this deque.
+     *
+     * <p>This method is equivalent to {@link #removeFirst()}.
+     *
+     * @return the element at the front of this deque (which is the top
+     *         of the stack represented by this deque)
+     * @throws NoSuchElementException if this deque is empty
+     */
+    E pop();
+
+
+    // *** Collection methods ***
+
+    /**
+     * Removes the first occurrence of the specified element from this deque.
+     * If the deque does not contain the element, it is unchanged.
+     * More formally, removes the first element <tt>e</tt> such that
+     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
+     * (if such an element exists).
+     * Returns <tt>true</tt> if this deque contained the specified element
+     * (or equivalently, if this deque changed as a result of the call).
+     *
+     * <p>This method is equivalent to {@link #removeFirstOccurrence}.
+     *
+     * @param o element to be removed from this deque, if present
+     * @return <tt>true</tt> if an element was removed as a result of this call
+     * @throws ClassCastException if the class of the specified element
+     *         is incompatible with this deque (optional)
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements (optional)
+     */
+    boolean remove(Object o);
+
+    /**
+     * Returns <tt>true</tt> if this deque contains the specified element.
+     * More formally, returns <tt>true</tt> if and only if this deque contains
+     * at least one element <tt>e</tt> such that
+     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
+     *
+     * @param o element whose presence in this deque is to be tested
+     * @return <tt>true</tt> if this deque contains the specified element
+     * @throws ClassCastException if the type of the specified element
+     *         is incompatible with this deque (optional)
+     * @throws NullPointerException if the specified element is null and this
+     *         deque does not permit null elements (optional)
+     */
+    boolean contains(Object o);
+
+    /**
+     * Returns the number of elements in this deque.
+     *
+     * @return the number of elements in this deque
+     */
+    public int size();
+
+    /**
+     * Returns an iterator over the elements in this deque in proper sequence.
+     * The elements will be returned in order from first (head) to last (tail).
+     *
+     * @return an iterator over the elements in this deque in proper sequence
+     */
+    Iterator<E> iterator();
+
+    /**
+     * Returns an iterator over the elements in this deque in reverse
+     * sequential order.  The elements will be returned in order from
+     * last (tail) to first (head).
+     *
+     * @return an iterator over the elements in this deque in reverse
+     * sequence
+     */
+    Iterator<E> descendingIterator();
+
+}
diff --git a/platform/android/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
+}
-- 
cgit v1.2.3