Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:51:39 +01:00
parent e09986deae
commit fa69fd81a1
48 changed files with 5156 additions and 0 deletions

68
lib/build.gradle Normal file
View file

@ -0,0 +1,68 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
group = 'com.artifex.mupdf'
version = '1.26.11a'
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.+'
if (file('../jni/build.gradle').isFile())
api project(':jni')
else
api 'com.artifex.mupdf:fitz:1.26.11'
}
android {
namespace 'com.artifex.mupdf.viewer'
compileSdkVersion 33
defaultConfig {
minSdkVersion 21
targetSdkVersion 35
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
project.afterEvaluate {
publishing {
publications {
release(MavenPublication) {
artifactId 'viewer'
artifact(bundleReleaseAar)
pom {
name = 'viewer'
url = 'http://www.mupdf.com'
licenses {
license {
name = 'GNU Affero General Public License'
url = 'https://www.gnu.org/licenses/agpl-3.0.html'
}
}
}
pom.withXml {
final dependenciesNode = asNode().appendNode('dependencies')
configurations.implementation.allDependencies.each {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
repositories {
maven {
name 'Local'
if (project.hasProperty('MAVEN_REPO')) {
url = MAVEN_REPO
} else {
url = "file://${System.properties['user.home']}/MAVEN"
}
}
}
}
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<activity
android:name=".DocumentActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- list the mime-types we know about -->
<data android:mimeType="application/pdf" />
<data android:mimeType="application/vnd.ms-xpsdocument" />
<data android:mimeType="application/oxps" />
<data android:mimeType="application/vnd.comicbook+zip" />
<data android:mimeType="application/x-cbz" />
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/x-fictionbook" />
<data android:mimeType="application/x-mobipocket-ebook" />
<!-- list application/octet-stream to catch the ones android doesn't recognize -->
<data android:mimeType="application/octet-stream" />
</intent-filter>
</activity>
<activity
android:name=".OutlineActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,88 @@
package com.artifex.mupdf.viewer;
import android.os.AsyncTask;
import android.util.Log;
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 String APP = "MuPDF";
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);
}
}

View file

@ -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();
}

View file

@ -0,0 +1,95 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.SeekableInputStream;
import android.util.Log;
import android.content.ContentResolver;
import android.net.Uri;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
public class ContentInputStream implements SeekableInputStream {
private final String APP = "MuPDF";
protected ContentResolver cr;
protected Uri uri;
protected InputStream is;
protected long length, p;
protected boolean mustReopenStream;
public ContentInputStream(ContentResolver cr, Uri uri, long size) throws IOException {
this.cr = cr;
this.uri = uri;
length = size;
mustReopenStream = false;
reopenStream();
}
public long seek(long offset, int whence) throws IOException {
long newp = p;
switch (whence) {
case SEEK_SET:
newp = offset;
break;
case SEEK_CUR:
newp = p + offset;
break;
case SEEK_END:
if (length < 0) {
byte[] buf = new byte[16384];
int k;
while ((k = is.read(buf)) != -1)
p += k;
length = p;
}
newp = length + offset;
break;
}
if (newp < p) {
if (!mustReopenStream) {
try {
is.skip(newp - p);
} catch (IOException x) {
Log.i(APP, "Unable to skip backwards, reopening input stream");
mustReopenStream = true;
}
}
if (mustReopenStream) {
reopenStream();
is.skip(newp);
}
} else if (newp > p) {
is.skip(newp - p);
}
return p = newp;
}
public long position() throws IOException {
return p;
}
public int read(byte[] buf) throws IOException {
int n = is.read(buf);
if (n > 0)
p += n;
else if (n < 0 && length < 0)
length = p;
return n;
}
public void reopenStream() throws IOException {
if (is != null)
{
is.close();
is = null;
}
is = cr.openInputStream(uri);
p = 0;
}
}

View file

@ -0,0 +1,844 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.SeekableInputStream;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
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.provider.OpenableColumns;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.util.DisplayMetrics;
import android.util.Log;
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.Toast;
import android.widget.ViewAnimator;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Locale;
public class DocumentActivity extends Activity
{
private final String APP = "MuPDF";
/* The core rendering instance */
enum TopBarMode {Main, Search, More};
private final int OUTLINE_REQUEST=0;
private MuPDFCore core;
private String mDocTitle;
private String mDocKey;
private ReaderView mDocView;
private View mButtonsView;
private boolean mButtonsVisible;
private EditText mPasswordView;
private TextView mDocNameView;
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 boolean mReturnToLibraryActivity = false;
protected int mDisplayDPI;
private int mLayoutEM = 10;
private int mLayoutW = 312;
private int mLayoutH = 504;
protected View mLayoutButton;
protected PopupMenu mLayoutPopupMenu;
private String toHex(byte[] digest) {
StringBuilder builder = new StringBuilder(2 * digest.length);
for (byte b : digest)
builder.append(String.format("%02x", b));
return builder.toString();
}
private MuPDFCore openBuffer(byte buffer[], String magic)
{
try
{
core = new MuPDFCore(buffer, magic);
}
catch (Exception e)
{
Log.e(APP, "Error opening document buffer: " + e);
return null;
}
return core;
}
private MuPDFCore openStream(SeekableInputStream stm, String magic)
{
try
{
core = new MuPDFCore(stm, magic);
}
catch (Exception e)
{
Log.e(APP, "Error opening document stream: " + e);
return null;
}
return core;
}
private MuPDFCore openCore(Uri uri, long size, String mimetype) throws IOException {
ContentResolver cr = getContentResolver();
Log.i(APP, "Opening document " + uri);
InputStream is = cr.openInputStream(uri);
byte[] buf = null;
int used = -1;
try {
final int limit = 8 * 1024 * 1024;
if (size < 0) { // size is unknown
buf = new byte[limit];
used = is.read(buf);
boolean atEOF = is.read() == -1;
if (used < 0 || (used == limit && !atEOF)) // no or partial data
buf = null;
} else if (size <= limit) { // size is known and below limit
buf = new byte[(int) size];
used = is.read(buf);
if (used < 0 || used < size) // no or partial data
buf = null;
}
if (buf != null && buf.length != used) {
byte[] newbuf = new byte[used];
System.arraycopy(buf, 0, newbuf, 0, used);
buf = newbuf;
}
} catch (OutOfMemoryError e) {
buf = null;
} finally {
is.close();
}
if (buf != null) {
Log.i(APP, " Opening document from memory buffer of size " + buf.length);
return openBuffer(buf, mimetype);
} else {
Log.i(APP, " Opening document from stream");
return openStream(new ContentInputStream(cr, uri, size), mimetype);
}
}
private void showCannotOpenDialog(String reason) {
Resources res = getResources();
AlertDialog alert = mAlertBuilder.create();
setTitle(String.format(Locale.ROOT, 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();
}
/** 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);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mDisplayDPI = (int)metrics.densityDpi;
mAlertBuilder = new AlertDialog.Builder(this);
if (core == null) {
if (savedInstanceState != null && savedInstanceState.containsKey("DocTitle")) {
mDocTitle = savedInstanceState.getString("DocTitle");
}
}
if (core == null) {
Intent intent = getIntent();
SeekableInputStream file;
mReturnToLibraryActivity = intent.getIntExtra(getComponentName().getPackageName() + ".ReturnToLibraryActivity", 0) != 0;
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
String mimetype = getIntent().getType();
if (uri == null) {
showCannotOpenDialog("No document uri to open");
return;
}
mDocKey = uri.toString();
Log.i(APP, "OPEN URI " + uri.toString());
Log.i(APP, " MAGIC (Intent) " + mimetype);
mDocTitle = null;
long size = -1;
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int idx;
idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_STRING)
mDocTitle = cursor.getString(idx);
idx = cursor.getColumnIndex(OpenableColumns.SIZE);
if (idx >= 0 && cursor.getType(idx) == Cursor.FIELD_TYPE_INTEGER)
size = cursor.getLong(idx);
if (size == 0)
size = -1;
}
} catch (Exception x) {
// Ignore any exception and depend on default values for title
// and size (unless one was decoded
} finally {
if (cursor != null)
cursor.close();
}
Log.i(APP, " NAME " + mDocTitle);
Log.i(APP, " SIZE " + size);
if (mimetype == null || mimetype.equals("application/octet-stream")) {
mimetype = getContentResolver().getType(uri);
Log.i(APP, " MAGIC (Resolved) " + mimetype);
}
if (mimetype == null || mimetype.equals("application/octet-stream")) {
mimetype = mDocTitle;
Log.i(APP, " MAGIC (Filename) " + mimetype);
}
try {
core = openCore(uri, size, mimetype);
SearchTaskResult.set(null);
} catch (Exception x) {
showCannotOpenDialog(x.toString());
return;
}
}
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 relayoutDocument() {
int loc = core.layout(mDocView.mCurrent, mLayoutW, mLayoutH, mLayoutEM);
mFlatOutline = null;
mDocView.mHistory.clear();
mDocView.refresh();
mDocView.setDisplayedViewIndex(loc);
}
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(Locale.ROOT, "%d / %d", i + 1, core.countPages()));
mPageSlider.setMax((core.countPages() - 1) * mPageSliderRes);
mPageSlider.setProgress(i * mPageSliderRes);
super.onMoveToChild(i);
}
@Override
protected void onTapMainDocArea() {
if (!mButtonsVisible) {
showButtons();
} else {
if (mTopBarMode == TopBarMode.Main)
hideButtons();
}
}
@Override
protected void onDocMotion() {
hideButtons();
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
if (core.isReflowable()) {
mLayoutW = w * 72 / mDisplayDPI;
mLayoutH = h * 72 / mDisplayDPI;
relayoutDocument();
} else {
refresh();
}
}
};
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
String docTitle = core.getTitle();
if (docTitle != null)
mDocNameView.setText(docTitle);
else
mDocNameView.setText(mDocTitle);
// 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.isReflowable()) {
mLayoutButton.setVisibility(View.VISIBLE);
mLayoutPopupMenu = new PopupMenu(this, mLayoutButton);
mLayoutPopupMenu.getMenuInflater().inflate(R.menu.layout_menu, mLayoutPopupMenu.getMenu());
mLayoutPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
float oldLayoutEM = mLayoutEM;
int id = item.getItemId();
if (id == R.id.action_layout_6pt) mLayoutEM = 6;
else if (id == R.id.action_layout_7pt) mLayoutEM = 7;
else if (id == R.id.action_layout_8pt) mLayoutEM = 8;
else if (id == R.id.action_layout_9pt) mLayoutEM = 9;
else if (id == R.id.action_layout_10pt) mLayoutEM = 10;
else if (id == R.id.action_layout_11pt) mLayoutEM = 11;
else if (id == R.id.action_layout_12pt) mLayoutEM = 12;
else if (id == R.id.action_layout_13pt) mLayoutEM = 13;
else if (id == R.id.action_layout_14pt) mLayoutEM = 14;
else if (id == R.id.action_layout_15pt) mLayoutEM = 15;
else if (id == R.id.action_layout_16pt) mLayoutEM = 16;
if (oldLayoutEM != mLayoutEM)
relayoutDocument();
return true;
}
});
mLayoutButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mLayoutPopupMenu.show();
}
});
}
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.putExtra("PALLETBUNDLE", Pallet.sendBundle(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"+mDocKey, 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 != null) {
mDocView.pushHistory();
mDocView.setDisplayedViewIndex(resultCode-RESULT_FIRST_USER);
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mDocKey != null && mDocView != null) {
if (mDocTitle != null)
outState.putString("DocTitle", mDocTitle);
// 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"+mDocKey, mDocView.getDisplayedViewIndex());
edit.apply();
}
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 (mDocKey != null && mDocView != null) {
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor edit = prefs.edit();
edit.putInt("page"+mDocKey, mDocView.getDisplayedViewIndex());
edit.apply();
}
}
public void onDestroy()
{
if (mDocView != null) {
mDocView.applyToChildren(new ReaderView.ViewMapper() {
@Override
public 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(Locale.ROOT, "%d / %d", index + 1, core.countPages()));
}
private void makeButtonsView() {
mButtonsView = getLayoutInflater().inflate(R.layout.document_activity, null);
mDocNameView = (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);
mLayoutButton = mButtonsView.findViewById(R.id.layoutButton);
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 == null || (mDocView != null && !mDocView.popHistory())) {
super.onBackPressed();
if (mReturnToLibraryActivity) {
Intent intent = getPackageManager().getLaunchIntentForPackage(getComponentName().getPackageName());
startActivity(intent);
}
}
}
}

View file

@ -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);
}

View file

@ -0,0 +1,232 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Cookie;
import com.artifex.mupdf.fitz.DisplayList;
import com.artifex.mupdf.fitz.Document;
import com.artifex.mupdf.fitz.Link;
import com.artifex.mupdf.fitz.Matrix;
import com.artifex.mupdf.fitz.Outline;
import com.artifex.mupdf.fitz.Page;
import com.artifex.mupdf.fitz.Quad;
import com.artifex.mupdf.fitz.Rect;
import com.artifex.mupdf.fitz.RectI;
import com.artifex.mupdf.fitz.SeekableInputStream;
import com.artifex.mupdf.fitz.android.AndroidDrawDevice;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.util.Log;
import java.util.ArrayList;
public class MuPDFCore
{
private final String APP = "MuPDF";
private int resolution;
private Document doc;
private Outline[] outline;
private int pageCount = -1;
private boolean reflowable = false;
private int currentPage;
private Page page;
private float pageWidth;
private float pageHeight;
private DisplayList displayList;
/* Default to "A Format" pocket book size. */
private int layoutW = 312;
private int layoutH = 504;
private int layoutEM = 10;
private MuPDFCore(Document doc) {
this.doc = doc;
doc.layout(layoutW, layoutH, layoutEM);
pageCount = doc.countPages();
reflowable = doc.isReflowable();
resolution = 160;
currentPage = -1;
}
public MuPDFCore(byte buffer[], String magic) {
this(Document.openDocument(buffer, magic));
}
public MuPDFCore(SeekableInputStream stm, String magic) {
this(Document.openDocument(stm, magic));
}
public String getTitle() {
return doc.getMetaData(Document.META_INFO_TITLE);
}
public int countPages() {
return pageCount;
}
public boolean isReflowable() {
return reflowable;
}
public synchronized int layout(int oldPage, int w, int h, int em) {
if (w != layoutW || h != layoutH || em != layoutEM) {
System.out.println("LAYOUT: " + w + "," + h);
layoutW = w;
layoutH = h;
layoutEM = em;
long mark = doc.makeBookmark(doc.locationFromPageNumber(oldPage));
doc.layout(layoutW, layoutH, layoutEM);
currentPage = -1;
pageCount = doc.countPages();
outline = null;
try {
outline = doc.loadOutline();
} catch (Exception ex) {
/* ignore error */
}
return doc.pageNumberFromLocation(doc.findBookmark(mark));
}
return oldPage;
}
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) {
if (page != null)
page.destroy();
page = null;
if (displayList != null)
displayList.destroy();
displayList = null;
page = null;
pageWidth = 0;
pageHeight = 0;
currentPage = -1;
if (doc != null) {
page = doc.loadPage(pageNum);
Rect b = page.getBounds();
pageWidth = b.x1 - b.x0;
pageHeight = b.y1 - b.y0;
}
currentPage = pageNum;
}
}
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 && page != null)
try {
displayList = page.toDisplayList();
} catch (Exception ex) {
displayList = null;
}
if (displayList == null || page == null)
return;
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);
try {
displayList.run(dev, ctm, cookie);
dev.close();
} finally {
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 != null ? page.getLinks() : null;
}
public synchronized int resolveLink(Link link) {
return doc.pageNumberFromLocation(doc.resolveLink(link));
}
public synchronized Quad[][] searchPage(int pageNum, String text) {
gotoPage(pageNum);
return page.search(text);
}
public synchronized boolean hasOutline() {
if (outline == null) {
try {
outline = doc.loadOutline();
} catch (Exception ex) {
/* ignore error */
}
}
return outline != null;
}
private void flattenOutlineNodes(ArrayList<OutlineActivity.Item> result, Outline list[], String indent) {
for (Outline node : list) {
if (node.title != null) {
int page = doc.pageNumberFromLocation(doc.resolveLink(node));
result.add(new OutlineActivity.Item(indent + node.title, 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) {
boolean authenticated = doc.authenticatePassword(password);
pageCount = doc.countPages();
reflowable = doc.isReflowable();
return authenticated;
}
}

View file

@ -0,0 +1,63 @@
package com.artifex.mupdf.viewer;
import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
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
{
private final String APP = "MuPDF";
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);
int idx = getIntent().getIntExtra("PALLETBUNDLE", -1);
Bundle bundle = Pallet.receiveBundle(idx);
if (bundle != null) {
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();
}
}

View file

@ -0,0 +1,105 @@
package com.artifex.mupdf.viewer;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.Log;
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 String APP = "MuPDF";
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() {
try {
return mCore.countPages();
} catch (RuntimeException e) {
return 0;
}
}
public Object getItem(int position) {
return null;
}
public long getItemId(int position) {
return 0;
}
public synchronized void releaseBitmaps()
{
// recycle and release the shared bitmap.
if (mSharedHqBm!=null)
mSharedHqBm.recycle();
mSharedHqBm = null;
}
public void refresh() {
mPageSizes.clear();
}
public synchronized 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())
{
if (parent.getWidth() > 0 && parent.getHeight() > 0)
mSharedHqBm = Bitmap.createBitmap(parent.getWidth(), parent.getHeight(), Bitmap.Config.ARGB_8888);
else
mSharedHqBm = null;
}
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) {
try {
return mCore.getPageSize(position);
} catch (RuntimeException e) {
return null;
}
}
@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;
}
}

View file

@ -0,0 +1,672 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Cookie;
import com.artifex.mupdf.fitz.Link;
import com.artifex.mupdf.fitz.Quad;
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.content.Intent;
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.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.FileUriExposedException;
import android.os.Handler;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
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.widget.Toast;
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 String APP = "MuPDF";
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, Boolean> 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, Boolean> mDrawPatch;
private Quad mSearchBoxes[][];
protected Link mLinks[];
private View mSearchView;
private boolean mIsBlank;
private boolean mHighlightLinks;
private ImageView mErrorIndicator;
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;
clearRenderError();
}
public void releaseResources() {
reinit();
if (mBusyIndicator != null) {
removeView(mBusyIndicator);
mBusyIndicator = null;
}
clearRenderError();
}
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);
}
protected void clearRenderError() {
if (mErrorIndicator == null)
return;
removeView(mErrorIndicator);
mErrorIndicator = null;
invalidate();
}
protected void setRenderError(String why) {
int page = mPageNumber;
reinit();
mPageNumber = page;
if (mBusyIndicator != null) {
removeView(mBusyIndicator);
mBusyIndicator = null;
}
if (mSearchView != null) {
removeView(mSearchView);
mSearchView = null;
}
if (mErrorIndicator == null) {
mErrorIndicator = new OpaqueImageView(mContext);
mErrorIndicator.setScaleType(ImageView.ScaleType.CENTER);
addView(mErrorIndicator);
Drawable mErrorIcon = getResources().getDrawable(R.drawable.ic_error_red_24dp);
mErrorIndicator.setImageDrawable(mErrorIcon);
mErrorIndicator.setBackgroundColor(BACKGROUND_COLOR);
}
setBackgroundColor(Color.TRANSPARENT);
mErrorIndicator.bringToFront();
mErrorIndicator.invalidate();
}
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 (size == null) {
setRenderError("Error loading page");
size = new PointF(612, 792);
}
// 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;
if (mErrorIndicator != null)
return;
if (mEntire == null) {
mEntire = new OpaqueImageView(mContext);
mEntire.setScaleType(ImageView.ScaleType.MATRIX);
addView(mEntire);
}
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, Boolean>(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(Boolean result) {
removeView(mBusyIndicator);
mBusyIndicator = null;
if (result.booleanValue()) {
clearRenderError();
mEntire.setImageBitmap(mEntireBm);
mEntire.invalidate();
} else {
setRenderError("Error rendering page");
}
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 (Quad[] searchBox : mSearchBoxes) {
for (Quad q : searchBox) {
Path path = new Path();
path.moveTo(q.ul_x * scale, q.ul_y * scale);
path.lineTo(q.ll_x * scale, q.ll_y * scale);
path.lineTo(q.lr_x * scale, q.lr_y * scale);
path.lineTo(q.ur_x * scale, q.ur_y * scale);
path.close();
canvas.drawPath(path, paint);
}
}
}
if (!mIsBlank && mLinks != null && mHighlightLinks) {
paint.setColor(LINK_COLOR);
for (Link link : mLinks)
canvas.drawRect(link.getBounds().x0*scale, link.getBounds().y0*scale,
link.getBounds().x1*scale, link.getBounds().y1*scale,
paint);
}
}
};
addView(mSearchView);
}
requestLayout();
}
public void setSearchBoxes(Quad 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);
}
if (mErrorIndicator != null) {
int limit = Math.min(mParentSize.x, mParentSize.y)/2;
mErrorIndicator.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);
}
if (mErrorIndicator != null) {
int bw = (int) (8.5 * mErrorIndicator.getMeasuredWidth());
int bh = (int) (11 * mErrorIndicator.getMeasuredHeight());
mErrorIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
}
}
public void updateHq(boolean update) {
if (mErrorIndicator != null) {
if (mPatch != null) {
mPatch.setImageBitmap(null);
mPatch.invalidate();
}
return;
}
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);
if (mSearchView != null)
mSearchView.bringToFront();
}
CancellableTaskDefinition<Void, Boolean> 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, Boolean>(task) {
public void onPostExecute(Boolean result) {
if (result.booleanValue()) {
mPatchViewSize = patchViewSize;
mPatchArea = patchArea;
clearRenderError();
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);
} else {
setRenderError("Error rendering patch");
}
}
};
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, Boolean>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
public void onPostExecute(Boolean result) {
if (result.booleanValue()) {
clearRenderError();
mEntire.setImageBitmap(mEntireBm);
mEntire.invalidate();
} else {
setRenderError("Error updating page");
}
}
};
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 int hitLink(Link link) {
if (link.isExternal()) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link.getURI()));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); // API>=21: FLAG_ACTIVITY_NEW_DOCUMENT
try {
mContext.startActivity(intent);
} catch (FileUriExposedException x) {
Log.e(APP, x.toString());
Toast.makeText(getContext(), "Android does not allow following file:// link: " + link.getURI(), Toast.LENGTH_LONG).show();
} catch (Throwable x) {
Log.e(APP, x.toString());
Toast.makeText(getContext(), x.getMessage(), Toast.LENGTH_LONG).show();
}
return 0;
} else {
return mCore.resolveLink(link);
}
}
public int 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.getBounds().contains(docRelX, docRelY))
return hitLink(l);
return 0;
}
protected CancellableTaskDefinition<Void, Boolean> 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, Boolean>() {
@Override
public Boolean doInBackground(Cookie cookie, Void ... params) {
if (bm == null)
return new Boolean(false);
// 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);
try {
mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
return new Boolean(true);
} catch (RuntimeException e) {
return new Boolean(false);
}
}
};
}
protected CancellableTaskDefinition<Void, Boolean> 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, Boolean>() {
@Override
public Boolean doInBackground(Cookie cookie, Void ... params) {
if (bm == null)
return new Boolean(false);
// 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);
try {
mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie);
return new Boolean(true);
} catch (RuntimeException e) {
return new Boolean(false);
}
}
};
}
protected Link[] getLinkInfo() {
try {
return mCore.getPageLinks(mPageNumber);
} catch (RuntimeException e) {
return null;
}
}
}

View file

@ -0,0 +1,39 @@
package com.artifex.mupdf.viewer;
import android.os.Bundle;
import java.util.HashMap;
import java.util.Map;
public class Pallet {
private static Pallet instance = new Pallet();
private final Map<Integer, Object> pallet = new HashMap<>();
private int sequenceNumber = 0;
private Pallet() {
}
private static Pallet getInstance() {
return instance;
}
public static int sendBundle(Bundle bundle) {
Pallet instance = getInstance();
int i = instance.sequenceNumber++;
if (instance.sequenceNumber < 0)
instance.sequenceNumber = 0;
instance.pallet.put(new Integer(i), bundle);
return i;
}
public static Bundle receiveBundle(int number) {
Bundle bundle = (Bundle) getInstance().pallet.get(new Integer(number));
if (bundle != null)
getInstance().pallet.remove(new Integer(number));
return bundle;
}
public static boolean hasBundle(int number) {
return getInstance().pallet.containsKey(new Integer(number));
}
}

View file

@ -0,0 +1,980 @@
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.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
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 final String APP = "MuPDF";
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 PageAdapter mAdapter;
protected 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;
public interface ViewMapper {
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 void clearHistory() {
mHistory.clear();
}
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;
/* All page views need recreating since both page and screen has changed size,
* invalidating both sizes and bitmaps. */
mAdapter.refresh();
int numChildren = mChildViews.size();
for (int i = 0; i < mChildViews.size(); i++) {
View v = mChildViews.valueAt(i);
onNotInUse(v);
removeViewInLayout(v);
}
mChildViews.clear();
mViewCache.clear();
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) {
if (mAdapter != null && mAdapter != adapter)
mAdapter.releaseBitmaps();
mAdapter = (PageAdapter) 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) {
int page = pageView.hitLink(e.getX(), e.getY());
if (page > 0) {
pushHistory();
setDisplayedViewIndex(page);
} else {
onTapMainDocArea();
}
} 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();
}
}

View file

@ -0,0 +1,133 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Quad;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.AsyncTask;
import android.util.Log;
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 final String APP = "MuPDF";
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);
Quad 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();
}
}

View file

@ -0,0 +1,24 @@
package com.artifex.mupdf.viewer;
import com.artifex.mupdf.fitz.Quad;
public class SearchTaskResult {
public final String txt;
public final int pageNumber;
public final Quad searchBoxes[][];
static private SearchTaskResult singleton;
SearchTaskResult(String _txt, int _pageNumber, Quad _searchBoxes[][]) {
txt = _txt;
pageNumber = _pageNumber;
searchBoxes = _searchBoxes;
}
static public SearchTaskResult get() {
return singleton;
}
static public void set(SearchTaskResult r) {
singleton = r;
}
}

View file

@ -0,0 +1,44 @@
package com.artifex.mupdf.viewer;
import android.annotation.SuppressLint;
import android.os.Build;
import android.util.Log;
import android.view.View;
public class Stepper {
private final String APP = "MuPDF";
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();
}
});
}
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="38.836"
android:viewportHeight="38.836">
<path
android:fillColor="#FFFF0000"
android:fillType="evenOdd"
android:pathData="M38.331,4.315 L34.521,0.505 19.418,15.609 4.315,0.505 0.505,4.315 15.609,19.418 0.505,34.521 4.315,38.331 19.418,23.228 34.521,38.331 38.331,34.521 23.228,19.418Z"
android:strokeColor="#FFFF0000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="0.715"/>
</vector>

View file

@ -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="M9,4v3h5v12h3L17,7h5L22,4L9,4zM3,12h3v7h3v-7h3L12,9L3,9v3z"/>
</vector>

View file

@ -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>

View file

@ -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>

View file

@ -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,9h14L17,7L3,7v2zM3,13h14v-2L3,11v2zM3,17h14v-2L3,15v2zM19,17h2v-2h-2v2zM19,7v2h2L21,7h-2zM19,13h2v-2h-2v2z"/>
</vector>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,165 @@
<?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"
android:keepScreenOn="true"
>
<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="16sp"
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/layoutButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_format_size_white_24dp"
android:visibility="gone"
/>
<ImageButton
android:id="@+id/outlineButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button"
android:src="@drawable/ic_toc_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="16sp"
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="16sp"
android:textColor="@android:color/white"
/>
</RelativeLayout>
</RelativeLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_layout_6pt" android:title="6pt" />
<item android:id="@+id/action_layout_7pt" android:title="7pt" />
<item android:id="@+id/action_layout_8pt" android:title="8pt" />
<item android:id="@+id/action_layout_9pt" android:title="9pt" />
<item android:id="@+id/action_layout_10pt" android:title="10pt" />
<item android:id="@+id/action_layout_11pt" android:title="11pt" />
<item android:id="@+id/action_layout_12pt" android:title="12pt" />
<item android:id="@+id/action_layout_13pt" android:title="13pt" />
<item android:id="@+id/action_layout_14pt" android:title="14pt" />
<item android:id="@+id/action_layout_15pt" android:title="15pt" />
<item android:id="@+id/action_layout_16pt" android:title="16pt" />
</menu>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="page_indicator">#C0202020</color>
<color name="toolbar">#C0202020</color>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cancel">Cancel</string>
<string name="cannot_open_document">Cannot open document</string>
<string name="cannot_open_document_Reason">Cannot open document: %1$s</string>
<string name="dismiss">Dismiss</string>
<string name="enter_password">Enter password</string>
<string name="no_further_occurrences_found">No further occurrences found</string>
<string name="not_supported">Not supported</string>
<string name="okay">Okay</string>
<string name="search">Search&#x2026;</string>
<string name="searching_">Searching&#x2026;</string>
<string name="text_not_found">Text not found</string>
</resources>