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…</string>
+ <string name="searching_">Searching…</string>
+ <string name="text_not_found">Text not found</string>
+</resources>