Import viewer library.
diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..61a9707
--- /dev/null
+++ b/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+	package="com.artifex.mupdf.viewer"
+	>
+	<application>
+		<activity
+			android:name=".DocumentActivity"
+			android:configChanges="orientation|screenSize|keyboardHidden"
+			>
+			<intent-filter>
+				<action android:name="android.intent.action.VIEW" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<category android:name="android.intent.category.BROWSABLE" />
+				<data android:mimeType="application/pdf" />
+				<data android:mimeType="application/vnd.ms-xpsdocument" />
+				<data android:mimeType="application/oxps" />
+				<data android:mimeType="application/x-cbz" />
+				<data android:mimeType="application/epub+zip" />
+				<data android:mimeType="text/xml" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.VIEW" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<category android:name="android.intent.category.BROWSABLE" />
+				<data android:pathPattern=".*\\.pdf" />
+				<data android:pathPattern=".*\\.xps" />
+				<data android:pathPattern=".*\\.oxps" />
+				<data android:pathPattern=".*\\.cbz" />
+				<data android:pathPattern=".*\\.epub" />
+				<data android:pathPattern=".*\\.fb2" />
+			</intent-filter>
+		</activity>
+		<activity
+			android:name=".OutlineActivity"
+			android:configChanges="orientation|screenSize|keyboardHidden"
+			>
+		</activity>
+	</application>
+</manifest>
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java
new file mode 100644
index 0000000..6a76575
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableAsyncTask.java
@@ -0,0 +1,85 @@
+package com.artifex.mupdf.viewer;
+
+import android.os.AsyncTask;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+// Ideally this would be a subclass of AsyncTask, however the cancel() method is final, and cannot
+// be overridden. I felt that having two different, but similar cancel methods was a bad idea.
+public class CancellableAsyncTask<Params, Result>
+{
+	private final AsyncTask<Params, Void, Result> asyncTask;
+	private final CancellableTaskDefinition<Params, Result> ourTask;
+
+	public void onPreExecute()
+	{
+
+	}
+
+	public void onPostExecute(Result result)
+	{
+
+	}
+
+	public CancellableAsyncTask(final CancellableTaskDefinition<Params, Result> task)
+	{
+		if (task == null)
+				throw new IllegalArgumentException();
+
+		this.ourTask = task;
+		asyncTask = new AsyncTask<Params, Void, Result>()
+				{
+					@Override
+					protected Result doInBackground(Params... params)
+					{
+						return task.doInBackground(params);
+					}
+
+					@Override
+					protected void onPreExecute()
+					{
+						CancellableAsyncTask.this.onPreExecute();
+					}
+
+					@Override
+					protected void onPostExecute(Result result)
+					{
+						CancellableAsyncTask.this.onPostExecute(result);
+						task.doCleanup();
+					}
+
+					@Override
+					protected void onCancelled(Result result)
+					{
+						task.doCleanup();
+					}
+				};
+	}
+
+	public void cancel()
+	{
+		this.asyncTask.cancel(true);
+		ourTask.doCancel();
+
+		try
+		{
+			this.asyncTask.get();
+		}
+		catch (InterruptedException e)
+		{
+		}
+		catch (ExecutionException e)
+		{
+		}
+		catch (CancellationException e)
+		{
+		}
+	}
+
+	public void execute(Params ... params)
+	{
+		asyncTask.execute(params);
+	}
+
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java
new file mode 100644
index 0000000..2833969
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/CancellableTaskDefinition.java
@@ -0,0 +1,8 @@
+package com.artifex.mupdf.viewer;
+
+public interface CancellableTaskDefinition <Params, Result>
+{
+	public Result doInBackground(Params ... params);
+	public void doCancel();
+	public void doCleanup();
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java b/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
new file mode 100644
index 0000000..98326bf
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/DocumentActivity.java
@@ -0,0 +1,712 @@
+package com.artifex.mupdf.viewer;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface.OnCancelListener;
+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.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.method.PasswordTransformationMethod;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+public class DocumentActivity extends Activity
+{
+	/* The core rendering instance */
+	enum TopBarMode {Main, Search, More};
+
+	private final int    OUTLINE_REQUEST=0;
+	private MuPDFCore    core;
+	private String       mFileName;
+	private ReaderView   mDocView;
+	private View         mButtonsView;
+	private boolean      mButtonsVisible;
+	private EditText     mPasswordView;
+	private TextView     mFilenameView;
+	private SeekBar      mPageSlider;
+	private int          mPageSliderRes;
+	private TextView     mPageNumberView;
+	private ImageButton  mSearchButton;
+	private ImageButton  mOutlineButton;
+	private ViewAnimator mTopBarSwitcher;
+	private ImageButton  mLinkButton;
+	private TopBarMode   mTopBarMode = TopBarMode.Main;
+	private ImageButton  mSearchBack;
+	private ImageButton  mSearchFwd;
+	private ImageButton  mSearchClose;
+	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 AlertDialog mAlertDialog;
+	private ArrayList<OutlineActivity.Item> mFlatOutline;
+
+	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(path);
+		}
+		catch (Exception e)
+		{
+			System.out.println(e);
+			return null;
+		}
+		catch (java.lang.OutOfMemoryError e)
+		{
+			//  out of memory is not an Exception, so we catch it separately.
+			System.out.println(e);
+			return null;
+		}
+		return core;
+	}
+
+	private MuPDFCore openBuffer(byte buffer[], String magic)
+	{
+		System.out.println("Trying to open byte buffer");
+		try
+		{
+			core = new MuPDFCore(buffer, magic);
+		}
+		catch (Exception e)
+		{
+			System.out.println(e);
+			return null;
+		}
+		return core;
+	}
+
+	/** Called when the activity is first created. */
+	@Override
+	public void onCreate(final Bundle savedInstanceState)
+	{
+		super.onCreate(savedInstanceState);
+
+		requestWindowFeature(Window.FEATURE_NO_TITLE);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+		mAlertBuilder = new AlertDialog.Builder(this);
+
+		if (core == null) {
+			if (savedInstanceState != null && savedInstanceState.containsKey("FileName")) {
+				mFileName = savedInstanceState.getString("FileName");
+			}
+		}
+		if (core == null) {
+			Intent intent = getIntent();
+			byte buffer[] = null;
+
+			if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+				Uri uri = intent.getData();
+				System.out.println("URI to open is: " + uri);
+				if (uri.toString().startsWith("content://")) {
+					String reason = null;
+					try {
+						InputStream is = getContentResolver().openInputStream(uri);
+						int len;
+						ByteArrayOutputStream bufferStream = new ByteArrayOutputStream();
+						byte[] data = new byte[16384];
+						while ((len = is.read(data, 0, data.length)) != -1) {
+							bufferStream.write(data, 0, len);
+						}
+						bufferStream.flush();
+						buffer = bufferStream.toByteArray();
+						is.close();
+					}
+					catch (java.lang.OutOfMemoryError e) {
+						System.out.println("Out of memory during buffer reading");
+						reason = e.toString();
+					}
+					catch (Exception e) {
+						System.out.println("Exception reading from stream: " + e);
+
+						// Handle view requests from the Transformer Prime's file manager
+						// Hopefully other file managers will use this same scheme, if not
+						// using explicit paths.
+						// I'm hoping that this case below is no longer needed...but it's
+						// hard to test as the file manager seems to have changed in 4.x.
+						try {
+							Cursor cursor = getContentResolver().query(uri, new String[]{"_data"}, null, null, null);
+							if (cursor.moveToFirst()) {
+								String str = cursor.getString(0);
+								if (str == null) {
+									reason = "Couldn't parse data in intent";
+								}
+								else {
+									uri = Uri.parse(str);
+								}
+							}
+						}
+						catch (Exception e2) {
+							System.out.println("Exception in Transformer Prime file manager code: " + e2);
+							reason = e2.toString();
+						}
+					}
+					if (reason != null) {
+						buffer = null;
+						Resources res = getResources();
+						AlertDialog alert = mAlertBuilder.create();
+						setTitle(String.format(res.getString(R.string.cannot_open_document_Reason), reason));
+						alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
+								new DialogInterface.OnClickListener() {
+									public void onClick(DialogInterface dialog, int which) {
+										finish();
+									}
+								});
+						alert.show();
+						return;
+					}
+				}
+				if (buffer != null) {
+					core = openBuffer(buffer, intent.getType());
+				} else {
+					String path = Uri.decode(uri.getEncodedPath());
+					if (path == null) {
+						path = uri.toString();
+					}
+					core = openFile(path);
+				}
+				SearchTaskResult.set(null);
+			}
+			if (core != null && core.needsPassword()) {
+				requestPassword(savedInstanceState);
+				return;
+			}
+			if (core != null && core.countPages() == 0)
+			{
+				core = null;
+			}
+		}
+		if (core == null)
+		{
+			AlertDialog alert = mAlertBuilder.create();
+			alert.setTitle(R.string.cannot_open_document);
+			alert.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.dismiss),
+					new DialogInterface.OnClickListener() {
+						public void onClick(DialogInterface dialog, int which) {
+							finish();
+						}
+					});
+			alert.setOnCancelListener(new OnCancelListener() {
+
+				@Override
+				public void onCancel(DialogInterface dialog) {
+					finish();
+				}
+			});
+			alert.show();
+			return;
+		}
+
+		createUI(savedInstanceState);
+	}
+
+	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 ReaderView(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();
+			}
+		};
+		mDocView.setAdapter(new PageAdapter(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.pushHistory();
+				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();
+			}
+		});
+
+		mSearchClose.setOnClickListener(new View.OnClickListener() {
+			public void onClick(View v) {
+				searchModeOff();
+			}
+		});
+
+		// 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) {
+					if (mFlatOutline == null)
+						mFlatOutline = core.getOutline();
+					if (mFlatOutline != null) {
+						Intent intent = new Intent(DocumentActivity.this, OutlineActivity.class);
+						Bundle bundle = new Bundle();
+						bundle.putInt("POSITION", mDocView.getDisplayedViewIndex());
+						bundle.putSerializable("OUTLINE", mFlatOutline);
+						intent.putExtras(bundle);
+						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();
+
+		// Stick the document view and the buttons overlay into a parent view
+		RelativeLayout layout = new RelativeLayout(this);
+		layout.setBackgroundColor(Color.DKGRAY);
+		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 >= RESULT_FIRST_USER) {
+				mDocView.pushHistory();
+				mDocView.setDisplayedViewIndex(resultCode-RESULT_FIRST_USER);
+			}
+			break;
+		}
+		super.onActivityResult(requestCode, resultCode, data);
+	}
+
+	@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);
+	}
+
+	@Override
+	protected void onPause() {
+		super.onPause();
+
+		if (mSearchTask != null)
+			mSearchTask.stop();
+
+		if (mFileName != null && mDocView != null) {
+			SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+			SharedPreferences.Editor edit = prefs.edit();
+			edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex());
+			edit.commit();
+		}
+	}
+
+	public void onDestroy()
+	{
+		if (mDocView != null) {
+			mDocView.applyToChildren(new ReaderView.ViewMapper() {
+				void applyToView(View view) {
+					((PageView)view).releaseBitmaps();
+				}
+			});
+		}
+		if (core != null)
+			core.onDestroy();
+		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, 0x00, 0x66, 0xCC) : 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 makeButtonsView() {
+		mButtonsView = getLayoutInflater().inflate(R.layout.document_activity,null);
+		mFilenameView = (TextView)mButtonsView.findViewById(R.id.docNameText);
+		mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider);
+		mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber);
+		mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton);
+		mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton);
+		mTopBarSwitcher = (ViewAnimator)mButtonsView.findViewById(R.id.switcher);
+		mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack);
+		mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward);
+		mSearchClose = (ImageButton)mButtonsView.findViewById(R.id.searchClose);
+		mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText);
+		mLinkButton = (ImageButton)mButtonsView.findViewById(R.id.linkButton);
+		mTopBarSwitcher.setVisibility(View.INVISIBLE);
+		mPageNumberView.setVisibility(View.INVISIBLE);
+
+		mPageSlider.setVisibility(View.INVISIBLE);
+	}
+
+	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() {
+		super.onStart();
+	}
+
+	@Override
+	protected void onStop() {
+		super.onStop();
+	}
+
+	@Override
+	public void onBackPressed() {
+		if (!mDocView.popHistory())
+			super.onBackPressed();
+	}
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java
new file mode 100644
index 0000000..b586eb8
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCancellableTaskDefinition.java
@@ -0,0 +1,40 @@
+package com.artifex.mupdf.viewer;
+
+import com.artifex.mupdf.fitz.Cookie;
+
+public abstract class MuPDFCancellableTaskDefinition<Params, Result> implements CancellableTaskDefinition<Params, Result>
+{
+	private Cookie cookie;
+
+	public MuPDFCancellableTaskDefinition()
+	{
+		this.cookie = new Cookie();
+	}
+
+	@Override
+	public void doCancel()
+	{
+		if (cookie == null)
+			return;
+
+		cookie.abort();
+	}
+
+	@Override
+	public void doCleanup()
+	{
+		if (cookie == null)
+			return;
+
+		cookie.destroy();
+		cookie = null;
+	}
+
+	@Override
+	public final Result doInBackground(Params ... params)
+	{
+		return doInBackground(cookie, params);
+	}
+
+	public abstract Result doInBackground(Cookie cookie, Params ... params);
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
new file mode 100644
index 0000000..e807c45
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/MuPDFCore.java
@@ -0,0 +1,161 @@
+package com.artifex.mupdf.viewer;
+
+import com.artifex.mupdf.fitz.Cookie;
+import com.artifex.mupdf.fitz.Document;
+import com.artifex.mupdf.fitz.Outline;
+import com.artifex.mupdf.fitz.Page;
+import com.artifex.mupdf.fitz.Link;
+import com.artifex.mupdf.fitz.DisplayList;
+import com.artifex.mupdf.fitz.Rect;
+import com.artifex.mupdf.fitz.RectI;
+import com.artifex.mupdf.fitz.Matrix;
+import com.artifex.mupdf.fitz.android.AndroidDrawDevice;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import java.util.ArrayList;
+
+public class MuPDFCore
+{
+	private int resolution;
+	private Document doc;
+	private Outline[] outline;
+	private int pageCount = -1;
+	private int currentPage;
+	private Page page;
+	private float pageWidth;
+	private float pageHeight;
+	private DisplayList displayList;
+
+	public MuPDFCore(String filename) {
+		doc = Document.openDocument(filename);
+		pageCount = doc.countPages();
+		resolution = 160;
+		currentPage = -1;
+	}
+
+	public MuPDFCore(byte buffer[], String magic) {
+		doc = Document.openDocument(buffer, magic);
+		pageCount = doc.countPages();
+		resolution = 160;
+		currentPage = -1;
+	}
+
+	public int countPages() {
+		return pageCount;
+	}
+
+	private synchronized void gotoPage(int pageNum) {
+		/* TODO: page cache */
+		if (pageNum > pageCount-1)
+			pageNum = pageCount-1;
+		else if (pageNum < 0)
+			pageNum = 0;
+		if (pageNum != currentPage) {
+			currentPage = pageNum;
+			if (page != null)
+				page.destroy();
+			page = null;
+			if (displayList != null)
+				displayList.destroy();
+			displayList = null;
+			page = doc.loadPage(pageNum);
+			Rect b = page.getBounds();
+			pageWidth = b.x1 - b.x0;
+			pageHeight = b.y1 - b.y0;
+		}
+	}
+
+	public synchronized PointF getPageSize(int pageNum) {
+		gotoPage(pageNum);
+		return new PointF(pageWidth, pageHeight);
+	}
+
+	public synchronized void onDestroy() {
+		if (displayList != null)
+			displayList.destroy();
+		displayList = null;
+		if (page != null)
+			page.destroy();
+		page = null;
+		if (doc != null)
+			doc.destroy();
+		doc = null;
+	}
+
+	public synchronized void drawPage(Bitmap bm, int pageNum,
+			int pageW, int pageH,
+			int patchX, int patchY,
+			int patchW, int patchH,
+			Cookie cookie) {
+		gotoPage(pageNum);
+
+		if (displayList == null)
+			displayList = page.toDisplayList(false);
+
+		float zoom = resolution / 72;
+		Matrix ctm = new Matrix(zoom, zoom);
+		RectI bbox = new RectI(page.getBounds().transform(ctm));
+		float xscale = (float)pageW / (float)(bbox.x1-bbox.x0);
+		float yscale = (float)pageH / (float)(bbox.y1-bbox.y0);
+		ctm.scale(xscale, yscale);
+
+		AndroidDrawDevice dev = new AndroidDrawDevice(bm, patchX, patchY);
+		displayList.run(dev, ctm, cookie);
+		dev.destroy();
+	}
+
+	public synchronized void updatePage(Bitmap bm, int pageNum,
+			int pageW, int pageH,
+			int patchX, int patchY,
+			int patchW, int patchH,
+			Cookie cookie) {
+		drawPage(bm, pageNum, pageW, pageH, patchX, patchY, patchW, patchH, cookie);
+	}
+
+	public synchronized Link[] getPageLinks(int pageNum) {
+		gotoPage(pageNum);
+		return page.getLinks();
+	}
+
+	public synchronized RectF[] searchPage(int pageNum, String text) {
+		gotoPage(pageNum);
+		Rect[] rs = page.search(text);
+		RectF[] rfs = new RectF[rs.length];
+		for (int i=0; i < rs.length; ++i)
+			rfs[i] = new RectF(rs[i].x0, rs[i].y0, rs[i].x1, rs[i].y1);
+		return rfs;
+	}
+
+	public synchronized boolean hasOutline() {
+		if (outline == null)
+			outline = doc.loadOutline();
+		return outline != null;
+	}
+
+	private void flattenOutlineNodes(ArrayList<OutlineActivity.Item> result, Outline list[], String indent) {
+		for (Outline node : list) {
+			if (node.title != null)
+				result.add(new OutlineActivity.Item(indent + node.title, node.page));
+			if (node.down != null)
+				flattenOutlineNodes(result, node.down, indent + "    ");
+		}
+	}
+
+	public synchronized ArrayList<OutlineActivity.Item> getOutline() {
+		ArrayList<OutlineActivity.Item> result = new ArrayList<OutlineActivity.Item>();
+		flattenOutlineNodes(result, outline, "");
+		return result;
+	}
+
+	public synchronized boolean needsPassword() {
+		return doc.needsPassword();
+	}
+
+	public synchronized boolean authenticatePassword(String password) {
+		return doc.authenticatePassword(password);
+	}
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java b/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java
new file mode 100644
index 0000000..dfda3cf
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/OutlineActivity.java
@@ -0,0 +1,57 @@
+package com.artifex.mupdf.viewer;
+
+import android.app.ListActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+public class OutlineActivity extends ListActivity
+{
+	public static class Item implements Serializable {
+		public String title;
+		public int page;
+		public Item(String title, int page) {
+			this.title = title;
+			this.page = page;
+		}
+		public String toString() {
+			return title;
+		}
+	}
+
+	protected ArrayAdapter<Item> adapter;
+
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		requestWindowFeature(Window.FEATURE_NO_TITLE);
+		getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+		adapter = new ArrayAdapter<Item>(this, android.R.layout.simple_list_item_1);
+		setListAdapter(adapter);
+
+		Bundle bundle = getIntent().getExtras();
+		int currentPage = bundle.getInt("POSITION");
+		ArrayList<Item> outline = (ArrayList<Item>)bundle.getSerializable("OUTLINE");
+		int found = -1;
+		for (int i = 0; i < outline.size(); ++i) {
+			Item item = outline.get(i);
+			if (found < 0 && item.page >= currentPage)
+				found = i;
+			adapter.add(item);
+		}
+		if (found >= 0)
+			setSelection(found);
+	}
+
+	protected void onListItemClick(ListView l, View v, int position, long id) {
+		Item item = adapter.getItem(position);
+		setResult(RESULT_FIRST_USER + item.page);
+		finish();
+	}
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java b/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
new file mode 100644
index 0000000..7b48722
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/PageAdapter.java
@@ -0,0 +1,86 @@
+package com.artifex.mupdf.viewer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.os.AsyncTask;
+
+public class PageAdapter extends BaseAdapter {
+	private final Context mContext;
+	private final MuPDFCore mCore;
+	private final SparseArray<PointF> mPageSizes = new SparseArray<PointF>();
+	private       Bitmap mSharedHqBm;
+
+	public PageAdapter(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 void releaseBitmaps()
+	{
+		//  recycle and release the shared bitmap.
+		if (mSharedHqBm!=null)
+			mSharedHqBm.recycle();
+		mSharedHqBm = null;
+	}
+
+	public View getView(final int position, View convertView, ViewGroup parent) {
+		final PageView pageView;
+		if (convertView == null) {
+			if (mSharedHqBm == null || mSharedHqBm.getWidth() != parent.getWidth() || mSharedHqBm.getHeight() != parent.getHeight())
+				mSharedHqBm = Bitmap.createBitmap(parent.getWidth(), parent.getHeight(), Bitmap.Config.ARGB_8888);
+
+			pageView = new PageView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight()), mSharedHqBm);
+		} else {
+			pageView = (PageView) 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/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java b/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
new file mode 100644
index 0000000..abdd23e
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/PageView.java
@@ -0,0 +1,539 @@
+package com.artifex.mupdf.viewer;
+
+import com.artifex.mupdf.fitz.Cookie;
+import com.artifex.mupdf.fitz.Link;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap.Config;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.text.method.PasswordTransformationMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.os.AsyncTask;
+
+// Make our ImageViews opaque to optimize redraw
+class OpaqueImageView extends ImageView {
+
+	public OpaqueImageView(Context context) {
+		super(context);
+	}
+
+	@Override
+	public boolean isOpaque() {
+		return true;
+	}
+}
+
+public class PageView extends ViewGroup {
+	private final MuPDFCore mCore;
+
+	private static final int HIGHLIGHT_COLOR = 0x80cc6600;
+	private static final int LINK_COLOR = 0x800066cc;
+	private static final int BOX_COLOR = 0xFF4444FF;
+	private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
+	private static final int PROGRESS_DIALOG_DELAY = 200;
+
+	protected final Context mContext;
+
+	protected     int       mPageNumber;
+	private       Point     mParentSize;
+	protected     Point     mSize;   // Size of page at minimum zoom
+	protected     float     mSourceScale;
+
+	private       ImageView mEntire; // Image rendered at minimum zoom
+	private       Bitmap    mEntireBm;
+	private       Matrix    mEntireMat;
+	private       AsyncTask<Void,Void,Link[]> mGetLinkInfo;
+	private       CancellableAsyncTask<Void, Void> mDrawEntire;
+
+	private       Point     mPatchViewSize; // View size on the basis of which the patch was created
+	private       Rect      mPatchArea;
+	private       ImageView mPatch;
+	private       Bitmap    mPatchBm;
+	private       CancellableAsyncTask<Void,Void> mDrawPatch;
+	private       RectF     mSearchBoxes[];
+	protected     Link      mLinks[];
+	private       View      mSearchView;
+	private       boolean   mIsBlank;
+	private       boolean   mHighlightLinks;
+
+	private       ProgressBar mBusyIndicator;
+	private final Handler   mHandler = new Handler();
+
+	public PageView(Context c, MuPDFCore core, Point parentSize, Bitmap sharedHqBm) {
+		super(c);
+		mContext = c;
+		mCore = core;
+		mParentSize = parentSize;
+		setBackgroundColor(BACKGROUND_COLOR);
+		mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
+		mPatchBm = sharedHqBm;
+		mEntireMat = new Matrix();
+	}
+
+	private void reinit() {
+		// Cancel pending render task
+		if (mDrawEntire != null) {
+			mDrawEntire.cancel();
+			mDrawEntire = null;
+		}
+
+		if (mDrawPatch != null) {
+			mDrawPatch.cancel();
+			mDrawPatch = null;
+		}
+
+		if (mGetLinkInfo != null) {
+			mGetLinkInfo.cancel(true);
+			mGetLinkInfo = null;
+		}
+
+		mIsBlank = true;
+		mPageNumber = 0;
+
+		if (mSize == null)
+			mSize = mParentSize;
+
+		if (mEntire != null) {
+			mEntire.setImageBitmap(null);
+			mEntire.invalidate();
+		}
+
+		if (mPatch != null) {
+			mPatch.setImageBitmap(null);
+			mPatch.invalidate();
+		}
+
+		mPatchViewSize = null;
+		mPatchArea = null;
+
+		mSearchBoxes = null;
+		mLinks = null;
+	}
+
+	public void releaseResources() {
+		reinit();
+
+		if (mBusyIndicator != null) {
+			removeView(mBusyIndicator);
+			mBusyIndicator = null;
+		}
+	}
+
+	public void releaseBitmaps() {
+		reinit();
+
+		// recycle bitmaps before releasing them.
+
+		if (mEntireBm!=null)
+			mEntireBm.recycle();
+		mEntireBm = null;
+
+		if (mPatchBm!=null)
+			mPatchBm.recycle();
+		mPatchBm = null;
+	}
+
+	public void blank(int page) {
+		reinit();
+		mPageNumber = page;
+
+		if (mBusyIndicator == null) {
+			mBusyIndicator = new ProgressBar(mContext);
+			mBusyIndicator.setIndeterminate(true);
+			addView(mBusyIndicator);
+		}
+
+		setBackgroundColor(BACKGROUND_COLOR);
+	}
+
+	public void setPage(int page, PointF size) {
+		// Cancel pending render task
+		if (mDrawEntire != null) {
+			mDrawEntire.cancel();
+			mDrawEntire = null;
+		}
+
+		mIsBlank = false;
+		// Highlights may be missing because mIsBlank was true on last draw
+		if (mSearchView != null)
+			mSearchView.invalidate();
+
+		mPageNumber = page;
+		if (mEntire == null) {
+			mEntire = new OpaqueImageView(mContext);
+			mEntire.setScaleType(ImageView.ScaleType.MATRIX);
+			addView(mEntire);
+		}
+
+		// Calculate scaled size that fits within the screen limits
+		// This is the size at minimum zoom
+		mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
+		Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
+		mSize = newSize;
+
+		mEntire.setImageBitmap(null);
+		mEntire.invalidate();
+
+		// Get the link info in the background
+		mGetLinkInfo = new AsyncTask<Void,Void,Link[]>() {
+			protected Link[] doInBackground(Void... v) {
+				return getLinkInfo();
+			}
+
+			protected void onPostExecute(Link[] v) {
+				mLinks = v;
+				if (mSearchView != null)
+					mSearchView.invalidate();
+			}
+		};
+
+		mGetLinkInfo.execute();
+
+		// Render the page in the background
+		mDrawEntire = new CancellableAsyncTask<Void, Void>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
+
+			@Override
+			public void onPreExecute() {
+				setBackgroundColor(BACKGROUND_COLOR);
+				mEntire.setImageBitmap(null);
+				mEntire.invalidate();
+
+				if (mBusyIndicator == null) {
+					mBusyIndicator = new ProgressBar(mContext);
+					mBusyIndicator.setIndeterminate(true);
+					addView(mBusyIndicator);
+					mBusyIndicator.setVisibility(INVISIBLE);
+					mHandler.postDelayed(new Runnable() {
+						public void run() {
+							if (mBusyIndicator != null)
+								mBusyIndicator.setVisibility(VISIBLE);
+						}
+					}, PROGRESS_DIALOG_DELAY);
+				}
+			}
+
+			@Override
+			public void onPostExecute(Void result) {
+				removeView(mBusyIndicator);
+				mBusyIndicator = null;
+				mEntire.setImageBitmap(mEntireBm);
+				mEntire.invalidate();
+				setBackgroundColor(Color.TRANSPARENT);
+
+			}
+		};
+
+		mDrawEntire.execute();
+
+		if (mSearchView == null) {
+			mSearchView = new View(mContext) {
+				@Override
+				protected void onDraw(final Canvas canvas) {
+					super.onDraw(canvas);
+					// Work out current total scale factor
+					// from source to view
+					final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+					final Paint paint = new Paint();
+
+					if (!mIsBlank && mSearchBoxes != null) {
+						paint.setColor(HIGHLIGHT_COLOR);
+						for (RectF rect : mSearchBoxes)
+							canvas.drawRect(rect.left*scale, rect.top*scale,
+									rect.right*scale, rect.bottom*scale,
+									paint);
+					}
+
+					if (!mIsBlank && mLinks != null && mHighlightLinks) {
+						paint.setColor(LINK_COLOR);
+						for (Link link : mLinks)
+							canvas.drawRect(link.bounds.x0*scale, link.bounds.y0*scale,
+									link.bounds.x1*scale, link.bounds.y1*scale,
+									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();
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+		int x, y;
+		switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
+		case View.MeasureSpec.UNSPECIFIED:
+			x = mSize.x;
+			break;
+		default:
+			x = View.MeasureSpec.getSize(widthMeasureSpec);
+		}
+		switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
+		case View.MeasureSpec.UNSPECIFIED:
+			y = mSize.y;
+			break;
+		default:
+			y = View.MeasureSpec.getSize(heightMeasureSpec);
+		}
+
+		setMeasuredDimension(x, y);
+
+		if (mBusyIndicator != null) {
+			int limit = Math.min(mParentSize.x, mParentSize.y)/2;
+			mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
+		}
+	}
+
+	@Override
+	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+		int w = right-left;
+		int h = bottom-top;
+
+		if (mEntire != null) {
+			if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
+				mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
+				mEntire.setImageMatrix(mEntireMat);
+				mEntire.invalidate();
+			}
+			mEntire.layout(0, 0, w, h);
+		}
+
+		if (mSearchView != null) {
+			mSearchView.layout(0, 0, w, h);
+		}
+
+		if (mPatchViewSize != null) {
+			if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
+				// Zoomed since patch was created
+				mPatchViewSize = null;
+				mPatchArea = null;
+				if (mPatch != null) {
+					mPatch.setImageBitmap(null);
+					mPatch.invalidate();
+				}
+			} else {
+				mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
+			}
+		}
+
+		if (mBusyIndicator != null) {
+			int bw = mBusyIndicator.getMeasuredWidth();
+			int bh = mBusyIndicator.getMeasuredHeight();
+
+			mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
+		}
+	}
+
+	public void updateHq(boolean update) {
+		Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
+		if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
+			// If the viewArea's size matches the unzoomed size, there is no need for an hq patch
+			if (mPatch != null) {
+				mPatch.setImageBitmap(null);
+				mPatch.invalidate();
+			}
+		} else {
+			final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
+			final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
+
+			// Intersect and test that there is an intersection
+			if (!patchArea.intersect(viewArea))
+				return;
+
+			// Offset patch area to be relative to the view top left
+			patchArea.offset(-viewArea.left, -viewArea.top);
+
+			boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
+
+			// If being asked for the same area as last time and not because of an update then nothing to do
+			if (area_unchanged && !update)
+				return;
+
+			boolean completeRedraw = !(area_unchanged && update);
+
+			// Stop the drawing of previous patch if still going
+			if (mDrawPatch != null) {
+				mDrawPatch.cancel();
+				mDrawPatch = null;
+			}
+
+			// Create and add the image view if not already done
+			if (mPatch == null) {
+				mPatch = new OpaqueImageView(mContext);
+				mPatch.setScaleType(ImageView.ScaleType.MATRIX);
+				addView(mPatch);
+				mSearchView.bringToFront();
+			}
+
+			CancellableTaskDefinition<Void, Void> task;
+
+			if (completeRedraw)
+				task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
+								patchArea.left, patchArea.top,
+								patchArea.width(), patchArea.height());
+			else
+				task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
+						patchArea.left, patchArea.top,
+						patchArea.width(), patchArea.height());
+
+			mDrawPatch = new CancellableAsyncTask<Void,Void>(task) {
+
+				public void onPostExecute(Void result) {
+					mPatchViewSize = patchViewSize;
+					mPatchArea = patchArea;
+					mPatch.setImageBitmap(mPatchBm);
+					mPatch.invalidate();
+					//requestLayout();
+					// Calling requestLayout here doesn't lead to a later call to layout. No idea
+					// why, but apparently others have run into the problem.
+					mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
+				}
+			};
+
+			mDrawPatch.execute();
+		}
+	}
+
+	public void update() {
+		// Cancel pending render task
+		if (mDrawEntire != null) {
+			mDrawEntire.cancel();
+			mDrawEntire = null;
+		}
+
+		if (mDrawPatch != null) {
+			mDrawPatch.cancel();
+			mDrawPatch = null;
+		}
+
+		// Render the page in the background
+		mDrawEntire = new CancellableAsyncTask<Void, Void>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
+
+			public void onPostExecute(Void result) {
+				mEntire.setImageBitmap(mEntireBm);
+				mEntire.invalidate();
+			}
+		};
+
+		mDrawEntire.execute();
+
+		updateHq(true);
+	}
+
+	public void removeHq() {
+			// Stop the drawing of the patch if still going
+			if (mDrawPatch != null) {
+				mDrawPatch.cancel();
+				mDrawPatch = null;
+			}
+
+			// And get rid of it
+			mPatchViewSize = null;
+			mPatchArea = null;
+			if (mPatch != null) {
+				mPatch.setImageBitmap(null);
+				mPatch.invalidate();
+			}
+	}
+
+	public int getPage() {
+		return mPageNumber;
+	}
+
+	@Override
+	public boolean isOpaque() {
+		return true;
+	}
+
+	public Link 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;
+
+		if (mLinks != null)
+			for (Link l: mLinks)
+				if (l.bounds.contains(docRelX, docRelY))
+					return l;
+		return null;
+	}
+
+	protected CancellableTaskDefinition<Void, Void> getDrawPageTask(final Bitmap bm, final int sizeX, final int sizeY,
+			final int patchX, final int patchY, final int patchWidth, final int patchHeight) {
+		return new MuPDFCancellableTaskDefinition<Void, Void>() {
+			@Override
+			public Void doInBackground(Cookie cookie, Void ... params) {
+				// Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
+				// is not incremented when drawing.
+				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
+						Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+					bm.eraseColor(0);
+				mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
+				return null;
+			}
+		};
+
+	}
+
+	protected CancellableTaskDefinition<Void, Void> getUpdatePageTask(final Bitmap bm, final int sizeX, final int sizeY,
+			final int patchX, final int patchY, final int patchWidth, final int patchHeight)
+	{
+		return new MuPDFCancellableTaskDefinition<Void, Void>() {
+			@Override
+			public Void doInBackground(Cookie cookie, Void ... params) {
+				// Workaround bug in Android Honeycomb 3.x, where the bitmap generation count
+				// is not incremented when drawing.
+				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB &&
+						Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+					bm.eraseColor(0);
+				mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
+				return null;
+			}
+		};
+	}
+
+	protected Link[] getLinkInfo() {
+		return mCore.getPageLinks(mPageNumber);
+	}
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java b/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
new file mode 100644
index 0000000..b7ea3f3
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/ReaderView.java
@@ -0,0 +1,968 @@
+package com.artifex.mupdf.viewer;
+
+import com.artifex.mupdf.fitz.Link;
+
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+import java.util.Stack;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.SparseArray;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.WindowManager;
+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 Context mContext;
+	private boolean mLinksEnabled = false;
+	private boolean tapDisabled = false;
+	private int tapPageMargin;
+
+	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        = 64.0f;
+
+	private static final boolean HORIZONTAL_SCROLLING = true;
+
+	private Adapter           mAdapter;
+	private int               mCurrent;    // Adapter's index for the current view
+	private boolean           mResetLayout;
+	private final SparseArray<View>
+				  mChildViews = new SparseArray<View>(3);
+					       // Shadows the children of the adapter view
+					       // but with more sensible indexing
+	private final LinkedList<View>
+				  mViewCache = new LinkedList<View>();
+	private boolean           mUserInteracting;  // Whether the user is interacting
+	private boolean           mScaling;    // Whether the user is currently pinch zooming
+	private float             mScale     = 1.0f;
+	private int               mXScroll;    // Scroll amounts recorded from events.
+	private int               mYScroll;    // and then accounted for in onLayout
+	private GestureDetector mGestureDetector;
+	private ScaleGestureDetector mScaleGestureDetector;
+	private Scroller    mScroller;
+	private Stepper     mStepper;
+	private int               mScrollerLastX;
+	private int               mScrollerLastY;
+	private float		  mLastScaleFocusX;
+	private float		  mLastScaleFocusY;
+
+	protected Stack<Integer> mHistory;
+
+	static abstract class ViewMapper {
+		abstract void applyToView(View view);
+	}
+
+	public ReaderView(Context context) {
+		super(context);
+		setup(context);
+	}
+
+	public ReaderView(Context context, AttributeSet attrs) {
+		super(context, attrs);
+		setup(context);
+	}
+
+	public ReaderView(Context context, AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		setup(context);
+	}
+
+	private void setup(Context context)
+	{
+		mContext = context;
+		mGestureDetector = new GestureDetector(context, this);
+		mScaleGestureDetector = new ScaleGestureDetector(context, this);
+		mScroller = new Scroller(context);
+		mStepper = new Stepper(this, this);
+		mHistory = new Stack<Integer>();
+
+		// Get the screen size etc to customise tap margins.
+		// We calculate the size of 1 inch of the screen for tapping.
+		// On some devices the dpi values returned are wrong, so we
+		// sanity check it: we first restrict it so that we are never
+		// less than 100 pixels (the smallest Android device screen
+		// dimension I've seen is 480 pixels or so). Then we check
+		// to ensure we are never more than 1/5 of the screen width.
+		DisplayMetrics dm = new DisplayMetrics();
+		WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+		wm.getDefaultDisplay().getMetrics(dm);
+		tapPageMargin = (int)dm.xdpi;
+		if (tapPageMargin < 100)
+			tapPageMargin = 100;
+		if (tapPageMargin > dm.widthPixels/5)
+			tapPageMargin = dm.widthPixels/5;
+	}
+
+	public boolean popHistory() {
+		if (mHistory.empty())
+			return false;
+		setDisplayedViewIndex(mHistory.pop());
+		return true;
+	}
+
+	public void pushHistory() {
+		mHistory.push(mCurrent);
+	}
+
+	public int getDisplayedViewIndex() {
+		return mCurrent;
+	}
+
+	public void setDisplayedViewIndex(int i) {
+		if (0 <= i && i < mAdapter.getCount()) {
+			onMoveOffChild(mCurrent);
+			mCurrent = i;
+			onMoveToChild(i);
+			mResetLayout = true;
+			requestLayout();
+		}
+	}
+
+	public void moveToNext() {
+		View v = mChildViews.get(mCurrent+1);
+		if (v != null)
+			slideViewOntoScreen(v);
+	}
+
+	public void moveToPrevious() {
+		View v = mChildViews.get(mCurrent-1);
+		if (v != null)
+			slideViewOntoScreen(v);
+	}
+
+	// When advancing down the page, we want to advance by about
+	// 90% of a screenful. But we'd be happy to advance by between
+	// 80% and 95% if it means we hit the bottom in a whole number
+	// of steps.
+	private int smartAdvanceAmount(int screenHeight, int max) {
+		int advance = (int)(screenHeight * 0.9 + 0.5);
+		int leftOver = max % advance;
+		int steps = max / advance;
+		if (leftOver == 0) {
+			// We'll make it exactly. No adjustment
+		} else if ((float)leftOver / steps <= screenHeight * 0.05) {
+			// We can adjust up by less than 5% to make it exact.
+			advance += (int)((float)leftOver/steps + 0.5);
+		} else {
+			int overshoot = advance - leftOver;
+			if ((float)overshoot / steps <= screenHeight * 0.1) {
+				// We can adjust down by less than 10% to make it exact.
+				advance -= (int)((float)overshoot/steps + 0.5);
+			}
+		}
+		if (advance > max)
+			advance = max;
+		return advance;
+	}
+
+	public void smartMoveForwards() {
+		View v = mChildViews.get(mCurrent);
+		if (v == null)
+			return;
+
+		// The following code works in terms of where the screen is on the views;
+		// so for example, if the currentView is at (-100,-100), the visible
+		// region would be at (100,100). If the previous page was (2000, 3000) in
+		// size, the visible region of the previous page might be (2100 + GAP, 100)
+		// (i.e. off the previous page). This is different to the way the rest of
+		// the code in this file is written, but it's easier for me to think about.
+		// At some point we may refactor this to fit better with the rest of the
+		// code.
+
+		// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
+		int screenWidth = getWidth();
+		int screenHeight = getHeight();
+		// We might be mid scroll; we want to calculate where we scroll to based on
+		// where this scroll would end, not where we are now (to allow for people
+		// bashing 'forwards' very fast.
+		int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
+		int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
+		// right/bottom is in terms of pixels within the scaled document; e.g. 1000
+		int top = -(v.getTop() + mYScroll + remainingY);
+		int right = screenWidth -(v.getLeft() + mXScroll + remainingX);
+		int bottom = screenHeight+top;
+		// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
+		int docWidth = v.getMeasuredWidth();
+		int docHeight = v.getMeasuredHeight();
+
+		int xOffset, yOffset;
+		if (bottom >= docHeight) {
+			// We are flush with the bottom. Advance to next column.
+			if (right + screenWidth > docWidth) {
+				// No room for another column - go to next page
+				View nv = mChildViews.get(mCurrent+1);
+				if (nv == null) // No page to advance to
+					return;
+				int nextTop = -(nv.getTop() + mYScroll + remainingY);
+				int nextLeft = -(nv.getLeft() + mXScroll + remainingX);
+				int nextDocWidth = nv.getMeasuredWidth();
+				int nextDocHeight = nv.getMeasuredHeight();
+
+				// Allow for the next page maybe being shorter than the screen is high
+				yOffset = (nextDocHeight < screenHeight ? ((nextDocHeight - screenHeight)>>1) : 0);
+
+				if (nextDocWidth < screenWidth) {
+					// Next page is too narrow to fill the screen. Scroll to the top, centred.
+					xOffset = (nextDocWidth - screenWidth)>>1;
+				} else {
+					// Reset X back to the left hand column
+					xOffset = right % screenWidth;
+					// Adjust in case the previous page is less wide
+					if (xOffset + screenWidth > nextDocWidth)
+						xOffset = nextDocWidth - screenWidth;
+				}
+				xOffset -= nextLeft;
+				yOffset -= nextTop;
+			} else {
+				// Move to top of next column
+				xOffset = screenWidth;
+				yOffset = screenHeight - bottom;
+			}
+		} else {
+			// Advance by 90% of the screen height downwards (in case lines are partially cut off)
+			xOffset = 0;
+			yOffset = smartAdvanceAmount(screenHeight, docHeight - bottom);
+		}
+		mScrollerLastX = mScrollerLastY = 0;
+		mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
+		mStepper.prod();
+	}
+
+	public void smartMoveBackwards() {
+		View v = mChildViews.get(mCurrent);
+		if (v == null)
+			return;
+
+		// The following code works in terms of where the screen is on the views;
+		// so for example, if the currentView is at (-100,-100), the visible
+		// region would be at (100,100). If the previous page was (2000, 3000) in
+		// size, the visible region of the previous page might be (2100 + GAP, 100)
+		// (i.e. off the previous page). This is different to the way the rest of
+		// the code in this file is written, but it's easier for me to think about.
+		// At some point we may refactor this to fit better with the rest of the
+		// code.
+
+		// screenWidth/Height are the actual width/height of the screen. e.g. 480/800
+		int screenWidth = getWidth();
+		int screenHeight = getHeight();
+		// We might be mid scroll; we want to calculate where we scroll to based on
+		// where this scroll would end, not where we are now (to allow for people
+		// bashing 'forwards' very fast.
+		int remainingX = mScroller.getFinalX() - mScroller.getCurrX();
+		int remainingY = mScroller.getFinalY() - mScroller.getCurrY();
+		// left/top is in terms of pixels within the scaled document; e.g. 1000
+		int left = -(v.getLeft() + mXScroll + remainingX);
+		int top = -(v.getTop() + mYScroll + remainingY);
+		// docWidth/Height are the width/height of the scaled document e.g. 2000x3000
+		int docHeight = v.getMeasuredHeight();
+
+		int xOffset, yOffset;
+		if (top <= 0) {
+			// We are flush with the top. Step back to previous column.
+			if (left < screenWidth) {
+				/* No room for previous column - go to previous page */
+				View pv = mChildViews.get(mCurrent-1);
+				if (pv == null) /* No page to advance to */
+					return;
+				int prevDocWidth = pv.getMeasuredWidth();
+				int prevDocHeight = pv.getMeasuredHeight();
+
+				// Allow for the next page maybe being shorter than the screen is high
+				yOffset = (prevDocHeight < screenHeight ? ((prevDocHeight - screenHeight)>>1) : 0);
+
+				int prevLeft = -(pv.getLeft() + mXScroll);
+				int prevTop = -(pv.getTop() + mYScroll);
+				if (prevDocWidth < screenWidth) {
+					// Previous page is too narrow to fill the screen. Scroll to the bottom, centred.
+					xOffset = (prevDocWidth - screenWidth)>>1;
+				} else {
+					// Reset X back to the right hand column
+					xOffset = (left > 0 ? left % screenWidth : 0);
+					if (xOffset + screenWidth > prevDocWidth)
+						xOffset = prevDocWidth - screenWidth;
+					while (xOffset + screenWidth*2 < prevDocWidth)
+						xOffset += screenWidth;
+				}
+				xOffset -= prevLeft;
+				yOffset -= prevTop-prevDocHeight+screenHeight;
+			} else {
+				// Move to bottom of previous column
+				xOffset = -screenWidth;
+				yOffset = docHeight - screenHeight + top;
+			}
+		} else {
+			// Retreat by 90% of the screen height downwards (in case lines are partially cut off)
+			xOffset = 0;
+			yOffset = -smartAdvanceAmount(screenHeight, top);
+		}
+		mScrollerLastX = mScrollerLastY = 0;
+		mScroller.startScroll(0, 0, remainingX - xOffset, remainingY - yOffset, 400);
+		mStepper.prod();
+	}
+
+	public void resetupChildren() {
+		for (int i = 0; i < mChildViews.size(); i++)
+			onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
+	}
+
+	public void applyToChildren(ViewMapper mapper) {
+		for (int i = 0; i < mChildViews.size(); i++)
+			mapper.applyToView(mChildViews.valueAt(i));
+	}
+
+	public void refresh() {
+		mResetLayout = true;
+
+		mScale = 1.0f;
+		mXScroll = mYScroll = 0;
+
+		requestLayout();
+	}
+
+	public View getView(int i) {
+		return mChildViews.get(i);
+	}
+
+	public View getDisplayedView() {
+		return mChildViews.get(mCurrent);
+	}
+
+	public void run() {
+		if (!mScroller.isFinished()) {
+			mScroller.computeScrollOffset();
+			int x = mScroller.getCurrX();
+			int y = mScroller.getCurrY();
+			mXScroll += x - mScrollerLastX;
+			mYScroll += y - mScrollerLastY;
+			mScrollerLastX = x;
+			mScrollerLastY = y;
+			requestLayout();
+			mStepper.prod();
+		}
+		else if (!mUserInteracting) {
+			// End of an inertial scroll and the user is not interacting.
+			// The layout is stable
+			View v = mChildViews.get(mCurrent);
+			if (v != null)
+				postSettle(v);
+		}
+	}
+
+	public boolean onDown(MotionEvent arg0) {
+		mScroller.forceFinished(true);
+		return true;
+	}
+
+	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+			float velocityY) {
+		if (mScaling)
+			return true;
+
+		View v = mChildViews.get(mCurrent);
+		if (v != null) {
+			Rect bounds = getScrollBounds(v);
+			switch(directionOfTravel(velocityX, velocityY)) {
+			case MOVING_LEFT:
+				if (HORIZONTAL_SCROLLING && bounds.left >= 0) {
+					// Fling off to the left bring next view onto screen
+					View vl = mChildViews.get(mCurrent+1);
+
+					if (vl != null) {
+						slideViewOntoScreen(vl);
+						return true;
+					}
+				}
+				break;
+			case MOVING_UP:
+				if (!HORIZONTAL_SCROLLING && bounds.top >= 0) {
+					// Fling off to the top bring next view onto screen
+					View vl = mChildViews.get(mCurrent+1);
+
+					if (vl != null) {
+						slideViewOntoScreen(vl);
+						return true;
+					}
+				}
+				break;
+			case MOVING_RIGHT:
+				if (HORIZONTAL_SCROLLING && bounds.right <= 0) {
+					// Fling off to the right bring previous view onto screen
+					View vr = mChildViews.get(mCurrent-1);
+
+					if (vr != null) {
+						slideViewOntoScreen(vr);
+						return true;
+					}
+				}
+				break;
+			case MOVING_DOWN:
+				if (!HORIZONTAL_SCROLLING && bounds.bottom <= 0) {
+					// Fling off to the bottom bring previous view onto screen
+					View vr = mChildViews.get(mCurrent-1);
+
+					if (vr != null) {
+						slideViewOntoScreen(vr);
+						return true;
+					}
+				}
+				break;
+			}
+			mScrollerLastX = mScrollerLastY = 0;
+			// If the page has been dragged out of bounds then we want to spring back
+			// nicely. fling jumps back into bounds instantly, so we don't want to use
+			// fling in that case. On the other hand, we don't want to forgo a fling
+			// just because of a slightly off-angle drag taking us out of bounds other
+			// than in the direction of the drag, so we test for out of bounds only
+			// in the direction of travel.
+			//
+			// Also don't fling if out of bounds in any direction by more than fling
+			// margin
+			Rect expandedBounds = new Rect(bounds);
+			expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
+
+			if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
+					&& expandedBounds.contains(0, 0)) {
+				mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
+				mStepper.prod();
+			}
+		}
+
+		return true;
+	}
+
+	public void onLongPress(MotionEvent e) { }
+
+	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+			float distanceY) {
+		PageView pageView = (PageView)getDisplayedView();
+		if (!tapDisabled)
+			onDocMotion();
+		if (!mScaling) {
+			mXScroll -= distanceX;
+			mYScroll -= distanceY;
+			requestLayout();
+		}
+		return true;
+	}
+
+	public void onShowPress(MotionEvent e) { }
+
+	public boolean onScale(ScaleGestureDetector detector) {
+		float previousScale = mScale;
+		mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), MIN_SCALE), MAX_SCALE);
+
+		{
+			float factor = mScale/previousScale;
+
+			View v = mChildViews.get(mCurrent);
+			if (v != null) {
+				float currentFocusX = detector.getFocusX();
+				float currentFocusY = detector.getFocusY();
+				// Work out the focus point relative to the view top left
+				int viewFocusX = (int)currentFocusX - (v.getLeft() + mXScroll);
+				int viewFocusY = (int)currentFocusY - (v.getTop() + mYScroll);
+				// Scroll to maintain the focus point
+				mXScroll += viewFocusX - viewFocusX * factor;
+				mYScroll += viewFocusY - viewFocusY * factor;
+
+				if (mLastScaleFocusX>=0)
+					mXScroll+=currentFocusX-mLastScaleFocusX;
+				if (mLastScaleFocusY>=0)
+					mYScroll+=currentFocusY-mLastScaleFocusY;
+
+				mLastScaleFocusX=currentFocusX;
+				mLastScaleFocusY=currentFocusY;
+				requestLayout();
+			}
+		}
+		return true;
+	}
+
+	public boolean onScaleBegin(ScaleGestureDetector detector) {
+		tapDisabled = true;
+		mScaling = true;
+		// Ignore any scroll amounts yet to be accounted for: the
+		// screen is not showing the effect of them, so they can
+		// only confuse the user
+		mXScroll = mYScroll = 0;
+		mLastScaleFocusX = mLastScaleFocusY = -1;
+		return true;
+	}
+
+	public void onScaleEnd(ScaleGestureDetector detector) {
+		mScaling = false;
+	}
+
+	@Override
+	public boolean onTouchEvent(MotionEvent event) {
+		if ((event.getAction() & event.getActionMasked()) == MotionEvent.ACTION_DOWN)
+		{
+			tapDisabled = false;
+		}
+
+		mScaleGestureDetector.onTouchEvent(event);
+		mGestureDetector.onTouchEvent(event);
+
+		if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+			mUserInteracting = true;
+		}
+		if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+			mUserInteracting = false;
+
+			View v = mChildViews.get(mCurrent);
+			if (v != null) {
+				if (mScroller.isFinished()) {
+					// If, at the end of user interaction, there is no
+					// current inertial scroll in operation then animate
+					// the view onto screen if necessary
+					slideViewOntoScreen(v);
+				}
+
+				if (mScroller.isFinished()) {
+					// If still there is no inertial scroll in operation
+					// then the layout is stable
+					postSettle(v);
+				}
+			}
+		}
+
+		requestLayout();
+		return true;
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+		int n = getChildCount();
+		for (int i = 0; i < n; i++)
+			measureView(getChildAt(i));
+	}
+
+	@Override
+	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+		super.onLayout(changed, left, top, right, bottom);
+
+		try {
+			onLayout2(changed, left, top, right, bottom);
+		}
+		catch (java.lang.OutOfMemoryError e) {
+			System.out.println("Out of memory during layout");
+		}
+	}
+
+	private void onLayout2(boolean changed, int left, int top, int right,
+			int bottom) {
+
+		// "Edit mode" means when the View is being displayed in the Android GUI editor. (this class
+		// is instantiated in the IDE, so we need to be a bit careful what we do).
+		if (isInEditMode())
+			return;
+
+		View cv = mChildViews.get(mCurrent);
+		Point cvOffset;
+
+		if (!mResetLayout) {
+			// Move to next or previous if current is sufficiently off center
+			if (cv != null) {
+				boolean move;
+				cvOffset = subScreenSizeOffset(cv);
+				// cv.getRight() may be out of date with the current scale
+				// so add left to the measured width for the correct position
+				if (HORIZONTAL_SCROLLING)
+					move = cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2;
+				else
+					move = cv.getTop() + cv.getMeasuredHeight() + cvOffset.y + GAP/2 + mYScroll < getHeight()/2;
+				if (move && mCurrent + 1 < mAdapter.getCount()) {
+					postUnsettle(cv);
+					// post to invoke test for end of animation
+					// where we must set hq area for the new current view
+					mStepper.prod();
+
+					onMoveOffChild(mCurrent);
+					mCurrent++;
+					onMoveToChild(mCurrent);
+				}
+
+				if (HORIZONTAL_SCROLLING)
+					move = cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2;
+				else
+					move = cv.getTop() - cvOffset.y - GAP/2 + mYScroll >= getHeight()/2;
+				if (move && mCurrent > 0) {
+					postUnsettle(cv);
+					// post to invoke test for end of animation
+					// where we must set hq area for the new current view
+					mStepper.prod();
+
+					onMoveOffChild(mCurrent);
+					mCurrent--;
+					onMoveToChild(mCurrent);
+				}
+			}
+
+			// Remove not needed children and hold them for reuse
+			int numChildren = mChildViews.size();
+			int childIndices[] = new int[numChildren];
+			for (int i = 0; i < numChildren; i++)
+				childIndices[i] = mChildViews.keyAt(i);
+
+			for (int i = 0; i < numChildren; i++) {
+				int ai = childIndices[i];
+				if (ai < mCurrent - 1 || ai > mCurrent + 1) {
+					View v = mChildViews.get(ai);
+					onNotInUse(v);
+					mViewCache.add(v);
+					removeViewInLayout(v);
+					mChildViews.remove(ai);
+				}
+			}
+		} else {
+			mResetLayout = false;
+			mXScroll = mYScroll = 0;
+
+			// Remove all children and hold them for reuse
+			int numChildren = mChildViews.size();
+			for (int i = 0; i < numChildren; i++) {
+				View v = mChildViews.valueAt(i);
+				onNotInUse(v);
+				mViewCache.add(v);
+				removeViewInLayout(v);
+			}
+			mChildViews.clear();
+
+			// post to ensure generation of hq area
+			mStepper.prod();
+		}
+
+		// Ensure current view is present
+		int cvLeft, cvRight, cvTop, cvBottom;
+		boolean notPresent = (mChildViews.get(mCurrent) == null);
+		cv = getOrCreateChild(mCurrent);
+		// When the view is sub-screen-size in either dimension we
+		// offset it to center within the screen area, and to keep
+		// the views spaced out
+		cvOffset = subScreenSizeOffset(cv);
+		if (notPresent) {
+			// Main item not already present. Just place it top left
+			cvLeft = cvOffset.x;
+			cvTop = cvOffset.y;
+		} else {
+			// Main item already present. Adjust by scroll offsets
+			cvLeft = cv.getLeft() + mXScroll;
+			cvTop = cv.getTop() + mYScroll;
+		}
+		// Scroll values have been accounted for
+		mXScroll = mYScroll = 0;
+		cvRight = cvLeft + cv.getMeasuredWidth();
+		cvBottom = cvTop + cv.getMeasuredHeight();
+
+		if (!mUserInteracting && mScroller.isFinished()) {
+			Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+			cvRight += corr.x;
+			cvLeft += corr.x;
+			cvTop += corr.y;
+			cvBottom += corr.y;
+		} else if (HORIZONTAL_SCROLLING && cv.getMeasuredHeight() <= getHeight()) {
+			// When the current view is as small as the screen in height, clamp
+			// it vertically
+			Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+			cvTop += corr.y;
+			cvBottom += corr.y;
+		} else if (!HORIZONTAL_SCROLLING && cv.getMeasuredWidth() <= getWidth()) {
+			// When the current view is as small as the screen in width, clamp
+			// it horizontally
+			Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+			cvRight += corr.x;
+			cvLeft += corr.x;
+		}
+
+		cv.layout(cvLeft, cvTop, cvRight, cvBottom);
+
+		if (mCurrent > 0) {
+			View lv = getOrCreateChild(mCurrent - 1);
+			Point leftOffset = subScreenSizeOffset(lv);
+			if (HORIZONTAL_SCROLLING)
+			{
+				int gap = leftOffset.x + GAP + cvOffset.x;
+				lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
+						(cvBottom + cvTop - lv.getMeasuredHeight())/2,
+						cvLeft - gap,
+						(cvBottom + cvTop + lv.getMeasuredHeight())/2);
+			} else {
+				int gap = leftOffset.y + GAP + cvOffset.y;
+				lv.layout((cvLeft + cvRight - lv.getMeasuredWidth())/2,
+						cvTop - lv.getMeasuredHeight() - gap,
+						(cvLeft + cvRight + lv.getMeasuredWidth())/2,
+						cvTop - gap);
+			}
+		}
+
+		if (mCurrent + 1 < mAdapter.getCount()) {
+			View rv = getOrCreateChild(mCurrent + 1);
+			Point rightOffset = subScreenSizeOffset(rv);
+			if (HORIZONTAL_SCROLLING)
+			{
+				int gap = cvOffset.x + GAP + rightOffset.x;
+				rv.layout(cvRight + gap,
+						(cvBottom + cvTop - rv.getMeasuredHeight())/2,
+						cvRight + rv.getMeasuredWidth() + gap,
+						(cvBottom + cvTop + rv.getMeasuredHeight())/2);
+			} else {
+				int gap = cvOffset.y + GAP + rightOffset.y;
+				rv.layout((cvLeft + cvRight - rv.getMeasuredWidth())/2,
+						cvBottom + gap,
+						(cvLeft + cvRight + rv.getMeasuredWidth())/2,
+						cvBottom + gap + rv.getMeasuredHeight());
+			}
+		}
+
+		invalidate();
+	}
+
+	@Override
+	public Adapter getAdapter() {
+		return mAdapter;
+	}
+
+	@Override
+	public View getSelectedView() {
+		return null;
+	}
+
+	@Override
+	public void setAdapter(Adapter adapter) {
+
+		//  release previous adapter's bitmaps
+		if (null!=mAdapter && adapter!=mAdapter) {
+			if (adapter instanceof PageAdapter){
+				((PageAdapter) adapter).releaseBitmaps();
+			}
+		}
+
+		mAdapter = adapter;
+
+		requestLayout();
+	}
+
+	@Override
+	public void setSelection(int arg0) {
+		throw new UnsupportedOperationException(getContext().getString(R.string.not_supported));
+	}
+
+	private View getCached() {
+		if (mViewCache.size() == 0)
+			return null;
+		else
+			return mViewCache.removeFirst();
+	}
+
+	private View getOrCreateChild(int i) {
+		View v = mChildViews.get(i);
+		if (v == null) {
+			v = mAdapter.getView(i, getCached(), this);
+			addAndMeasureChild(i, v);
+			onChildSetup(i, v);
+		}
+
+		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 its 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);
+
+		// 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));
+	}
+
+	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
+		// won't be executed until after the system has performed
+		// layout.
+		post (new Runnable() {
+			public void run () {
+				onSettle(v);
+			}
+		});
+	}
+
+	private void postUnsettle(final View v) {
+		post (new Runnable() {
+			public void run () {
+				onUnsettle(v);
+			}
+		});
+	}
+
+	private void slideViewOntoScreen(View v) {
+		Point corr = getCorrection(getScrollBounds(v));
+		if (corr.x != 0 || corr.y != 0) {
+			mScrollerLastX = mScrollerLastY = 0;
+			mScroller.startScroll(0, 0, corr.x, corr.y, 400);
+			mStepper.prod();
+		}
+	}
+
+	private Point subScreenSizeOffset(View v) {
+		return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
+				Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
+	}
+
+	private static int directionOfTravel(float vx, float vy) {
+		if (Math.abs(vx) > 2 * Math.abs(vy))
+			return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
+		else if (Math.abs(vy) > 2 * Math.abs(vx))
+			return (vy > 0) ? MOVING_DOWN : MOVING_UP;
+		else
+			return MOVING_DIAGONALLY;
+	}
+
+	private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
+		switch (directionOfTravel(vx, vy)) {
+		case MOVING_DIAGONALLY: return bounds.contains(0, 0);
+		case MOVING_LEFT: return bounds.left <= 0;
+		case MOVING_RIGHT: return bounds.right >= 0;
+		case MOVING_UP: return bounds.top <= 0;
+		case MOVING_DOWN: return bounds.bottom >= 0;
+		default: throw new NoSuchElementException();
+		}
+	}
+
+	protected void onTapMainDocArea() {}
+	protected void onDocMotion() {}
+
+	public void setLinksEnabled(boolean b) {
+		mLinksEnabled = b;
+		resetupChildren();
+		invalidate();
+	}
+
+	public boolean onSingleTapUp(MotionEvent e) {
+		Link link = null;
+		if (!tapDisabled) {
+			PageView pageView = (PageView) getDisplayedView();
+			if (mLinksEnabled && pageView != null && (link = pageView.hitLink(e.getX(), e.getY())) != null) {
+				if (link.uri != null) {
+					Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link.uri));
+					mContext.startActivity(intent);
+				} else {
+					pushHistory();
+					setDisplayedViewIndex(link.page);
+				}
+			} else if (e.getX() < tapPageMargin) {
+				smartMoveBackwards();
+			} else if (e.getX() > super.getWidth() - tapPageMargin) {
+				smartMoveForwards();
+			} else if (e.getY() < tapPageMargin) {
+				smartMoveBackwards();
+			} else if (e.getY() > super.getHeight() - tapPageMargin) {
+				smartMoveForwards();
+			} else {
+				onTapMainDocArea();
+			}
+		}
+		return true;
+	}
+
+	protected void onChildSetup(int i, View v) {
+		if (SearchTaskResult.get() != null
+				&& SearchTaskResult.get().pageNumber == i)
+			((PageView) v).setSearchBoxes(SearchTaskResult.get().searchBoxes);
+		else
+			((PageView) v).setSearchBoxes(null);
+
+		((PageView) v).setLinkHighlighting(mLinksEnabled);
+	}
+
+	protected void onMoveToChild(int i) {
+		if (SearchTaskResult.get() != null
+				&& SearchTaskResult.get().pageNumber != i) {
+			SearchTaskResult.set(null);
+			resetupChildren();
+		}
+	}
+
+	protected void onMoveOffChild(int i) {
+	}
+
+	protected void onSettle(View v) {
+		// When the layout has settled ask the page to render
+		// in HQ
+		((PageView) v).updateHq(false);
+	}
+
+	protected void onUnsettle(View v) {
+		// When something changes making the previous settled view
+		// no longer appropriate, tell the page to remove HQ
+		((PageView) v).removeHq();
+	}
+
+	protected void onNotInUse(View v) {
+		((PageView) v).releaseResources();
+	}
+}
diff --git a/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
new file mode 100644
index 0000000..69b116c
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTask.java
@@ -0,0 +1,129 @@
+package com.artifex.mupdf.viewer;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.AsyncTask;
+
+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/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java
new file mode 100644
index 0000000..e70db39
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/SearchTaskResult.java
@@ -0,0 +1,24 @@
+package com.artifex.mupdf.viewer;
+
+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/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java b/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
new file mode 100644
index 0000000..633775b
--- /dev/null
+++ b/lib/src/main/java/com/artifex/mupdf/viewer/Stepper.java
@@ -0,0 +1,42 @@
+package com.artifex.mupdf.viewer;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.view.View;
+
+public class Stepper {
+	protected final View mPoster;
+	protected final Runnable mTask;
+	protected boolean mPending;
+
+	public Stepper(View v, Runnable r) {
+		mPoster = v;
+		mTask = r;
+		mPending = false;
+	}
+
+	@SuppressLint("NewApi")
+	public void prod() {
+		if (!mPending) {
+			mPending = true;
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+				mPoster.postOnAnimation(new Runnable() {
+					@Override
+					public void run() {
+						mPending = false;
+						mTask.run();
+					}
+				});
+			} else {
+				mPoster.post(new Runnable() {
+					@Override
+					public void run() {
+						mPending = false;
+						mTask.run();
+					}
+				});
+
+			}
+		}
+	}
+}
diff --git a/lib/src/main/res/drawable/button.xml b/lib/src/main/res/drawable/button.xml
new file mode 100644
index 0000000..162ab93
--- /dev/null
+++ b/lib/src/main/res/drawable/button.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:state_pressed="true">
+		<shape android:shape="oval">
+			<solid android:color="#a0a0a0" />
+			<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
+		</shape>
+	</item>
+	<item>
+		<shape android:shape="oval">
+			<solid android:color="@android:color/transparent" />
+			<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
+		</shape>
+	</item>
+</selector>
diff --git a/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml b/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
new file mode 100644
index 0000000..7428907
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_chevron_left_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml b/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml
new file mode 100644
index 0000000..36b411a
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_chevron_right_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/ic_close_white_24dp.xml b/lib/src/main/res/drawable/ic_close_white_24dp.xml
new file mode 100644
index 0000000..d11cc5c
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_close_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/ic_link_white_24dp.xml b/lib/src/main/res/drawable/ic_link_white_24dp.xml
new file mode 100644
index 0000000..d9f3fe3
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_link_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/ic_list_white_24dp.xml b/lib/src/main/res/drawable/ic_list_white_24dp.xml
new file mode 100644
index 0000000..7f5d99c
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_list_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/ic_search_white_24dp.xml b/lib/src/main/res/drawable/ic_search_white_24dp.xml
new file mode 100644
index 0000000..47432c1
--- /dev/null
+++ b/lib/src/main/res/drawable/ic_search_white_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+</vector>
diff --git a/lib/src/main/res/drawable/page_indicator.xml b/lib/src/main/res/drawable/page_indicator.xml
new file mode 100644
index 0000000..f2ca93c
--- /dev/null
+++ b/lib/src/main/res/drawable/page_indicator.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+	<solid android:color="@color/page_indicator" />
+	<padding android:left="12dp" android:top="4dp" android:right="12dp" android:bottom="4dp" />
+	<corners android:radius="6dp" />
+</shape>
diff --git a/lib/src/main/res/drawable/seek_line.xml b/lib/src/main/res/drawable/seek_line.xml
new file mode 100644
index 0000000..ee67d29
--- /dev/null
+++ b/lib/src/main/res/drawable/seek_line.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="line" >
+	<stroke android:width="2dp" android:color="@android:color/white" />
+</shape>
diff --git a/lib/src/main/res/drawable/seek_thumb.xml b/lib/src/main/res/drawable/seek_thumb.xml
new file mode 100644
index 0000000..2a0745c
--- /dev/null
+++ b/lib/src/main/res/drawable/seek_thumb.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+	<size android:width="12dp" android:height="12dp" />
+	<stroke android:width="2dp" android:color="@android:color/white" />
+</shape>
diff --git a/lib/src/main/res/layout/document_activity.xml b/lib/src/main/res/layout/document_activity.xml
new file mode 100644
index 0000000..68a6a83
--- /dev/null
+++ b/lib/src/main/res/layout/document_activity.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	>
+
+	<ViewAnimator
+		android:id="@+id/switcher"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		android:layout_centerHorizontal="true"
+		>
+
+		<LinearLayout
+			android:id="@+id/mainBar"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:orientation="horizontal"
+			android:background="@color/toolbar"
+			>
+
+			<TextView
+				android:id="@+id/docNameText"
+				android:layout_width="0dp"
+				android:layout_height="wrap_content"
+				android:layout_weight="1"
+				android:layout_gravity="center"
+				android:paddingLeft="16dp"
+				android:paddingRight="8dp"
+				android:singleLine="true"
+				android:ellipsize="end"
+				android:textSize="16dp"
+				android:textColor="@android:color/white"
+				/>
+
+			<ImageButton
+				android:id="@+id/linkButton"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_link_white_24dp"
+				/>
+
+			<ImageButton
+				android:id="@+id/searchButton"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_search_white_24dp"
+				/>
+
+			<ImageButton
+				android:id="@+id/outlineButton"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_list_white_24dp"
+				/>
+
+		</LinearLayout>
+
+		<LinearLayout
+			android:id="@+id/searchBar"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:orientation="horizontal"
+			android:background="@color/toolbar"
+			>
+
+			<ImageButton
+				android:id="@+id/searchClose"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_close_white_24dp"
+				/>
+
+			<EditText
+				android:id="@+id/searchText"
+				android:background="@android:color/transparent"
+				android:layout_width="0dp"
+				android:layout_height="wrap_content"
+				android:layout_weight="1"
+				android:layout_gravity="center"
+				android:inputType="text"
+				android:imeOptions="actionSearch"
+				android:singleLine="true"
+				android:hint="@string/search"
+				android:textSize="16dp"
+				android:textColor="@android:color/white"
+				android:textColorHighlight="#a0a0a0"
+				android:textColorHint="#a0a0a0"
+				/>
+
+			<ImageButton
+				android:id="@+id/searchBack"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_chevron_left_white_24dp"
+				/>
+
+			<ImageButton
+				android:id="@+id/searchForward"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:background="@drawable/button"
+				android:src="@drawable/ic_chevron_right_white_24dp"
+				/>
+
+		</LinearLayout>
+
+	</ViewAnimator>
+
+	<RelativeLayout
+		android:id="@+id/lowerButtons"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentBottom="true"
+		android:layout_centerHorizontal="true"
+		>
+
+		<SeekBar
+			android:id="@+id/pageSlider"
+			android:layout_width="match_parent"
+			android:layout_height="36dp"
+			android:layout_alignParentBottom="true"
+			android:layout_centerHorizontal="true"
+			android:layout_margin="0dp"
+			android:paddingLeft="16dp"
+			android:paddingRight="16dp"
+			android:paddingTop="12dp"
+			android:paddingBottom="8dp"
+			android:background="@color/toolbar"
+			android:thumb="@drawable/seek_thumb"
+			android:progressDrawable="@drawable/seek_line"
+			/>
+
+		<TextView
+			android:id="@+id/pageNumber"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_above="@+id/pageSlider"
+			android:layout_centerHorizontal="true"
+			android:layout_marginBottom="16dp"
+			android:background="@drawable/page_indicator"
+			android:textSize="16dp"
+			android:textColor="@android:color/white"
+			/>
+
+	</RelativeLayout>
+
+</RelativeLayout>
diff --git a/lib/src/main/res/values/colors.xml b/lib/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5c23909
--- /dev/null
+++ b/lib/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<color name="page_indicator">#C0202020</color>
+	<color name="toolbar">#C0202020</color>
+</resources>
diff --git a/lib/src/main/res/values/strings.xml b/lib/src/main/res/values/strings.xml
new file mode 100644
index 0000000..292ba78
--- /dev/null
+++ b/lib/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<string name="cancel">Cancel</string>
+	<string name="cannot_open_document">Cannot open document</string>
+	<string name="cannot_open_document_Reason">Cannot open document: %1$s</string>
+	<string name="dismiss">Dismiss</string>
+	<string name="enter_password">Enter password</string>
+	<string name="no_further_occurrences_found">No further occurrences found</string>
+	<string name="not_supported">Not supported</string>
+	<string name="okay">Okay</string>
+	<string name="search">Search&#x2026;</string>
+	<string name="searching_">Searching&#x2026;</string>
+	<string name="text_not_found">Text not found</string>
+</resources>